最近小編公司招聘 iOS, 於是小編從網上找了幾道面試題,來考察候選人iOS 開發方面的技術水平,其中有一道面試題便是 KVO 底層實現是什麼? 如何手動出發 KVO? 修改成員變數的值會出發 KVO 嗎? KVC 賦值會出發 KVO 嗎? 當你瞭解 KVO 實現原理後,這幾道面試題自然不在話下.接下來我將通過程式碼和講解來窺探 KVO 背後的奧祕.
首先建立一個 Person 類 內部有個 name 屬性,然後 建立p1 和 p2兩個例項物件,其中p1新增了kvo監聽,p2沒有新增 kvo 監聽,然後重寫了 observeValueForKeyPath 方法 監聽Person.name 屬性發生改變時候的通知.
從本質上來看 Person 給name賦值的時候 呼叫的是 setName 方法 ,無論 p1還是p2 呼叫的 setter 方法都是一樣的,為什麼 p1改變 name 屬性值就能有通知, p2確沒有,呼叫的 都是同一個 setName:(NSString *)name 方法,區別怎麼那麼大?
小編窺探嘗試1
接下來小編列印下p1和p2的記憶體地址 看看p1和p2記憶體地址能不能一探究竟.
從 p1和 p2記憶體地址上也看不出來什麼東東.
小編窺探嘗試2 列印 p1和 p2 的 class 資訊
what 什麼 輸出的 class 都是 Person 類 ,既然同一個類 同一個 setter 方法,為什麼我們不一樣呢?
小編窺探嘗試3 列印 object_getClass 試試看 我們都知道object_getClass(id) 才會返回這個例項物件的真實 class 型別
什麼 , 新增 KVO 之後說好的 Person 類跑哪去了, NSKVONotifying_Person是什麼東東?
為了進一步窺探 KVO 新增前後的變化 小編窺探嘗試4 列印 setName 方法實現IMP指標有沒有發生改變,我們知道同一個方法的實現 IMP 地址是不變的.
連 setName方法都不一樣了 , 為了一探究竟 小編絕對對上邊的 NSKVONotifying_Person 和 新增 KVO 之後的 imp 指標進行進一步研究.
首先 在 lldb 上輸入 imp1和 imp2
發生了 imp1 方法實現在 Foundation 框架裡的 _NSSetObjectValueAndNotify 函式中 ,而 imp2 則呼叫了 Person setName 方法
也就是說新增了 KVO 之後 p1 修改 name 值之後 不再呼叫 Person 的 setName方法 ,而 p2沒有新增 kvo 監聽 依然正常呼叫 setName:方法 ,由此可以得出 p1 新增完 KVO 監聽後 系統修改了預設方法實現,那麼既然沒有呼叫 setName: 方法 為什麼 p1.name 的值也發生了改變?
接下來我們準備對剛才 NSKVONotifying_Person 類進行下一步研究, NSKVONotifying_Person 和 Person 有沒有內在的聯絡呢?
小編窺探嘗試5 NSKVONotifying_Person和 Person 之間的聯絡時什麼
通過列印 NSKVONotifying_Person 的 superclass 和 Person 的 superclass 可以得出, NSKVONotifying_Person是一個 Person 子類,那麼為什麼蘋果會動態建立這麼一個 子類呢? NSKVONotifying_Person 這個子類 跟 Person 內部有哪些不同呢 ?
這個時候 我們去輸出下 Person 和 NSKVONotifying_Person 內部的方法列表 和 屬性列表 ,看看NSKVONotifying_Person 子類都新增了那些方法和屬性.
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
id cls1 = object_getClass(p1);
id cls2 = object_getClass(p2);
NSLog(@"新增 KVO 之前: cls1 = %@ cls2 = %@ ",cls1,cls2);
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
cls1 = object_getClass(p1);
cls2 = object_getClass(p2);
NSString *methodList1 = [self printPersonMethods:cls1];
NSString *methodList2 = [self printPersonMethods:cls2];
NSLog(@"%@",methodList1);
NSLog(@"%@",methodList2);
// NSLog(@"新增 KVO 之後: cls1 = %@ cls2 = %@ ",cls1,cls2);
// id super_cls1 = class_getSuperclass(cls1);
// id super_cls2 = class_getSuperclass(cls2);
//
// NSLog(@"super_cls1 = %@ ,super_cls2 = %@",super_cls1,super_cls2);
//
// p1.name = @"dzb";
// p2.name = @"123";
}
- (NSString *) printPersonMethods:(id)obj {
unsigned int count = 0;
Method *methods = class_copyMethodList([obj class],&count);
NSMutableString *methodList = [NSMutableString string];
[methodList appendString:@"[\n"];
for (int i = 0; i<count; i++) {
Method method = methods[i];
SEL sel = method_getName(method);
[methodList appendFormat:@"%@",NSStringFromSelector(sel)];
[methodList appendString:@"\n"];
}
[methodList appendFormat:@"]"];
free(methods);
return methodList;
}
複製程式碼
從輸出結果可以看出來 NSKVONotifying_Person 內部也有一個 setName:方法 還重寫了 class 和 dealloc 方法 , _isKVOA, 那麼我們可以大致的得出, p1新增 kVO 後 runtime 動態的生成了一個 NSKVONotifying_Person子類 並重寫了 setName 方法 ,那麼 setName 內部一定是做了一些事情,才會觸發 observeValueForKeyPath 監聽方法.
繼續探究 NSKVONotifying_Person 子類 重寫 setName 都做了什麼? 其實 setName 方法內部 是呼叫了 Foundation 的 _NSSetObjectValueAndNotify 函式 ,在 _NSSetObjectValueAndNotify 內部
1首先會呼叫 willChangeValueForKey
2然後給 name 屬性賦值
3 最後呼叫 didChangeValueForKey
4最後呼叫 observer 的 observeValueForKeyPath 去告訴監聽器屬性值發生了改變 .
由於蘋果 Foundation 框架是不開源的 ,所以我們依然可以通過重寫Person 的 willChangeValueForKey 和 didChangeValueForKey 驗證我們的猜想 .
首先當我們改變p1.name 的值時 並不是首先執行的 setName: 這個方法 ,而是先呼叫了 willChangeValueForKey 其次 呼叫父類的 setter 方法 對屬性賦值 ,然後再呼叫 didChangeValueForKey 方法 ,並在 didChangeValueForKey 內部 呼叫監聽器的 observeValueForKeyPath方法 告訴外界 屬性值發生了改變.
至於重寫了 dealloc 和 class 方法 是為了做一些 KVO 釋放記憶體 和 隱藏外界對於 NSKVONotifying_Person 子類的存在
這就是我們呼叫 [p1 class] 和 [p2 class]結果都顯示 Person 類 ,讓我們誤以為 Person 沒有發生變化 補充說明 ,KVC 對屬性賦值時候 是會在這個類裡邊 去查詢 _age isAge setAge setIsAge 等方法的 ,最終會呼叫屬性的 setter 方法 ,那麼如果新增了 KVO 還是會被觸發的 . 相反 設定成員變數 _age 由於不會觸發 setter 方法 ,因此不會去觸發 KVO 相關的程式碼 .
好了,我是大兵布萊恩特,歡迎加入博主技術交流群,iOS 開發交流群