本篇主要是對小碼哥底層視頻學習的總結。方便日后復習。
上一篇《iOS底層原理總結 - 探尋Class的本質》:
http://www.lxweimin.com/p/748bc1d63184
本篇學習總結:
- 探尋KVO本質
- KVO底層方法調用順序
- 代碼求證KVO實現機制
- 探尋KVC本質
- KVC賦值和查找順序
好了,帶著問題,我們一一開始閱讀吧 ??
一.探尋KVO本質
1.面試題:iOS用什么方式實現對一個對象的KVO?(KVO的本質是什么?)
看到這個面試題,我們就從頭開始說一下什么是KVO,KVO的全稱 Key-Value Observing,俗稱“鍵值監聽”,可以用于監聽某個對象屬性值的改變。
先來一段代碼:
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc]init];
self.person2.age = 2;
// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 10;
self.person2.age = 20;
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// 當監聽對象的屬性值發生改變時,就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
//打印結果如下:
監聽到<MJPerson: 0x2830605c0>的age屬性值改變了 - {
kind = 1;
new = 10;//新賦值數據
old = 1;//舊賦值數據 NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
} - 123 //添加監聽時的上下文
上述代碼可以看出,p1添加observer后,一旦age屬性值發生改變,會立刻通知observer,執行observer的observeValueForKeyPath方法。
我們嘗試探尋KVO底層實現原理,我們通過斷點方式探尋。
KVO底層原理分析
p1和p2的age屬性值發生改變,自然會調用-setAge方法,我們把斷點打在-setAge方法這里,斷點確實走到了這里,我們發現p1對象和p2對象調用同樣的setter方法,但是我們發現p1除了調用setter方法之外還會另外執行observer的observeValueForKeyPath方法,說明KVO在運行時獲取對p1對象做了一些改變,使得p1對象在調用setage方法的時候可能做了一些額外的操作,所以問題出在對象身上,兩個對象在內存中肯定不一樣,兩個對象可能本質上并不一樣。接下來來探索KVO內部是怎么實現的。
KVO底層原理實現
我們分別將斷點打到添加監聽之前跟之后,
通過上面倆圖我們會發現,p1對象執行addObserver操作之后,p1實例對象的isa指針指向NSKVONotifyin_Person類對象,而p2實例對象的isa指針還是指向MJPerson類對象。
那么我們先來觀察p2對象在內存中時如何存儲的,然后對比p2觀察p1
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 10;
self.person2.age = 20;
}
首先我們知道,p2實例對象在調用-setAge方法的時候,會通過p2實例對象中的isa指針找到MJPerson類對象,然后去類對象的方法列表中查找-setAge方法,進而找到方法對應的實現,如下圖所示:
但是剛才我們發現p1對象添加監聽者之后,isa指針指向了NSKVONotifyin_Person類對象,NSKVONotifyin_Person類對象其實是MJPerson的類對象的子類,NSKVONotifyin_Person類對象的supercalss指針指向MJPerson的類對象。NSKVONotifyin_Person類對象是運行時通過runtime生成的,那么p1實例對象調用setage方法,肯定是通過p1實例對象中的isa指針找到NSKVONotifyin_Person類對象,在NSKVONotifyin_Person類對象找到setage方法及實現。
小碼哥將NSKVONotifyin_Person類對象中的setage方法實現邏輯告訴我們了:
在調用NSKVONotifyin_Person類對象中的setage方法,其實是調用了Foundation框架中C語言函數_NSsetIntValueAndNotify,_NSsetIntValueAndNotify方法內部的邏輯,首先是調用willChangeValueForKey告訴將要改變某一個屬性的值,然后調用父類的setage方法對成員變量賦值,最后調用didChangeValueForKey告訴已經改變了某一個屬性的值,didChangeValueForKey中調用監聽器的監聽方法,最終來到監聽者的observeValueForKeyPath方法中。
到此,整個KVO調用底層流程就搞清楚了,我們需要用代碼求證一下步驟
二.代碼求證KVO實現流程
1.給p1添加observer后,在runtime時期動態創建MJPerson類對象的子類即NSKVONotifyin_Person類對象,修改p1的isa指針指向為NSKVONotifyin_Person類對象,前面已經有圖印證了這點。
2.為了驗證p1添加observer前后調用-setAge方法后實現有什么區別,我們通過打印方法實現的地址來看一下p1和p2的-setAge的方法實現的地址在添加KVO前后有什么變化。
我們只是看實現的內存地址,好像并不能看出什么,前面不是說p1對象添加observer后,調用-setAge方法的時候,其實調用_NSsetIntValueAndNotify方法了嗎,方法名怎么看呢?我們在終端打印一下方法實現,如下圖:
這一步也印證了咱們前面總結的
Foundation框架中會根據屬性的類型,調用不同的方法。例如我們之前定義的int類型的age屬性,那么我們看到Foundation框架中調用的_NSsetIntValueAndNotify函數。那么我們把age的屬性類型變為double重新打印一遍
補充一點Founddation框架知識:
我們發現調用的函數變為了_NSSetDoubleValueAndNotify,那么這說明Foundation框架中有許多此類型的函數,通過屬性的不同類型調用不同的函數。
那么我們可以推測Foundation框架中還有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函數。
我們可以找到Foundation框架文件,通過命令行查詢關鍵字找到相關函數
用一張圖來總結一下吧:
三.NSKVONotifyin_Person內部結構是怎樣的?
首先我們知道,NSKVONotifyin_Person作為MJPerson 類對象的子類,其superclass指針指向MJPerson類對象,并且NSKVONotifyin_Person內部一定對-setAge方法做了單獨的實現,那么NSKVONotifyin_Person同MJPerson* 類的差別可能就在于其內存儲的對象方法及實現不同。
我們通過runtime分別打印MJPerson* 類對象和NSKVONotifyin_Person類對象內存儲的對象方法。
先上代碼:
//MJPerson類
@interface MJPerson : NSObject
@property (assign, nonatomic) int age;
@end
//ViewController.m
- (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);
}
打印結果如下圖:
上述兩個類對象方法列表不一樣,我們直接看下圖:
這里有兩點要說明,一是-setAge方法內部調用Foundation框架中的_NSSetIntValueAndNotify方法,這點前面已經講完了,二是重寫class方法,NSKVONotifyin_Person重寫class方法是為了隱藏NSKVONotifyin_Person。不被外界所看到。我們在p1添加過KVO監聽之后,分別打印p1和p2對象的class可以發現他們都返回Person
NSLog(@"p1 = %@,p2 = %@",[self.person1 class],[self.person2 class]);
//打印結果如下:
p1 = MJPerson,p2 = MJPerson
如果NSKVONotifyin_Person不重寫class方法,那么當對象要調用class對象方法的時候就會一直向上找來到NSObject,而NSObject的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));
}
驗證didChangeValueForKey:內部會調用observer的observeValueForKeyPath:ofObject:change:context:方法
我們在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");
}
四.探尋KVC本質
先說一下KVC:Key Value Coding(鍵值編碼),獲取或者給對象的某個屬性進行賦值,常用方法如下:
- (nullable id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
key 跟keypath的區別就是:key 是單純當前調用對象的某個特定屬性名字,keypath既可以是某個特定屬性名字,也可以是某個路徑下查找的名字
五.KVC賦值和查找順序
總結一下本篇面試題:
- 1.iOS 用什么方式實現對一個對象的KVO?(KVO本質是什么?)
答:當一個對象使用了KVO監聽,iOS系統會修改這個對象的isa指針,改為指向一個全新的通過runtime動態創建的子類,子類擁有自己的setter方法實現,setter方法實現內部會調用Foundation框架的_NSSetIntValueAndNotify方法,在_NSSetIntValueAndNotify 方法里面調用willChangeValueForKey方法,然后調用父類的setter方法對成員變量賦值,最后調用didChangeValueForKey方法,最終通知監聽者調用observeValueForKeyPath 方法,
這里我們要特別說明一點:單純給成員變量賦值,沒有調用willChangeValueForKey和didChangeValueForKey方法,最終也不會調用observeValueForKeyPath方法
- 2.面試題:手動能否觸發實現一個KVO?
答案是可以的,只要知道了KVO底層調用順序,就可以寫出來,以代碼為例說明:
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
[self.person1 willChangeValueForKey:@"age"];
self.person1.age = 10000;
[self.person1 didChangeValueForKey:@"age"];
}
//- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
//{
// self.person1.age = 10;
//}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// observeValueForKeyPath:ofObject:change:context:
// 當監聽對象的屬性值發生改變時,就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
//打印結果如下:
監聽到<MJPerson: 0x280a4c890>的age屬性值改變了 - {
kind = 1;
new = 10000;
old = 1;
} - (null)
- 3.面試題:KVC改變屬性值回引起KVO變化嗎?
答:可以的,直接來看代碼及打印吧
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
// [self.person1 willChangeValueForKey:@"age"];
// [self.person1 setValue:@(101) forKey:@"age"];
// [self.person1 didChangeValueForKey:@"age"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.person1 setValue:@(102) forKey:@"age"];
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// observeValueForKeyPath:ofObject:change:context:
// 當監聽對象的屬性值發生改變時,就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
本篇學習先記錄到此,感謝閱讀,如有錯誤,不吝賜教。