對小碼哥底層班視頻學習的總結與記錄。面試題部分,經過對面試題的分析探索問題的本質內容。面試
問題bash
- iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼?)
- 如何手動觸發KVO
首先須要瞭解KVO基本使用,KVO的全稱 Key-Value Observing,俗稱「鍵值監聽」,能夠用於監聽某個對象屬性值的改變。markdown
- (void)viewDidLoad { [super viewDidLoad]; Person *p1 = [[Person alloc] init]; Person *p2 = [[Person alloc] init]; p1.age = 1; p1.age = 2; p2.age = 2; // self 監聽 p1的 age屬性 NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; p1.age = 10; [p1 removeObserver:self forKeyPath:@"age"]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"監聽到%@的%@改變了%@", object, keyPath,change); } // 打印內容 監聽到<Person: 0x604000205460>的age改變了{ kind = 1; new = 10; old = 2; } 複製代碼
上述代碼中能夠看出,在添加監聽以後,age屬性的值在發生改變時,就會通知到監聽者,執行監聽者的observeValueForKeyPath方法。app
經過上述代碼咱們發現,一旦age屬性的值發生改變時,就會通知到監聽者,而且咱們知道賦值操做都是調用 set方法,咱們能夠來到Person類中重寫age的set方法,觀察是不是KVO在set方法內部作了一些操做來通知監聽者。框架
咱們發現即便重寫了set方法,p1對象和p2對象調用一樣的set方法,可是咱們發現p1除了調用set方法以外還會另外執行監聽器的observeValueForKeyPath方法。函數
說明KVO在運行時獲取對p1對象作了一些改變。至關於在程序運行過程當中,對p1對象作了一些變化,使得p1對象在調用setage方法的時候可能作了一些額外的操做,因此問題出在對象身上,兩個對象在內存中確定不同,兩個對象可能本質上並不同。接下來來探索KVO內部是怎麼實現的。學習
首先咱們對上述代碼中添加監聽的地方打斷點,看觀察一下,addObserver方法對p1對象作了什麼處理?也就是說p1對象在通過addObserver方法以後發生了什麼改變,咱們經過打印isa指針以下圖所示spa
經過上圖咱們發現,p1對象執行過addObserver操做以後,p1對象的isa指針由以前的指向類對象Person變爲指向NSKVONotifyin_Person類對象,而p2對象沒有任何改變。也就是說一旦p1對象添加了KVO監聽之後,其isa指針就會發生變化,所以set方法的執行效果就不同了。命令行
那麼咱們先來觀察p2對象在內容中是如何存儲的,而後對比p2來觀察p1。 首先咱們知道,p2在調用setage方法的時候,首先會經過p2對象中的isa指針找到Person類對象,而後在類對象中找到setage方法。而後找到方法對應的實現。以下圖所示3d
可是剛纔咱們發現p1對象的isa指針在通過KVO監聽以後已經指向了NSKVONotifyin_Person類對象,NSKVONotifyin_Person實際上是Person的子類,那麼也就是說其superclass指針是指向Person類對象的,NSKVONotifyin_Person是runtime在運行時生成的。那麼p1對象在調用setage方法的時候,確定會根據p1的isa找到NSKVONotifyin_Person,在NSKVONotifyin_Person中找setage的方法及實現。
通過查閱資料咱們能夠了解到。 NSKVONotifyin_Person中的setage方法中其實調用了 Fundation框架中C語言函數 _NSsetIntValueAndNotify,_NSsetIntValueAndNotify內部作的操做至關於,首先調用willChangeValueForKey 將要改變方法,以後調用父類的setage方法對成員變量賦值,最後調用didChangeValueForKey已經改變方法。didChangeValueForKey中會調用監聽器的監聽方法,最終來到監聽者的observeValueForKeyPath方法中。
首先通過以前打斷點打印isa指針,咱們已經驗證了,在執行添加監聽的方法時,會將isa指針指向一個經過runtime建立的Person的子類NSKVONotifyin_Person。 另外咱們能夠經過打印方法實現的地址來看一下p1和p2的setage的方法實現的地址在添加KVO先後有什麼變化。
// 經過methodForSelector找到方法實現的地址 NSLog(@"添加KVO監聽以前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]); NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; NSLog(@"添加KVO監聽以後 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]); 複製代碼
咱們發如今添加KVO監聽以前,p1和p2的setAge方法實現的地址相同,而通過KVO監聽以後,p1的setAge方法實現的地址發生了變化,咱們經過打印方法實現來看一下先後的變化發現,確實如咱們上面所講的同樣,p1的setAge方法的實現由Person類方法中的setAge方法轉換爲了C語言的Foundation框架的_NSsetIntValueAndNotify函數。
Foundation框架中會根據屬性的類型,調用不一樣的方法。例如咱們以前定義的int類型的age屬性,那麼咱們看到Foundation框架中調用的_NSsetIntValueAndNotify函數。那麼咱們把age的屬性類型變爲double從新打印一遍
咱們發現調用的函數變爲了_NSSet DoubleValueAndNotify,那麼這說明Foundation框架中有許多此類型的函數,經過屬性的不一樣類型調用不一樣的函數。 那麼咱們能夠推測Foundation框架中還有不少例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify
等等函數。
咱們能夠找到Foundation框架文件,經過命令行查詢關鍵字找到相關函數
首先咱們知道,NSKVONotifyin_Person做爲Person的子類,其superclass指針指向Person類,而且NSKVONotifyin_Person內部必定對setAge方法作了單獨的實現,那麼NSKVONotifyin_Person同Person類的差異可能就在於其內存儲的對象方法及實現不一樣。 咱們經過runtime分別打印Person類對象和NSKVONotifyin_Person類對象內存儲的對象方法
- (void)viewDidLoad { [super viewDidLoad]; Person *p1 = [[Person alloc] init]; p1.age = 1.0; Person *p2 = [[Person alloc] init]; p1.age = 2.0; // self 監聽 p1的 age屬性 NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; [self printMethods: object_getClass(p2)]; [self printMethods: object_getClass(p1)]; [p1 removeObserver:self forKeyPath:@"age"]; } - (void) printMethods:(Class)cls { unsigned int count ; Method *methods = class_copyMethodList(cls, &count); NSMutableString *methodNames = [NSMutableString string]; [methodNames appendFormat:@"%@ - ", cls]; for (int i = 0 ; i < count; i++) { Method method = methods[i]; NSString *methodName = NSStringFromSelector(method_getName(method)); [methodNames appendString: methodName]; [methodNames appendString:@" "]; } NSLog(@"%@",methodNames); free(methods); } 複製代碼
上述打印內容以下
經過上述代碼咱們發現NSKVONotifyin_Person中有4個對象方法。分別爲setAge: class dealloc _isKVOA,那麼至此咱們能夠畫出NSKVONotifyin_Person的內存結構以及方法調用順序。
這裏NSKVONotifyin_Person重寫class方法是爲了隱藏NSKVONotifyin_Person。不被外界所看到。咱們在p1添加過KVO監聽以後,分別打印p1和p2對象的class能夠發現他們都返回Person。
NSLog(@"%@,%@",[p1 class],[p2 class]); // 打印結果 Person,Person 複製代碼
若是NSKVONotifyin_Person不重寫class方法,那麼當對象要調用class對象方法的時候就會一直向上找來到nsobject,而nsobect的class的實現大體爲返回本身isa指向的類,返回p1的isa指向的類那麼打印出來的類就是NSKVONotifyin_Person,可是apple不但願將NSKVONotifyin_Person類暴露出來,而且不但願咱們知道NSKVONotifyin_Person內部實現,因此在內部重寫了class類,直接返回Person類,因此外界在調用p1的class對象方法時,是Person類。這樣p1給外界的感受p1仍是Person類,並不知道NSKVONotifyin_Person子類的存在。
那麼咱們能夠猜想NSKVONotifyin_Person內重寫的class內部實現大體爲
- (Class) class { // 獲得類對象,在找到類對象父類 return class_getSuperclass(object_getClass(self)); } 複製代碼
咱們在Person類中重寫willChangeValueForKey:和didChangeValueForKey:方法,模擬他們的實現。
- (void)setAge:(int)age { NSLog(@"setAge:"); _age = age; } - (void)willChangeValueForKey:(NSString *)key { NSLog(@"willChangeValueForKey: - begin"); [super willChangeValueForKey:key]; NSLog(@"willChangeValueForKey: - end"); } - (void)didChangeValueForKey:(NSString *)key { NSLog(@"didChangeValueForKey: - begin"); [super didChangeValueForKey:key]; NSLog(@"didChangeValueForKey: - end"); } 複製代碼
再次運行來查看didChangeValueForKey的方法內運行過程,經過打印內容能夠看到,確實在didChangeValueForKey方法內部已經調用了observer的observeValueForKeyPath:ofObject:change:context:方法。
- iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼?) 答. 當一個對象使用了KVO監聽,iOS系統會修改這個對象的isa指針,改成指向一個全新的經過Runtime動態建立的子類,子類擁有本身的set方法實現,set方法實現內部會順序調用willChangeValueForKey方法、原來的setter方法實現、didChangeValueForKey方法,而didChangeValueForKey方法內部又會調用監聽器的observeValueForKeyPath:ofObject:change:context:監聽方法。
- 如何手動觸發KVO 答. 被監聽的屬性的值被修改時,就會自動觸發KVO。若是想要手動觸發KVO,則須要咱們本身調用willChangeValueForKey和didChangeValueForKey方法便可在不改變屬性值的狀況下手動觸發KVO,而且這兩個方法缺一不可。
經過如下代碼能夠驗證
Person *p1 = [[Person alloc] init]; p1.age = 1.0; NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; [p1 willChangeValueForKey:@"age"]; [p1 didChangeValueForKey:@"age"]; [p1 removeObserver:self forKeyPath:@"age"]; 複製代碼
經過打印咱們能夠發現,didChangeValueForKey方法內部成功調用了observeValueForKeyPath:ofObject:change:context:,而且age的值並無發生改變。
文中若是有不對的地方歡迎指出。我是xx_cc,一隻長大好久但尚未二夠的傢伙。