iOS窺探KVO底層實現原理篇

大兵布萊恩特0409發表於2018-07-12

最近小編公司招聘 iOS, 於是小編從網上找了幾道面試題,來考察候選人iOS 開發方面的技術水平,其中有一道面試題便是 KVO 底層實現是什麼? 如何手動出發 KVO? 修改成員變數的值會出發 KVO 嗎? KVC 賦值會出發 KVO 嗎? 當你瞭解 KVO 實現原理後,這幾道面試題自然不在話下.接下來我將通過程式碼和講解來窺探 KVO 背後的奧祕.

image.png

首先建立一個 Person 類 內部有個 name 屬性,然後 建立p1 和 p2兩個例項物件,其中p1新增了kvo監聽,p2沒有新增 kvo 監聽,然後重寫了 observeValueForKeyPath 方法 監聽Person.name 屬性發生改變時候的通知.

image.png

從本質上來看 Person 給name賦值的時候 呼叫的是 setName 方法 ,無論 p1還是p2 呼叫的 setter 方法都是一樣的,為什麼 p1改變 name 屬性值就能有通知, p2確沒有,呼叫的 都是同一個 setName:(NSString *)name 方法,區別怎麼那麼大?

小編窺探嘗試1

接下來小編列印下p1和p2的記憶體地址 看看p1和p2記憶體地址能不能一探究竟.

image.png

從 p1和 p2記憶體地址上也看不出來什麼東東.

小編窺探嘗試2 列印 p1和 p2 的 class 資訊

image.png

what 什麼 輸出的 class 都是 Person 類 ,既然同一個類 同一個 setter 方法,為什麼我們不一樣呢?

小編窺探嘗試3 列印 object_getClass 試試看 我們都知道object_getClass(id) 才會返回這個例項物件的真實 class 型別

image.png

什麼 , 新增 KVO 之後說好的 Person 類跑哪去了, NSKVONotifying_Person是什麼東東?

為了進一步窺探 KVO 新增前後的變化 小編窺探嘗試4 列印 setName 方法實現IMP指標有沒有發生改變,我們知道同一個方法的實現 IMP 地址是不變的.

image.png

連 setName方法都不一樣了 , 為了一探究竟 小編絕對對上邊的 NSKVONotifying_Person 和 新增 KVO 之後的 imp 指標進行進一步研究.

首先 在 lldb 上輸入 imp1和 imp2

image.png

發生了 imp1 方法實現在 Foundation 框架裡的 _NSSetObjectValueAndNotify 函式中 ,而 imp2 則呼叫了 Person setName 方法

image.png

也就是說新增了 KVO 之後 p1 修改 name 值之後 不再呼叫 Person 的 setName方法 ,而 p2沒有新增 kvo 監聽 依然正常呼叫 setName:方法 ,由此可以得出 p1 新增完 KVO 監聽後 系統修改了預設方法實現,那麼既然沒有呼叫 setName: 方法 為什麼 p1.name 的值也發生了改變?

接下來我們準備對剛才 NSKVONotifying_Person 類進行下一步研究, NSKVONotifying_Person 和 Person 有沒有內在的聯絡呢?

小編窺探嘗試5 NSKVONotifying_Person和 Person 之間的聯絡時什麼

image.png

通過列印 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;
}

複製程式碼

image.png

從輸出結果可以看出來 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 去告訴監聽器屬性值發生了改變 .

image.png

由於蘋果 Foundation 框架是不開源的 ,所以我們依然可以通過重寫Person 的 willChangeValueForKey 和 didChangeValueForKey 驗證我們的猜想 .

image.png

首先當我們改變p1.name 的值時 並不是首先執行的 setName: 這個方法 ,而是先呼叫了 willChangeValueForKey 其次 呼叫父類的 setter 方法 對屬性賦值 ,然後再呼叫 didChangeValueForKey 方法 ,並在 didChangeValueForKey 內部 呼叫監聽器的 observeValueForKeyPath方法 告訴外界 屬性值發生了改變.

Untitled.gif

image.png

至於重寫了 dealloc 和 class 方法 是為了做一些 KVO 釋放記憶體 和 隱藏外界對於 NSKVONotifying_Person 子類的存在

image.png

這就是我們呼叫 [p1 class] 和 [p2 class]結果都顯示 Person 類 ,讓我們誤以為 Person 沒有發生變化 補充說明 ,KVC 對屬性賦值時候 是會在這個類裡邊 去查詢 _age isAge setAge setIsAge 等方法的 ,最終會呼叫屬性的 setter 方法 ,那麼如果新增了 KVO 還是會被觸發的 . 相反 設定成員變數 _age 由於不會觸發 setter 方法 ,因此不會去觸發 KVO 相關的程式碼 .

image.png

好了,我是大兵布萊恩特,歡迎加入博主技術交流群,iOS 開發交流群

QQ20180712-0.png

相關文章