本篇主要是對(duì)小碼哥底層視頻學(xué)習(xí)的總結(jié)缘薛。方便日后復(fù)習(xí)乒躺。
上一篇《iOS底層原理總結(jié) - 探尋Class的本質(zhì)》:
http://www.reibang.com/p/748bc1d63184
本篇學(xué)習(xí)總結(jié):
- 探尋KVO本質(zhì)
- KVO底層方法調(diào)用順序
- 代碼求證KVO實(shí)現(xiàn)機(jī)制
- 探尋KVC本質(zhì)
- KVC賦值和查找順序
好了衰猛,帶著問題,我們一一開始閱讀吧 ??
一.探尋KVO本質(zhì)
1.面試題:iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO示损?(KVO的本質(zhì)是什么馁痴?)
看到這個(gè)面試題,我們就從頭開始說一下什么是KVO,KVO的全稱 Key-Value Observing簿晓,俗稱“鍵值監(jiān)聽”眶拉,可以用于監(jiān)聽某個(gè)對(duì)象屬性值的改變。
先來一段代碼:
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc]init];
self.person2.age = 2;
// 給person1對(duì)象添加KVO監(jiān)聽
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"];
}
// 當(dāng)監(jiān)聽對(duì)象的屬性值發(fā)生改變時(shí)憔儿,就會(huì)調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監(jiān)聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
//打印結(jié)果如下:
監(jiān)聽到<MJPerson: 0x2830605c0>的age屬性值改變了 - {
kind = 1;
new = 10;//新賦值數(shù)據(jù)
old = 1;//舊賦值數(shù)據(jù) NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
} - 123 //添加監(jiān)聽時(shí)的上下文
上述代碼可以看出忆植,p1添加observer后,一旦age屬性值發(fā)生改變谒臼,會(huì)立刻通知observer朝刊,執(zhí)行observer的observeValueForKeyPath方法。
我們嘗試探尋KVO底層實(shí)現(xiàn)原理蜈缤,我們通過斷點(diǎn)方式探尋拾氓。
KVO底層原理分析
p1和p2的age屬性值發(fā)生改變,自然會(huì)調(diào)用-setAge方法底哥,我們把斷點(diǎn)打在-setAge方法這里咙鞍,斷點(diǎn)確實(shí)走到了這里,我們發(fā)現(xiàn)p1對(duì)象和p2對(duì)象調(diào)用同樣的setter方法趾徽,但是我們發(fā)現(xiàn)p1除了調(diào)用setter方法之外還會(huì)另外執(zhí)行observer的observeValueForKeyPath方法续滋,說明KVO在運(yùn)行時(shí)獲取對(duì)p1對(duì)象做了一些改變,使得p1對(duì)象在調(diào)用setage方法的時(shí)候可能做了一些額外的操作孵奶,所以問題出在對(duì)象身上疲酌,兩個(gè)對(duì)象在內(nèi)存中肯定不一樣,兩個(gè)對(duì)象可能本質(zhì)上并不一樣。接下來來探索KVO內(nèi)部是怎么實(shí)現(xiàn)的朗恳。
KVO底層原理實(shí)現(xiàn)
我們分別將斷點(diǎn)打到添加監(jiān)聽之前跟之后湿颅,
通過上面?zhèn)z圖我們會(huì)發(fā)現(xiàn),p1對(duì)象執(zhí)行addObserver操作之后僻肖,p1實(shí)例對(duì)象的isa指針指向NSKVONotifyin_Person類對(duì)象肖爵,而p2實(shí)例對(duì)象的isa指針還是指向MJPerson類對(duì)象。
那么我們先來觀察p2對(duì)象在內(nèi)存中時(shí)如何存儲(chǔ)的臀脏,然后對(duì)比p2觀察p1
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 10;
self.person2.age = 20;
}
首先我們知道,p2實(shí)例對(duì)象在調(diào)用-setAge方法的時(shí)候冀自,會(huì)通過p2實(shí)例對(duì)象中的isa指針找到MJPerson類對(duì)象揉稚,然后去類對(duì)象的方法列表中查找-setAge方法,進(jìn)而找到方法對(duì)應(yīng)的實(shí)現(xiàn)熬粗,如下圖所示:
但是剛才我們發(fā)現(xiàn)p1對(duì)象添加監(jiān)聽者之后搀玖,isa指針指向了NSKVONotifyin_Person類對(duì)象,NSKVONotifyin_Person類對(duì)象其實(shí)是MJPerson的類對(duì)象的子類驻呐,NSKVONotifyin_Person類對(duì)象的supercalss指針指向MJPerson的類對(duì)象灌诅。NSKVONotifyin_Person類對(duì)象是運(yùn)行時(shí)通過runtime生成的,那么p1實(shí)例對(duì)象調(diào)用setage方法含末,肯定是通過p1實(shí)例對(duì)象中的isa指針找到NSKVONotifyin_Person類對(duì)象猜拾,在NSKVONotifyin_Person類對(duì)象找到setage方法及實(shí)現(xiàn)。
小碼哥將NSKVONotifyin_Person類對(duì)象中的setage方法實(shí)現(xiàn)邏輯告訴我們了:
在調(diào)用NSKVONotifyin_Person類對(duì)象中的setage方法佣盒,其實(shí)是調(diào)用了Foundation框架中C語言函數(shù)_NSsetIntValueAndNotify挎袜,_NSsetIntValueAndNotify方法內(nèi)部的邏輯,首先是調(diào)用willChangeValueForKey告訴將要改變某一個(gè)屬性的值肥惭,然后調(diào)用父類的setage方法對(duì)成員變量賦值盯仪,最后調(diào)用didChangeValueForKey告訴已經(jīng)改變了某一個(gè)屬性的值,didChangeValueForKey中調(diào)用監(jiān)聽器的監(jiān)聽方法蜜葱,最終來到監(jiān)聽者的observeValueForKeyPath方法中全景。
到此,整個(gè)KVO調(diào)用底層流程就搞清楚了,我們需要用代碼求證一下步驟
二.代碼求證KVO實(shí)現(xiàn)流程
1.給p1添加observer后牵囤,在runtime時(shí)期動(dòng)態(tài)創(chuàng)建MJPerson類對(duì)象的子類即NSKVONotifyin_Person類對(duì)象爸黄,修改p1的isa指針指向?yàn)?strong>NSKVONotifyin_Person類對(duì)象,前面已經(jīng)有圖印證了這點(diǎn)奔浅。
2.為了驗(yàn)證p1添加observer前后調(diào)用-setAge方法后實(shí)現(xiàn)有什么區(qū)別馆纳,我們通過打印方法實(shí)現(xiàn)的地址來看一下p1和p2的-setAge的方法實(shí)現(xiàn)的地址在添加KVO前后有什么變化。
我們只是看實(shí)現(xiàn)的內(nèi)存地址汹桦,好像并不能看出什么鲁驶,前面不是說p1對(duì)象添加observer后,調(diào)用-setAge方法的時(shí)候舞骆,其實(shí)調(diào)用_NSsetIntValueAndNotify方法了嗎钥弯,方法名怎么看呢径荔?我們?cè)诮K端打印一下方法實(shí)現(xiàn),如下圖:
這一步也印證了咱們前面總結(jié)的
Foundation框架中會(huì)根據(jù)屬性的類型脆霎,調(diào)用不同的方法总处。例如我們之前定義的int類型的age屬性,那么我們看到Foundation框架中調(diào)用的_NSsetIntValueAndNotify函數(shù)睛蛛。那么我們把a(bǔ)ge的屬性類型變?yōu)閐ouble重新打印一遍
補(bǔ)充一點(diǎn)Founddation框架知識(shí):
我們發(fā)現(xiàn)調(diào)用的函數(shù)變?yōu)榱薩NSSetDoubleValueAndNotify鹦马,那么這說明Foundation框架中有許多此類型的函數(shù),通過屬性的不同類型調(diào)用不同的函數(shù)忆肾。
那么我們可以推測(cè)Foundation框架中還有很多例如_NSSetBoolValueAndNotify荸频、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify客冈、_NSSetLongValueAndNotify等等函數(shù)旭从。
我們可以找到Foundation框架文件,通過命令行查詢關(guān)鍵字找到相關(guān)函數(shù)
用一張圖來總結(jié)一下吧:
三.NSKVONotifyin_Person內(nèi)部結(jié)構(gòu)是怎樣的场仲?
首先我們知道和悦,NSKVONotifyin_Person作為MJPerson 類對(duì)象的子類,其superclass指針指向MJPerson類對(duì)象渠缕,并且NSKVONotifyin_Person內(nèi)部一定對(duì)-setAge方法做了單獨(dú)的實(shí)現(xiàn)鸽素,那么NSKVONotifyin_Person同MJPerson* 類的差別可能就在于其內(nèi)存儲(chǔ)的對(duì)象方法及實(shí)現(xiàn)不同。
我們通過runtime分別打印MJPerson* 類對(duì)象和NSKVONotifyin_Person類對(duì)象內(nèi)存儲(chǔ)的對(duì)象方法褐健。
先上代碼:
//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 監(jiān)聽 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);
}
打印結(jié)果如下圖:
上述兩個(gè)類對(duì)象方法列表不一樣付鹿,我們直接看下圖:
這里有兩點(diǎn)要說明,一是-setAge方法內(nèi)部調(diào)用Foundation框架中的_NSSetIntValueAndNotify方法蚜迅,這點(diǎn)前面已經(jīng)講完了舵匾,二是重寫class方法,NSKVONotifyin_Person重寫class方法是為了隱藏NSKVONotifyin_Person。不被外界所看到。我們?cè)趐1添加過KVO監(jiān)聽之后镜遣,分別打印p1和p2對(duì)象的class可以發(fā)現(xiàn)他們都返回Person
NSLog(@"p1 = %@,p2 = %@",[self.person1 class],[self.person2 class]);
//打印結(jié)果如下:
p1 = MJPerson,p2 = MJPerson
如果NSKVONotifyin_Person不重寫class方法遥倦,那么當(dāng)對(duì)象要調(diào)用class對(duì)象方法的時(shí)候就會(huì)一直向上找來到NSObject伞鲫,而NSObject的class的實(shí)現(xiàn)大致為返回自己isa指向的類,返回p1的isa指向的類那么打印出來的類就是NSKVONotifyin_Person,但是apple不希望將NSKVONotifyin_Person類暴露出來,并且不希望我們知道NSKVONotifyin_Person內(nèi)部實(shí)現(xiàn)蹋辅,所以在內(nèi)部重寫了class類,直接返回Person類挫掏,所以外界在調(diào)用p1的class對(duì)象方法時(shí)侦另,是Person類。這樣p1給外界的感覺p1還是Person類,并不知道NSKVONotifyin_Person子類的存在
那么我們可以猜測(cè)NSKVONotifyin_Person內(nèi)重寫的class內(nèi)部實(shí)現(xiàn)大致為
- (Class) class {
// 得到類對(duì)象褒傅,在找到類對(duì)象父類
return class_getSuperclass(object_getClass(self));
}
驗(yàn)證didChangeValueForKey:內(nèi)部會(huì)調(diào)用observer的observeValueForKeyPath:ofObject:change:context:方法
我們?cè)赑erson類中重寫willChangeValueForKey:和didChangeValueForKey:方法弃锐,模擬他們的實(shí)現(xiàn)
- (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本質(zhì)
先說一下KVC:Key Value Coding(鍵值編碼),獲取或者給對(duì)象的某個(gè)屬性進(jìn)行賦值殿托,常用方法如下:
- (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的區(qū)別就是:key 是單純當(dāng)前調(diào)用對(duì)象的某個(gè)特定屬性名字霹菊,keypath既可以是某個(gè)特定屬性名字,也可以是某個(gè)路徑下查找的名字
五.KVC賦值和查找順序
總結(jié)一下本篇面試題:
- 1.iOS 用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO支竹?(KVO本質(zhì)是什么旋廷?)
答:當(dāng)一個(gè)對(duì)象使用了KVO監(jiān)聽,iOS系統(tǒng)會(huì)修改這個(gè)對(duì)象的isa指針礼搁,改為指向一個(gè)全新的通過runtime動(dòng)態(tài)創(chuàng)建的子類柳洋,子類擁有自己的setter方法實(shí)現(xiàn),setter方法實(shí)現(xiàn)內(nèi)部會(huì)調(diào)用Foundation框架的_NSSetIntValueAndNotify方法叹坦,在_NSSetIntValueAndNotify 方法里面調(diào)用willChangeValueForKey方法,然后調(diào)用父類的setter方法對(duì)成員變量賦值卑雁,最后調(diào)用didChangeValueForKey方法募书,最終通知監(jiān)聽者調(diào)用observeValueForKeyPath 方法,
這里我們要特別說明一點(diǎn):?jiǎn)渭兘o成員變量賦值测蹲,沒有調(diào)用willChangeValueForKey和didChangeValueForKey方法莹捡,最終也不會(huì)調(diào)用observeValueForKeyPath方法
- 2.面試題:手動(dòng)能否觸發(fā)實(shí)現(xiàn)一個(gè)KVO?
答案是可以的扣甲,只要知道了KVO底層調(diào)用順序篮赢,就可以寫出來,以代碼為例說明:
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
// 給person1對(duì)象添加KVO監(jiān)聽
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:
// 當(dāng)監(jiān)聽對(duì)象的屬性值發(fā)生改變時(shí)琉挖,就會(huì)調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監(jiān)聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
//打印結(jié)果如下:
監(jiān)聽到<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對(duì)象添加KVO監(jiān)聽
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:
// 當(dāng)監(jiān)聽對(duì)象的屬性值發(fā)生改變時(shí)示辈,就會(huì)調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監(jiān)聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
本篇學(xué)習(xí)先記錄到此寥茫,感謝閱讀,如有錯(cuò)誤矾麻,不吝賜教纱耻。