前言
最近打算重新梳理一遍iOS底層的知識(shí)芥喇,盡量把所有的底層知識(shí)點(diǎn)都搞懂搞透徹西采,礙于iOS不開源凰萨,有很多東西并不能很直觀的去學(xué)習(xí)继控,所以可能有瑕疵,希望大家可以理解胖眷,并一起交流武通,筆者也盡可能做到盡善盡美吧。
KVO概述
KVO的底層是如何實(shí)現(xiàn)的呢珊搀?
對(duì)于這個(gè)問題冶忱,我想大家都可以簡(jiǎn)單的聊上這么幾句。
對(duì)某個(gè)實(shí)例的某一個(gè)屬性添加KVO監(jiān)聽后境析,系統(tǒng)會(huì)利用runtime的運(yùn)行時(shí)特性囚枪,生成一個(gè)臨時(shí)的類NSKVONotifying_xxx,然后把該實(shí)例的isa指針指向NSKVONotifying_xxx劳淆,監(jiān)聽哪個(gè)屬性链沼,就重寫NSKVONotifying_xxx中此屬性的set方法,然后在重寫的set方法中實(shí)現(xiàn)監(jiān)聽和通知沛鸵。
簡(jiǎn)單的來(lái)說就是這樣括勺,但是這太籠統(tǒng)了,下面我們通過例子曲掰,一步一步的來(lái)分析疾捍。
探究
1. 為什么會(huì)想到可能是類發(fā)生了變化?
Person * person1 = [[Person alloc] init];
Person * person2 = [[Person alloc] init];
[person1 addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
person1.age = 10;
person2.age = 20;
這是一個(gè)最基本的KVO使用栏妖,在回調(diào)中只有person1的值改變被監(jiān)聽了乱豆,但是我們?cè)谫x值的時(shí)候都是調(diào)用了age的set方法,如果我們?cè)赑erson類中實(shí)現(xiàn)setAge:的方法并debug吊趾,這兩次賦值都會(huì)走setAge方法宛裕,問題不是出在setAge這里房官,所以我們推測(cè)可能是類發(fā)聲了某些變化。(此處應(yīng)該有runtime的知識(shí)基礎(chǔ)续滋,runtime有能力對(duì)類做一些動(dòng)態(tài)的改變)翰守。
所以我們可以獲取一下這兩個(gè)實(shí)例的類型
// 輸出 person1:NSKVONotifying_Person
NSLog(@"person1:%@",object_getClass(person1));
// 輸出 person2:Person
NSLog(@"person2:%@",object_getClass(person2));
到這里我們就可以確定確實(shí)是生成了一個(gè)中間類。并且讓person1的isa指針指向了這個(gè)類(object_getClassName方法就是返回isa的指向)疲酌。
注:此處為什么要用runtime的api蜡峰,因?yàn)閞untime的api調(diào)用后的結(jié)果更加接近本質(zhì)
2. NSKVONotifying_Person類中做了什么處理?
首先我們先看一下這面這個(gè)圖朗恳,其實(shí)這就是添加了KVO之后類的類型結(jié)構(gòu)
關(guān)于NSKVONotifying_Person類實(shí)現(xiàn)的方法湿颅,我們是怎么樣得到的呢,這里我們可以借助runtime的api窺探一下粥诫。
[self printMethodList:object_getClass(person1)];
// 下面是方法實(shí)現(xiàn)
- (void)printMethodList:(Class)cls {
unsigned int count;
Method * methodList = class_copyMethodList(cls, &count);
for (unsigned int i = 0; i < count; i++) {
Method method = methodList[i];
NSLog(@"method(%d) : %@", i, NSStringFromSelector(method_getName(method)));
}
free(methodList);
}
輸出結(jié)果
method(0) : setAge:
method(1) : class
method(2) : dealloc
method(3) : _isKVOA
到這一步油航,我們可以先做一下小總結(jié):
person2的isa指針指向Person類,所以在setAge的時(shí)候怀浆,就直接調(diào)用了Person中實(shí)現(xiàn)的setAge:方法谊囚,正常的賦值操作,沒有觸發(fā)KVO执赡。但是person1的isa動(dòng)態(tài)改變镰踏,指向了NSKVONotifying_Person,同時(shí)NSKVONotifying_Person中又重新實(shí)現(xiàn)了setAge:方法沙合,所以在給person1的age賦值時(shí)奠伪,首先調(diào)用的是NSKVONotifying_Person中的setAge:方法,但是我們?cè)谥暗膁ebug中發(fā)現(xiàn)首懈,Person中的setAge:方法也會(huì)調(diào)用绊率,其實(shí)這很容易理解,這應(yīng)該是在NSKVONotifying_Person的setAge:實(shí)現(xiàn)中又調(diào)用了Person的setAge究履,畢竟NSKVONotifying_Person的isa指向Person(請(qǐng)自行驗(yàn)證)滤否。
3. NSKVONotifying_Person中的setAge:的實(shí)現(xiàn)
個(gè)人感覺,挖掘setAge:的實(shí)現(xiàn)是比較難的挎袜。
我們通過下面的方法打印一下方法IMP的地址
NSLog(@"person1添加KVO之前的兩個(gè)setAge地址: \n -person1:%p -- person2:%p",
[person1 methodForSelector:@selector(setAge:)],
[person2 methodForSelector:@selector(setAge:)]);
[person1 addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
NSLog(@"person1添加KVO之后的兩個(gè)setAge地址: \n -person1:%p -- person2:%p",
[person1 methodForSelector:@selector(setAge:)],
[person2 methodForSelector:@selector(setAge:)]);
輸出:
person1添加KVO之前的兩個(gè)setAge地址:
-person1:0x1005ee850 -- person2:0x1005ee850
person1添加KVO之后的兩個(gè)setAge地址:
-person1:0x7fff25623f0e -- person2:0x1005ee850
我們可以看到顽聂,在添加KVO監(jiān)聽前后,person2的setAge實(shí)現(xiàn)的地址沒有發(fā)生變化盯仪,但是person1的變了紊搪,我們?cè)诖蛴∨_(tái)用lldb命令打印一下0x7fff25623f0e
(lldb) p (IMP)0x7fff25623f0e
(IMP) $1 = 0x00007fff25623f0e (Foundation`_NSSetIntValueAndNotify)
可以看到setAge的實(shí)現(xiàn)其實(shí)就是調(diào)用了Foundation框架的_NSSetIntValueAndNotify方法。那具體的_NSSetIntValueAndNotify內(nèi)部實(shí)現(xiàn)是怎么樣的呢全景?因?yàn)镕oundation不開源耀石,我們只能猜測(cè),并對(duì)我們的猜測(cè)做出相應(yīng)的驗(yàn)證爸黄。
下面是我看過一些大神的分析之后猜測(cè)的_NSSetIntValueAndNotify實(shí)現(xiàn)的偽代碼(特此鳴謝我們的MJ老師)
void _NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)keyPath {
// 通知監(jiān)聽者,已經(jīng)修改完畢
[observer observeValueForKeyPath:keyPath ofObject:self change:nil context:nil];
}
如何驗(yàn)證一下我們的猜測(cè)呢滞伟?
我們知道NSKVONotifying_Person類中沒有實(shí)現(xiàn)willChangeValueForKey和didChangeValueForKey這兩個(gè)方法揭鳞,所以我們可以在NSKVONotifying_Person的父類,也就是Person類型重寫這兩個(gè)方法梆奈,改造完之后的Person類里面應(yīng)該是下面這樣子:
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person
- (void)setAge:(int)age {
_age = age;
NSLog(@"age:%d",age);
}
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
person1在添加了KVO監(jiān)聽野崇,并設(shè)置值person1.age = 10;
之后,輸出如下:
2020-09-07 23:47:13.787066+0800 KVO[8122:119707] willChangeValueForKey
2020-09-07 23:47:13.787229+0800 KVO[8122:119707] age:10
2020-09-07 23:47:13.787334+0800 KVO[8122:119707] didChangeValueForKey - begin
2020-09-07 23:47:13.787592+0800 KVO[8122:119707] <Person: 0x60000288c190> -- age -- {
kind = 1;
new = 10;
old = 0;
}
2020-09-07 23:47:13.787732+0800 KVO[8122:119707] didChangeValueForKey - end
輸出結(jié)果與我們的猜測(cè)一致亩钟。
其他小知識(shí)點(diǎn)
NSKVONotifying_Person也重寫了class方法乓梨,使用[person1 class]的時(shí)候返回的是Person,其實(shí)也很容易理解清酥,只是為了隱藏NSKVONotifying_Person這個(gè)類扶镀,盡量隱藏KVO的內(nèi)部實(shí)現(xiàn)。
大家也可以看一下我下面附上的參考文章焰轻,寫的很不錯(cuò)臭觉。
結(jié)束語(yǔ)
經(jīng)過上面的層層分析,我們探究了KVO的實(shí)現(xiàn)原理辱志,有不縝密的地方還請(qǐng)指點(diǎn)蝠筑。
感謝閱讀。