iOS底層原理總結(jié) - 探尋KVO本質(zhì)

問題

  1. iOS用什么方式實現(xiàn)對一個對象的KVO根悼?(KVO的本質(zhì)是什么?)
  2. 如何手動觸發(fā)KVO

首先需要了解KVO基本使用蜀撑,KVO的全稱 Key-Value Observing挤巡,俗稱“鍵值監(jiān)聽”,可以用于監(jiān)聽某個對象屬性值的改變酷麦。

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p1 = [[Person alloc] init];
    Person *p2 = [[Person alloc] init];
    p1.age = 1;
    p1.age = 2;
    p2.age = 2;
    // self 監(jiān)聽 p1的 age屬性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    p1.age = 10;
    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監(jiān)聽到%@的%@改變了%@", object, keyPath,change);
}

// 打印內(nèi)容
監(jiān)聽到<Person: 0x604000205460>的age改變了{(lán)
    kind = 1;
    new = 10;
    old = 2;
}
復(fù)制代碼

上述代碼中可以看出矿卑,在添加監(jiān)聽之后,age屬性的值在發(fā)生改變時沃饶,就會通知到監(jiān)聽者母廷,執(zhí)行監(jiān)聽者的observeValueForKeyPath方法瀑晒。

探尋KVO底層實現(xiàn)原理

通過上述代碼我們發(fā)現(xiàn),一旦age屬性的值發(fā)生改變時徘意,就會通知到監(jiān)聽者苔悦,并且我們知道賦值操作都是調(diào)用 set方法,我們可以來到Person類中重寫age的set方法椎咧,觀察是否是KVO在set方法內(nèi)部做了一些操作來通知監(jiān)聽者玖详。

我們發(fā)現(xiàn)即使重寫了set方法,p1對象和p2對象調(diào)用同樣的set方法勤讽,但是我們發(fā)現(xiàn)p1除了調(diào)用set方法之外還會另外執(zhí)行監(jiān)聽器的observeValueForKeyPath方法蟋座。

說明KVO在運行時獲取對p1對象做了一些改變。相當(dāng)于在程序運行過程中脚牍,對p1對象做了一些變化向臀,使得p1對象在調(diào)用setage方法的時候可能做了一些額外的操作,所以問題出在對象身上诸狭,兩個對象在內(nèi)存中肯定不一樣券膀,兩個對象可能本質(zhì)上并不一樣。接下來來探索KVO內(nèi)部是怎么實現(xiàn)的驯遇。

KVO底層實現(xiàn)分析

首先我們對上述代碼中添加監(jiān)聽的地方打斷點芹彬,看觀察一下,addObserver方法對p1對象做了什么處理叉庐?也就是說p1對象在經(jīng)過addObserver方法之后發(fā)生了什么改變舒帮,我們通過打印isa指針如下圖所示

image.png

通過上圖我們發(fā)現(xiàn),p1對象執(zhí)行過addObserver操作之后陡叠,p1對象的isa指針由之前的指向類對象Person變?yōu)橹赶騈SKVONotifyin_Person類對象玩郊,而p2對象沒有任何改變。也就是說一旦p1對象添加了KVO監(jiān)聽以后枉阵,其isa指針就會發(fā)生變化译红,因此set方法的執(zhí)行效果就不一樣了。

那么我們先來觀察p2對象在內(nèi)容中是如何存儲的岭妖,然后對比p2來觀察p1临庇。 首先我們知道,p2在調(diào)用setage方法的時候昵慌,首先會通過p2對象中的isa指針找到Person類對象,然后在類對象中找到setage方法淮蜈。然后找到方法對應(yīng)的實現(xiàn)斋攀。如下圖所示

image.png

但是剛才我們發(fā)現(xiàn)p1對象的isa指針在經(jīng)過KVO監(jiān)聽之后已經(jīng)指向了NSKVONotifyin_Person類對象,NSKVONotifyin_Person其實是Person的子類梧田,那么也就是說其superclass指針是指向Person類對象的淳蔼,NSKVONotifyin_Person是runtime在運行時生成的侧蘸。那么p1對象在調(diào)用setage方法的時候,肯定會根據(jù)p1的isa找到NSKVONotifyin_Person鹉梨,在NSKVONotifyin_Person中找setage的方法及實現(xiàn)讳癌。

經(jīng)過查閱資料我們可以了解到。 NSKVONotifyin_Person中的setage方法中其實調(diào)用了 Fundation框架中C語言函數(shù) _NSsetIntValueAndNotify存皂,_NSsetIntValueAndNotify內(nèi)部做的操作相當(dāng)于晌坤,首先調(diào)用willChangeValueForKey 將要改變方法,之后調(diào)用父類的setage方法對成員變量賦值旦袋,最后調(diào)用didChangeValueForKey已經(jīng)改變方法骤菠。didChangeValueForKey中會調(diào)用監(jiān)聽器的監(jiān)聽方法,最終來到監(jiān)聽者的observeValueForKeyPath方法中疤孕。

那么如何驗證KVO真的如上面所講的方式實現(xiàn)商乎?

首先經(jīng)過之前打斷點打印isa指針,我們已經(jīng)驗證了祭阀,在執(zhí)行添加監(jiān)聽的方法時鹉戚,會將isa指針指向一個通過runtime創(chuàng)建的Person的子類NSKVONotifyin_Person。 另外我們可以通過打印方法實現(xiàn)的地址來看一下p1和p2的setage的方法實現(xiàn)的地址在添加KVO前后有什么變化专控。

// 通過methodForSelector找到方法實現(xiàn)的地址
NSLog(@"添加KVO監(jiān)聽之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"添加KVO監(jiān)聽之后 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
復(fù)制代碼
image.png

我們發(fā)現(xiàn)在添加KVO監(jiān)聽之前崩瓤,p1和p2的setAge方法實現(xiàn)的地址相同,而經(jīng)過KVO監(jiān)聽之后踩官,p1的setAge方法實現(xiàn)的地址發(fā)生了變化却桶,我們通過打印方法實現(xiàn)來看一下前后的變化發(fā)現(xiàn),確實如我們上面所講的一樣蔗牡,p1的setAge方法的實現(xiàn)由Person類方法中的setAge方法轉(zhuǎn)換為了C語言的Foundation框架的_NSsetIntValueAndNotify函數(shù)颖系。

Foundation框架中會根據(jù)屬性的類型,調(diào)用不同的方法辩越。例如我們之前定義的int類型的age屬性嘁扼,那么我們看到Foundation框架中調(diào)用的_NSsetIntValueAndNotify函數(shù)。那么我們把age的屬性類型變?yōu)?strong>double重新打印一遍

image.png

我們發(fā)現(xiàn)調(diào)用的函數(shù)變?yōu)榱薩NSSetDoubleValueAndNotify黔攒,那么這說明Foundation框架中有許多此類型的函數(shù)趁啸,通過屬性的不同類型調(diào)用不同的函數(shù)。 那么我們可以推測Foundation框架中還有很多例如_NSSetBoolValueAndNotify督惰、_NSSetCharValueAndNotify不傅、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函數(shù)赏胚。

我們可以找到Foundation框架文件访娶,通過命令行查詢關(guān)鍵字找到相關(guān)函數(shù)

image.png

NSKVONotifyin_Person內(nèi)部結(jié)構(gòu)是怎樣的?

首先我們知道觉阅,NSKVONotifyin_Person作為Person的子類崖疤,其superclass指針指向Person類秘车,并且NSKVONotifyin_Person內(nèi)部一定對setAge方法做了單獨的實現(xiàn),那么NSKVONotifyin_Person同Person類的差別可能就在于其內(nèi)存儲的對象方法及實現(xiàn)不同劫哼。 我們通過runtime分別打印Person類對象和NSKVONotifyin_Person類對象內(nèi)存儲的對象方法

- (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);
}
復(fù)制代碼

上述打印內(nèi)容如下

image.png

通過上述代碼我們發(fā)現(xiàn)NSKVONotifyin_Person中有4個對象方法叮趴。分別為setAge: class dealloc _isKVOA,那么至此我們可以畫出NSKVONotifyin_Person的內(nèi)存結(jié)構(gòu)以及方法調(diào)用順序权烧。

image.png

這里NSKVONotifyin_Person重寫class方法是為了隱藏NSKVONotifyin_Person眯亦。不被外界所看到晤郑。我們在p1添加過KVO監(jiān)聽之后院溺,分別打印p1和p2對象的class可以發(fā)現(xiàn)他們都返回Person碑隆。

NSLog(@"%@,%@",[p1 class],[p2 class]);
// 打印結(jié)果 Person,Person
復(fù)制代碼

如果NSKVONotifyin_Person不重寫class方法印屁,那么當(dāng)對象要調(diào)用class對象方法的時候就會一直向上找來到nsobject审胸,而nsobect的class的實現(xiàn)大致為返回自己isa指向的類卓缰,返回p1的isa指向的類那么打印出來的類就是NSKVONotifyin_Person蛛株,但是apple不希望將NSKVONotifyin_Person類暴露出來抓半,并且不希望我們知道NSKVONotifyin_Person內(nèi)部實現(xiàn)扔字,所以在內(nèi)部重寫了class類囊嘉,直接返回Person類,所以外界在調(diào)用p1的class對象方法時革为,是Person類扭粱。這樣p1給外界的感覺p1還是Person類,并不知道NSKVONotifyin_Person子類的存在震檩。

那么我們可以猜測NSKVONotifyin_Person內(nèi)重寫的class內(nèi)部實現(xiàn)大致為

- (Class) class {
     // 得到類對象琢蛤,在找到類對象父類
     return class_getSuperclass(object_getClass(self));
}
復(fù)制代碼

驗證didChangeValueForKey:內(nèi)部會調(diào)用observer的observeValueForKeyPath:ofObject:change:context:方法

我們在Person類中重寫willChangeValueForKey:和didChangeValueForKey:方法,模擬他們的實現(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");
}
復(fù)制代碼

再次運行來查看didChangeValueForKey的方法內(nèi)運行過程博其,通過打印內(nèi)容可以看到,確實在didChangeValueForKey方法內(nèi)部已經(jīng)調(diào)用了observer的observeValueForKeyPath:ofObject:change:context:方法迂猴。

image.png

回答問題:

  1. iOS用什么方式實現(xiàn)對一個對象的KVO慕淡?(KVO的本質(zhì)是什么?) 答. 當(dāng)一個對象使用了KVO監(jiān)聽沸毁,iOS系統(tǒng)會修改這個對象的isa指針峰髓,改為指向一個全新的通過Runtime動態(tài)創(chuàng)建的子類,子類擁有自己的set方法實現(xiàn)息尺,set方法實現(xiàn)內(nèi)部會順序調(diào)用willChangeValueForKey方法携兵、原來的setter方法實現(xiàn)、didChangeValueForKey方法掷倔,而didChangeValueForKey方法內(nèi)部又會調(diào)用監(jiān)聽器的observeValueForKeyPath:ofObject:change:context:監(jiān)聽方法眉孩。
  1. 如何手動觸發(fā)KVO 答. 被監(jiān)聽的屬性的值被修改時,就會自動觸發(fā)KVO勒葱。如果想要手動觸發(fā)KVO浪汪,則需要我們自己調(diào)用willChangeValueForKey和didChangeValueForKey方法即可在不改變屬性值的情況下手動觸發(fā)KVO,并且這兩個方法缺一不可凛虽。

通過以下代碼可以驗證

Person *p1 = [[Person alloc] init];
p1.age = 1.0;

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];

[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];

[p1 removeObserver:self forKeyPath:@"age"];
復(fù)制代碼
image.png

通過打印我們可以發(fā)現(xiàn)死遭,didChangeValueForKey方法內(nèi)部成功調(diào)用了observeValueForKeyPath:ofObject:change:context:,并且age的值并沒有發(fā)生改變凯旋。

以下文章可以做一個學(xué)習(xí)參考:
GCD面試要點
block面試要點
Runtime面試要點
RunLoop面試要點
內(nèi)存管理面試要點
MVC呀潭、MVVM面試要點
網(wǎng)絡(luò)性能優(yōu)化面試要點
網(wǎng)絡(luò)編程面試要點
KVC&KVO面試要點
數(shù)據(jù)存儲面試要點
混編技術(shù)面試要點
設(shè)計模式面試要點
UI面試要點
原文地址:https://juejin.cn/post/6844903593925935117

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市至非,隨后出現(xiàn)的幾起案子钠署,更是在濱河造成了極大的恐慌,老刑警劉巖荒椭,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谐鼎,死亡現(xiàn)場離奇詭異,居然都是意外死亡趣惠,警方通過查閱死者的電腦和手機狸棍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來味悄,“玉大人草戈,你說我怎么就攤上這事∈躺” “怎么了唐片?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長涨颜。 經(jīng)常有香客問我费韭,道長,這世上最難降的妖魔是什么咐低? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任揽思,我火速辦了婚禮,結(jié)果婚禮上见擦,老公的妹妹穿的比我還像新娘钉汗。我一直安慰自己,他們只是感情好鲤屡,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布损痰。 她就那樣靜靜地躺著,像睡著了一般酒来。 火紅的嫁衣襯著肌膚如雪卢未。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音辽社,去河邊找鬼伟墙。 笑死,一個胖子當(dāng)著我的面吹牛滴铅,可吹牛的內(nèi)容都是我干的戳葵。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼汉匙,長吁一口氣:“原來是場噩夢啊……” “哼拱烁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起噩翠,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤戏自,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后伤锚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體擅笔,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年见芹,在試婚紗的時候發(fā)現(xiàn)自己被綠了剂娄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡玄呛,死狀恐怖阅懦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情徘铝,我是刑警寧澤耳胎,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站惕它,受9級特大地震影響怕午,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜淹魄,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一郁惜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧甲锡,春花似錦兆蕉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至缸废,卻和暖如春包蓝,著一層夾襖步出監(jiān)牢的瞬間驶社,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工测萎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留亡电,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓绳泉,卻偏偏與公主長得像逊抡,于是被迫代替她去往敵國和親姆泻。 傳聞我的和親對象是個殘疾皇子零酪,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容