iOS底層系列:KVO

前言

最近打算重新梳理一遍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)

kvo_1.jpg

關(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)蝠筑。
感謝閱讀。

參考

mikeash.com: just this guy, you know?
自己實(shí)現(xiàn) KVO

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末荸频,一起剝皮案震驚了整個(gè)濱河市菱肖,隨后出現(xiàn)的幾起案子客冈,更是在濱河造成了極大的恐慌旭从,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件场仲,死亡現(xiàn)場(chǎng)離奇詭異和悦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)渠缕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門鸽素,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人亦鳞,你說我怎么就攤上這事馍忽。” “怎么了燕差?”我有些...
    開封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵遭笋,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我徒探,道長(zhǎng)瓦呼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任测暗,我火速辦了婚禮央串,結(jié)果婚禮上磨澡,老公的妹妹穿的比我還像新娘。我一直安慰自己质和,他們只是感情好稳摄,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著饲宿,像睡著了一般秩命。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上褒傅,一...
    開封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天弃锐,我揣著相機(jī)與錄音,去河邊找鬼殿托。 笑死霹菊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的支竹。 我是一名探鬼主播旋廷,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼礼搁!你這毒婦竟也來(lái)了饶碘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤馒吴,失蹤者是張志新(化名)和其女友劉穎扎运,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饮戳,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡豪治,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扯罐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片负拟。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖歹河,靈堂內(nèi)的尸體忽然破棺而出掩浙,到底是詐尸還是另有隱情,我是刑警寧澤秸歧,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布厨姚,位于F島的核電站,受9級(jí)特大地震影響寥茫,放射性物質(zhì)發(fā)生泄漏遣蚀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芭梯。 院中可真熱鬧险耀,春花似錦、人聲如沸玖喘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)累奈。三九已至贬派,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間澎媒,已是汗流浹背搞乏。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留戒努,地道東北人请敦。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像储玫,于是被迫代替她去往敵國(guó)和親侍筛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348