本文主要用於記錄在准備BAT面試中關於iOS遇到的問題和做一些相關面試題的筆記
iOS網絡層設計
1、網絡層和業務層的對接設計
-
使用哪種交互模式來和業務層對接 : 使用Delegate為主,目的是為了(1)減少代碼的分散度(2)減少業務層和網絡層的耦合,網絡層對於業務層應該是抽象的,隱藏了實現細節的 (3)只采用一種是限制了靈活性,方便進行維護
-
在網絡層不要濫用block :(1)block會延長對象的生命期,delegate則不會
(2)block適合於在每次回調的任務都不一樣的情況下,如果一樣則應使用delegate,蘋果內部的網絡層封裝為delegate(離散型),AF的網絡層封裝為block(集約型) -
使用一個reformer對象來封裝數據轉化邏輯,從而節省了業務層進行字典轉模型這樣類似的繁瑣操作,同時為了解決直接使用字典的可讀性差的問題,采用KPropertyStudentID這樣的const變量來作為字典的key。
-
使用離散型(delegate)的方式做網絡層封裝需要使用到繼承,使用一個BaseAPIManager作為父類,來處理所有需要集約化的操作(例如一些公用信息),然后讓很多子類來做離散化的操作
2、網絡層的安全防范
-
防止競爭對手使用自己的API,為自己的API設計一個簽名,服務端給出一個密鑰,在每次使用API的時候進行一個hash算法的操作,將hash出來的值和服務端hash出來的值進行一個對比,如果一樣,則表明是自己在使用API
-
防止中間人攻擊,使用較為安全的HTTPS協議,防止運營商在請求中加入廣告
MVC模式和MVVM模式的區別
1、MVC模式存在Controller中代碼臃腫的問題
之所以會出現MVC模式,是因為發現在開發中會有很多代碼可以進行復用,同時事實也正是如此,MVC三個沒款中,Model和View的代碼確實可以因為MVC模式而進行復用,在github上也有很多開源的項目中封裝了很多View,我們可以很方便得使用這些view,model類作為一個數據轉化邏輯的類也可以在同一個項目中進行多次復用,但是Controller卻很難在一個項目中進行復用,所以我們在寫代碼的時候盡量在Controller中只寫那些無法復用的代碼,例如將view添加到controller上,將model的數據傳給view等等,但是實際上很難做到這一點,往往有很多代碼我們都不知道放在哪里,到了最后便放在了controller里面,導致controller變得十分臃腫。
2、對MVC模式中的Controller進行瘦身
我們可以從下面幾點對Controller進行瘦身:
- 將添加view到controller上的代碼進行抽取到自定義的UIView中去封裝
- 將網絡請求抽取出來進行封裝
- 將數據獲取和數據的轉化邏輯抽取出來進行封裝
- 構造MVVM模式中的viewModel,其中封裝了數據的轉化邏輯
3、MVVM模式的認知
- MVVM模式存在雙向綁定技術,也就是說viewModel變化,那么model也跟着變化
- 雙向綁定會導致難以發現bug出現的位置
- 在大型項目中雙向綁定會導致內存消耗過大
4、總結
應該結合MVC和MVVM的各自的優點去讓Controller進行瘦身,而不應該盲目地去追求新技術,亦或是過於保守,不願意向前發展。
iOS中如何設置圓角
1、常規的設置方式帶來的性能損耗
使用cornerRadius屬性設置圓角是不會產生離屏渲染的,同時也不會真正在UI上產生圓角,這時候我們需要將masksToBounds設置為YES,才能夠產生在UI上的圓角效果,但是同時,這樣也會導致離屏渲染。產生離屏渲染對於性能上有很大的消耗,將會降低FPS幀數,原因是因為離屏渲染需要將圖像的處理放在屏幕之外的內存緩存區進行處理,處理結束之后才把得到的圖像放到主屏幕上。在這個過程中產生最大消耗的是兩次上下文的交互,將處理放到屏幕之外的緩存區,然后把得到的圖像放到主屏幕上。
2、使用不產生離屏渲染的方式來創造圓角
使用貝塞爾曲線UIBezierPath和Core Graphics框架畫出一個圓角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; imageView.image = [UIImage imageNamed:@"1"]; //開始對imageView進行畫圖 UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, [UIScreen mainScreen].scale); //使用貝塞爾曲線畫出一個圓形圖 [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip]; [imageView drawRect:imageView.bounds]; imageView.image = UIGraphicsGetImageFromCurrentImageContext(); //結束畫圖 UIGraphicsEndImageContext(); [self.view addSubview:imageView];
3、總結
- 如果屏幕中沒有很多的圓角的話,那么就采用常規的方式設置即可
- 如果屏幕中存在了大量的圓角的話,那么需要對圓角進行優化,防止離屏渲染
微信中點擊頭像放大動畫的思路
創建一個背景和新的UIImageView,UIImageView是位於背景之上的,先把背景的透明度改為0,然后進行動畫,動畫的效果是將新的UIImageView從原始的位置(這個位置是原來的UIImageView在新的背景上對應的frame)變化到放大的位置,然后監聽背景的點擊事件,點擊的時候進行透明度和frame的相反變化即可。具體過程我封裝好了上傳到Github了,點擊這里查看。
線上項目出現bug怎么解決
這里將會涉及到JSPatch這個框架的使用,這個框架的作用就是對bug進行熱修復..后續更新
iOS開發中有哪些情況會產生循環引用
1、block
2、delegate
3、NSTimer
解決辦法:使用一個NSTimer的Catagory,然后重寫初始化方法,在實現中利用block,從而在調用的時候可以使用weakSelf在block執行任務
autoreleasePool(加入到autoreleasePool中的對象)在什么時候釋放?
- RunLoop啟動的時候創建autoreleasePool
- RunLoop結束的時候銷毀autoreleasePool
- 當RunLoop進行休眠的時候,將會將之前的autoreasePool銷毀,同時創建新的autoreleasePool
iOS中的深淺復制
請查看這篇文章,講得很深入:iOS剖析深淺復制
iOS中的屬性修飾符
列舉的順序就是修飾符在聲明的時候的順序
1、原子性修飾符
- nonatomic:一般對於屬性都采用nonatomic來修飾,如果需要保證線程安全,則手動添加代碼進行保護
- atomic:默認是atomic,使用該修飾符,系統會對屬性進行原子性的保護操作,保證線程安全,但是會有性能損耗
2、讀寫權限修飾符
- readonly:只讀
- readwrite:默認為readwrite修飾
3、內存管理修飾符
- strong:強引用,主要有任何strong類型的指針指向對象,那么就不會被ARC銷毀
- copy:主要用於NSString、NSArray、NSDictionary以及block,前者是因為他們都有可變類型,后者是因為在MRC中,使用copy能夠將block從棧區拷貝到堆區,在ARC中使用strong和copy效果一樣,但是寫上copy仿佛在時刻提醒着我們編譯器幫我們進行了copy操作
- weak:弱引用:表示定義了一種非擁有關系,如果屬性所指向的對象被銷毀了,那么屬性值也會被清空,設置為nil指針
- assign:對於基本數據類型的修飾,只會單純進行賦值操作
- unsafe_unretained:由unsafe和unretained組成,unretained和weak相似,unsafe表示他是不安全的,可能引起野指針的出現,導致crash
- retain:ARC中引入了strong和weak,retain效果和strong等同
4、讀寫方法名修飾符
- setter:修改setter方法的名字
- getter:修改getter方法的名字
iOS中屬性內存管理修飾符中的那些CP
-
strong vs copy
self.name = anotherName;
例如上面的代碼,使用strong表示的是self.name和anotherName這兩個指針同時指向了一個對象,過程是self.name指向了anotherName指向的對象,而如果使用copy的話,self.name和anotherName這兩個指針同時指向了不同的對象,過程是copy會將anotherName所指向的對象拷貝一份出來(淺拷貝),然后讓self.name指向這個被拷貝出來的對象。
-
strong vs weak
只要存在strong類型修飾的屬性(指針)指向了一個對象,那么這個對象就不會被ARC銷毀,但是對於weak類型修飾的屬性(指針)指向了一個對象,如果這個對象被銷毀了,那么這個屬性(指針)就會被自動設置為nil。可以說weak類型的指針是沒有約束作用的,只是簡單弱弱地表示了一下關系。
這里還需要分析在聲明控件到底應該使用strong還是weak-
如果是使用storyboard:
-
如果是使用純代碼:
-
-
綜上,都應該使用weak去聲明控件,純代碼中如果使用了strong去聲明控件,那么有一種情況:如果將控件remove了,那么controller中的view里面的subviews所引用的那條線將會被切斷,但是strong屬性(指針)所引用的這條線依然存在,由於采用的是強引用,所以控件將不會被ARC給銷毀,那么就會一直占用內存,直到控制器銷毀。
-
-
weak vs assign
weak只能用於對象類型,assign可以用於基本類型,weak比起assign有一點更好,如果weak修飾的屬性指向的一個對象被銷毀了,那么這個屬性將會自動被設置為nil指針,如果assign修飾的屬性指向的一個對象被銷毀了,那么這個屬性不會被自動設置為nil,同時他也不知道所指向的對象已經被銷毀了,這樣就引發了野指針。 -
weak vs unsafe_unretained
如果weak修飾的屬性指向的一個對象被銷毀了,那么這個屬性將會自動被設置為nil指針,如果unsafe_unretained修飾的屬性指向的一個對象被銷毀了,那么這個屬性不會被自動設置為nil,同時他也不知道所指向的對象已經被銷毀了,這樣就引發了野指針。 -
assign vs unsafe_unretained
assign能修飾基本類型,unsafe_unretained只能修飾對象類型
iOS中的多線程
1、pthread
基於C語言,不常用
2、NSThread
需要自己手管理線程的生命周期,偶爾使用,例如獲取當前線程
[NSThread currentThread];
3、GCD(Grand Central Dispatch)
GCD是蘋果開發出來的多核編程的解決方案,雖然是基於C語言的,但是采用了block進行封裝,使用起來也很方便,同時也很重要,推薦使用GCD進行多線程編程
4、NSOperation
是蘋果對於GCD的封裝,效率不及GCD
iOS中的GCD
- 常規使用
主隊列:是一個特殊的串行隊列,在主線程中運行,用於刷新UI,是一個串行隊列
//串行隊列 dispatch_queue_t queue = dispatch_get_main_queue;
自定義創建隊列: 既可以創建串行隊列也可以創建並行隊列。
//串行隊列 dispatch_queue_t queue = dispatch_queue_create("nineteen", NULL); dispatch_queue_t queue = dispatch_queue_create("nineteen", DISPATCH_QUEUE_SERIAL); //並行隊列 dispatch_queue_t queue = dispatch_queue_create("nineteen", DISPATCH_QUEUE_CONCURRENT);
全局並行隊列:系統提供的並行隊列
//並行隊列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
- 其他使用
循環執行任務:dispatch_apply類似一個for循環,並發地執行每一項。所有任務結束后,dispatch_apply才會返回,會阻塞當前線程(類似同步執行)。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); /* *count: 循環執行次數 *queue: 隊列,可以是串行隊列或者是並行隊列(使用串行隊列可能導致死鎖) *block: 任務 */ dispatch_apply(count, queue, ^(size_t i) { NSLog(@"%zu %@", i, [NSThread currentThread]); });
隊列組:隊列組將很多隊列添加到一個組里,當組里所有任務都執行完后,它會通過一個方法通知我們。基本流程是首先創建一個隊列組,然后把任務添加到組中,最后等待隊列組的執行結果。
//創建隊列組 dispatch_group_t group = dispatch_group_create(); //創建隊列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //並行隊列執行3次循環 (隊列組只能用異步方法執行) dispatch_group_async(group, queue, ^{ for (NSInteger i = 0; i < 3; i++) { NSLog(@"group-01 - %@", [NSThread currentThread]); } }); //主隊列執行5次循環 dispatch_group_async(group, dispatch_get_main_queue(), ^{ for (NSInteger i = 0; i < 5; i++) { NSLog(@"group-02 - %@", [NSThread currentThread]); } }); //都完成后會自動通知 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"完成 - %@", [NSThread currentThread]); });
實現單例模式
static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //dispatch_once中的代碼只執行一次,常用來實現單例 });
GCD延遲操作
//創建隊列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //設置延時,單位秒 double delay = 3; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), queue, ^{ //3秒后需要執行的任務 });
GCD中的死鎖場景
五個案例了解GCD的死鎖
1、
案例:
NSLog(@"1"); // 任務1 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2"); // 任務2 }); NSLog(@"3"); // 任務3
結果:
1
2、
案例:
NSLog(@"1"); // 任務1 dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSLog(@"2"); // 任務2 }); NSLog(@"3"); // 任務3
結果:
1 2 3
3、
案例:
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL); NSLog(@"1"); // 任務1 dispatch_async(queue, ^{ NSLog(@"2"); // 任務2 dispatch_sync(queue, ^{ NSLog(@"3"); // 任務3 }); NSLog(@"4"); // 任務4 }); NSLog(@"5"); // 任務5
結果:
1 5 2 // 5和2的順序不一定
4、
案例:
NSLog(@"1"); // 任務1 dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"2"); // 任務2 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"3"); // 任務3 }); NSLog(@"4"); // 任務4 }); NSLog(@"5"); // 任務5
結果:
1 2 5 3 4 // 5和2的順序不一定
5、
案例:
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"1"); // 任務1 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2"); // 任務2 }); NSLog(@"3"); // 任務3 }); NSLog(@"4"); // 任務4 while (1) { } NSLog(@"5"); // 任務5
結果:
1 4 // 1和4的順序不一定
iOS中的遞歸鎖
如果加鎖操作處於一個循環或者遞歸中,在第一次加鎖還沒有解鎖的時候,就進行了第二次加鎖,所以就造成死鎖現象,這時候應該使用遞歸鎖來防止死鎖的發生。
iOS中的ARC是怎么解決內存管理問題的
ARC會自動處理對象的聲明周期,編譯的時候在合適的地方插入內存管理代碼
ARC中autorelease的使用場景
- 函數返回對象的時候:函數對象作為返回值過了作用域的時候應該被銷毀,但是這時候可能還沒有被賦值(被強引用),所以需要將該對象添加到自動釋放池中延長生命周期。
- _weak修飾屬性的時候:_weak修飾的屬性所指向的對象可能沒有一個強引用來引用他,可能會被銷毀,這時候就需要對其使用autorelease方法保證他不被銷毀
- id 的指針或對象的指針
iOS中的RunLoop
一般主線程會自動運行RunLoop,我們一般情況下不會去管。在其他子線程中,如果需要我們需要去管理。使用RunLoop后,可以把線程想象成進入了一個循環;如果沒有這個循環,子線程完成任務后,這個線程就結束了。所以如果需要一個線程處理各種事件而不讓它結束,就需要運行RunLoop。
SDWebImage是怎么使用RunLoop的
- (void)start{ ... self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; ... if(self.connection){ ... CFRunLoopRun( ) ... } }
- (void)cancelInternalAndStop { if (self.isFinished) return; [self cancelInternal]; CFRunLoopStop(CFRunLoopGetCurrent()); }
在創建self.connection成功后,執行了CFRunLoopRun(),開啟了runloop。在failed或finished的時候會調用CFRunLoopStop停止runloop。如果不開啟runloop的話,在執行完start ()后任務就完成了,NSURLConnection的代理就不會執行了。runloop相當於子線程的循環,可以靈活控制子線程的生命周期。
AFNetworking是怎么使用RunLoop的
AFNetworking解決這個問題采用了另一種方法:單獨起一個global thread,內置一個runloop,所有的connection都由這個runloop發起,回調也都由它接收。這是個不錯的想法,既不占用主線程,又不耗CPU資源:
iOS中的響應鏈
- 通過官方文檔提供的圖來看看事件響應
兩種方式,由於設置不同,但大致過程是一樣的

- 具體的響應過程
- 發生了觸摸事件后,系統會將該事件加入到UIApplication管理的一個隊列中
- UIApplication從隊列中取出最前面的事件,然后將事件傳遞下去,處理的順序大致為UIApplication->AppDelegate->UIWindow->UIViewController->superView->subViews
- 通常來說UIApplication會將事件先交給keyWindow來處理,keyWindow會找到一個最合適的視圖來處理這個事件,處理的第一步就從這里開始了
- keyWindow是這樣來找到合適視圖的:調用hitTest:withEvent方法去尋找能夠處理觸摸事件的視圖,hitTest:withEvent方法會遞歸地檢查view以及view的子類是否包含了觸摸點,像這樣一直遞歸下去,找到離用戶最近同時包含了觸摸點的一個view,然后將觸摸事件傳遞給這個view。在這個過程中是通過PointInside:withEvent:方法來判斷是否包含了觸摸點的,如果包含了,就返回YES,如果沒有包含就返回NO,然后hitTest:withEvent這個方法就返回nil,將不再對該視圖的子視圖進行判斷。
NSRunloop、runloop、autoreleasePool、thread
-
NSRunloop:NSRunloop是一個消息循環,它會檢測輸入元和定時源,然后做回調處理。NSRunloop封裝了windows中的消息處理,將SendMessage、PostMessage、GetMessage等細節封裝了起來。關於NSRunloop需要着重了解這幾點內容:
- NSRunloop用來監聽耗時的異步事件,例如網絡回調
- NSRunloop解決了CPU空轉問題,當沒有任何事件需要處理的時候,NSRunloop會把線程調整為休眠狀態,從而消除CPU的周期輪詢。
- 每一個線程都有一個NSRunloop,主線程是默認運行的,其他線程默認是沒有運行的,需要在NSRunloop中添加一個事件,然后去啟動這個線程的runloop。
-
runloop:新建iOS項目的時候會看到在main方法中會手動創建一個autoreleasePool,程序開始時創建,結束時銷毀,如果只是從表面上來看的話,那么這樣和內存泄露是沒有什么區別的。其實,對於每一個runloop,系統會隱式地創建一個autoreleasePool,這樣所有的autoreleasePool構成一個棧式的結構,在每一個runloop結束的時候,當前棧頂的autoreleasePool就會被彈出,同時銷毀,其中的所有對象也同樣被銷毀。這里所指的runloop不是NSRunloop,這里的runloop可能是一個UI事件,一個timer等等,具體來說指的是從接受到消息,到處理完這個消息的一個完整過程。
-
autoreleasePool和thread:
thread是不會自動創建autoreleasePool的
drawRect的作用
- drawRect方法的目的是進行UIView的繪制,使用的時候將繪制的具體內容寫在drawRect方法里面
- 蘋果不建議直接使用drawRect方法,而是調用setNeedsDisplay方法,系統接着會自動調用drawRect進行UIView的繪制
layoutSubviews的作用
- layoutSubviews的作用是對子視圖進行重新布局
- 蘋果不建議直接使用該方法,而是通過調用setLayoutSubviews,讓系統去自動調用layoutSubviews方法進行布局
- 下面列出在什么時候會出發layoutSubviews方法
- 直接調用setLayoutSubviews。(這個在上面蘋果官方文檔里有說明)
- addSubview的時候。
- 當view的frame發生改變的時候。
- 滑動UIScrollView的時候。
- 旋轉Screen會觸發父UIView上的layoutSubviews事件。
- 改變一個UIView大小的時候也會觸發父UIView上的layoutSubviews事件。
自定義控件
- 自定義控件分為兩種,一種是通過xib,另一種是通過純代碼的方式
- 自定義控件需要在兩個方法中進行重寫,達到開發者想要的效果
- initWithFrame:一般來說是重寫這個方法而不是init方法,因為init方法最終也會調用到initWithFrame方法,這個方法主要是對控件以及控件的子控件進行一些初始化的設置(如果是通過xib的話則是重寫awakFromNib方法)
- layoutSubviews:這個方法是描述子控件如何布局,也就是賦予子控件在自定義控件中的位置關系,所以說對於子控件的frame的設置代碼不應該放在initWithFrame中,而是應該放在layoutSubviews這個方法里面
數據持久化的幾種方式的對比
-
Plist文件(屬性列表):
plist文件是將某些特定的類,通過XML文件的方式保存在目錄中,這些類包括(如果存在對應的可變類也包括可變類)
NSArray
NSDictionary
NSData
NSString
NSNumber
NSDate -
Preference(偏好設置):
- 偏好設置是專門用來保存應用程序的配置信息的
- 如果沒有調用synchronize方法,系統會根據I/O情況不定時刻地保存到文件中。所以如果需要立即寫入文件的就必須調用synchronize方法。
- 偏好設置會將所有數據保存到同一個文件中。即preference目錄下的一個以此應用包名來命名的plist文件。
-
NSKeyedArchiver(歸檔):
歸檔在iOS中是另一種形式的序列化,只要遵循了NSCoding協議的對象都可以通過它實現序列化 - SQLite 3:
之前的所有存儲方法,都是覆蓋存儲。如果想要增加一條數據就必須把整個文件讀出來,然后修改數據后再把整個內容覆蓋寫入文件。所以它們都不適合存儲大量的內容,而SQLite 3卻能更好進行大量內容的讀寫操作。 - CoreData:
蘋果封裝的本地數據庫,一般用於規划應用中的對象
app的狀態
- Not running:app還沒運行
- Inactive:app運行在foreground但沒有接收事件
- Active:app運行在foreground和正在接收事件
- Background:運行在background和正在執行代碼
- Suspended:運行在background但沒有執行代碼
UIView和CALayer的區別
- UIView可以響應事件,而CALayer不行
- 一個 Layer 的 frame 是由它的 anchorPoint,position,bounds,和 transform 共同決定的,而一個 View 的 frame 只是簡單的返回 Layer的 frame,同樣 View 的 center和 bounds 也是返回 Layer 的一些屬性。
- UIView主要是對顯示內容的管理而 CALayer 主要側重顯示內容的繪制。
- 在做 iOS 動畫的時候,修改非 RootLayer的屬性(譬如位置、背景色等)會默認產生隱式動畫,而修改UIView則不會。
KVO的實現原理
- KVO是什么:
KVO提供一種機制,指定一個被觀察對象(例如A類),當對象某個屬性(例如A中的字符串name)發生更改時,對象會獲得通知,並作出相應處理 - KVO的原理:
- NSKVONotifying_A:
在這個過程,被觀察對象的 isa 指針從指向原來的A類,被KVO機制修改為指向系統新創建的子類 NSKVONotifying_A類,來實現當前類屬性值改變的監聽;所以當我們從應用層面上看來,完全沒有意識到有新的類出現,這是系統“隱瞞”了對KVO的底層實現過程,讓我們誤以為還是原來的類。 - 子類setter方法剖析:KVO的鍵值觀察通知依賴於 NSObject 的兩個方法:willChangeValueForKey:和 didChangevlueForKey:,在存取數值的前后分別調用2個方法:被觀察屬性發生改變之前,willChangeValueForKey:被調用,通知系統該 keyPath 的屬性值即將變更;當改變發生后, didChangeValueForKey: 被調用,通知系統該 keyPath 的屬性值已經變更;之后, observeValueForKey:ofObject:change:context: 也會被調用。
- NSKVONotifying_A: