KVO在運行時到底是怎么實現(xiàn)的

本文為大地瓜原創(chuàng)抑月,歡迎知識共享违崇,轉(zhuǎn)載請注明出處。
雖然你不注明出處我也沒什么精力和你計較靴跛。
作者微信號:christgreenlaw


本文的原文缀雳。本文只對其進行翻譯。


What Is It? 這是個啥玩意梢睛?

很多讀者早就知道這個東西了肥印,但是我們還是要簡單的回憶一下:KVO是Cocoa Bindings底層的技術(shù)识椰,讓某些對象能夠在其他對象的屬性被更改時獲得通知。一個對象觀察(observe)另一個對象的key深碱。當被觀察的對象更改了這個key 的值時腹鹉,觀察者就得到了通知。挺好理解的是吧莹痢?牛逼的地方在于:KVO一般不需要給被觀察者寫任何的代碼种蘸。

OverView 概覽

所以說到底這是怎么做到的?竟然不需要給被觀察者寫代碼竞膳?其實秘密就在OC runtime里航瞭。當你第一次觀察一個特定類的一個對象時,KVO就在運行時創(chuàng)建一個全新的類坦辟,這個類其實繼承了你所觀察的類刊侯。在這個新類中,它會將你所觀察的屬性(key)的setter重寫锉走。然后它修改你這個對象的isa指針(也就是告知OC runtime某一塊內(nèi)存到底是什么類型的對象的指針)滨彻,這樣你的對象就成了這個新類的實例。

被重寫的方法執(zhí)行了通知觀察者的工作挪蹭。所以這個邏輯就是:對一個key 的修改必須經(jīng)過key 的set方法亭饵。對set方法的修改使得set方法可以截獲修改,并在set方法被調(diào)用時向觀察者發(fā)送通知梁厉。(當然辜羊,如果你直接修改實例變量的話,你也可能不經(jīng)過set方法就進行修改词顾。KVO要求進行KVO的類要么決不能這樣做(也就是決不能直接修改實例變量)八秃,要么必須將對ivar的直接訪問包裝在手動的通知調(diào)用中。)

有點棘手的是:Apple實際上不希望別人知道這個技術(shù)原理肉盹。不僅僅是setter昔驱,動態(tài)繼承(dynamic subclass)也重寫了-class方法來欺騙你,返回原始的類上忍!如果你不仔細觀察的話骤肛,KVO觀察的對象就和其他沒有被觀察的部分一模一樣。

Digging Deeper 再挖掘一下

不多廢話了窍蓝,我們來看看到底是怎樣工作的萌衬。我寫了一個程序,它展示了KVO背后的原理它抱。由于動態(tài)KVO繼承(dynamic KVO subclass)希望隱藏自己的存在,我主要使用了OC runtime'方法來獲得我們希望獲得的信息朴艰。

以下是代碼:

// gcc -o kvoexplorer -framework Foundation kvoexplorer.m
    
    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    
    
    @interface TestClass : NSObject
    {
        int x;
        int y;
        int z;
    }
    @property int x;
    @property int y;
    @property int z;
    @end
    @implementation TestClass
    @synthesize x, y, z;
    @end
    
    static NSArray *ClassMethodNames(Class c)
    {
        NSMutableArray *array = [NSMutableArray array];
        
        unsigned int methodCount = 0;
        Method *methodList = class_copyMethodList(c, &methodCount);
        unsigned int i;
        for(i = 0; i < methodCount; i++)
            [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
        free(methodList);
        
        return array;
    }
    
    static void PrintDescription(NSString *name, id obj)
    {
        NSString *str = [NSString stringWithFormat:
            @"%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
            name,
            obj,
            class_getName([obj class]),
            class_getName(obj->isa),
            [ClassMethodNames(obj->isa) componentsJoinedByString:@", "]];
        printf("%s\n", [str UTF8String]);
    }
    
    int main(int argc, char **argv)
    {
        [NSAutoreleasePool new];
        
        TestClass *x = [[TestClass alloc] init];
        TestClass *y = [[TestClass alloc] init];
        TestClass *xy = [[TestClass alloc] init];
        TestClass *control = [[TestClass alloc] init];
        
        [x addObserver:x forKeyPath:@"x" options:0 context:NULL];
        [xy addObserver:xy forKeyPath:@"x" options:0 context:NULL];
        [y addObserver:y forKeyPath:@"y" options:0 context:NULL];
        [xy addObserver:xy forKeyPath:@"y" options:0 context:NULL];
        
        PrintDescription(@"control", control);
        PrintDescription(@"x", x);
        PrintDescription(@"y", y);
        PrintDescription(@"xy", xy);
        
        printf("Using NSObject methods, normal setX: is %p, overridden setX: is %p\n",
              [control methodForSelector:@selector(setX:)],
              [x methodForSelector:@selector(setX:)]);
        printf("Using libobjc functions, normal setX: is %p, overridden setX: is %p\n",
              method_getImplementation(class_getInstanceMethod(object_getClass(control),
                                       @selector(setX:))),
              method_getImplementation(class_getInstanceMethod(object_getClass(x),
                                       @selector(setX:))));
        
        return 0;
    }

大地瓜注:這段代碼似乎在Xcode9下無法正確執(zhí)行观蓄,會提示obj->isa這一行有問題混移。

我們來重頭到尾分析一下。

首先我們定義了一個TestClass類侮穿,有三個屬性歌径。(KVO對非@property的key也有效,但是這樣最容易定義setter和getter)亲茅。

接下來定義了一組功能函數(shù)回铛。ClassMethodNames使用了OC運行時方法遍歷一個類然后得到其實現(xiàn)的所有方法的列表。要注意:它只是獲得了在class中直接實現(xiàn)的方法克锣,并不獲取超類的方法茵肃。PrintDescription把傳入的對象的詳細描述打印,展示了通過-class方法獲得的對象的類袭祟,也展示了通過OCruntime函數(shù)獲得的對象的類以及這個類上實現(xiàn)的方法验残。

接下來我們開始試驗這些功能。我們創(chuàng)建了TestClass的四個實例巾乳,每一個都用不同的方法進行觀察您没。x實例觀察x的key,y也類似胆绊,xy將會同時觀察x和y的key氨鹏。z 的key不進行觀察以進行對比。最后control實例作為一個實驗中的控制變量压状,將不會進行觀察仆抵。

接下來我們打印出四個對象的描述。

接下來我們更深入的挖掘一下重寫的setter何缓,將control對象的和被觀察對象的-setX:方法的實現(xiàn)的地址打印出來肢础,進行對比。我們做兩次這個操作碌廓,因為使用-methodForSelector:不能展示出重寫传轰。KVO期望隱藏動態(tài)繼承,甚至希望用這個技術(shù)隱藏重寫的方法谷婆!但是使用OCruntime函數(shù)卻可以輸出合理的結(jié)果慨蛙。

運行代碼

所以說以上就是代碼所做的事。我們來看一下運行的結(jié)果:

    control: <TestClass: 0x104b20>
        NSObject class TestClass
        libobjc class TestClass
        implements methods <setX:, x, setY:, y, setZ:, z>
    x: <TestClass: 0x103280>
        NSObject class TestClass
        libobjc class NSKVONotifying_TestClass
        implements methods <setY:, setX:, class, dealloc, _isKVOA>
    y: <TestClass: 0x104b00>
        NSObject class TestClass
        libobjc class NSKVONotifying_TestClass
        implements methods <setY:, setX:, class, dealloc, _isKVOA>
    xy: <TestClass: 0x104b10>
        NSObject class TestClass
        libobjc class NSKVONotifying_TestClass
        implements methods <setY:, setX:, class, dealloc, _isKVOA>
    Using NSObject methods, normal setX: is 0x195e, overridden setX: is 0x195e
    Using libobjc functions, normal setX: is 0x195e, overridden setX: is 0x96a1a550

首先打印出了control對象纪挎。與期望相符的是期贫,它所屬的類是TestClass,并且也實現(xiàn)了我們從類的屬性所同步來的六個方法异袄。

接下來打印了三個被觀察的對象通砍。注意到:盡管-class方法還是顯示TestClass,使用object_getClass卻展現(xiàn)這個對象的真實面貌:它是一個NSKVONotifying_TestClass對象。這就是動態(tài)繼承的子類封孙!

來看一下它是怎么實現(xiàn)兩個被觀察的setter的迹冤。你會注意到不重寫-setZ:是很聰明的,因為即使它也是一個setter虎忌,但是并沒有人觀察它泡徙。我們可以推測,如果我們給z也添加一個觀察者膜蠢,那么NSKVONotifying_Class也會給-setZ:進行重寫堪藐。同時也要注意三個實例同屬一個類,也就是說它們都重寫了兩個setter挑围,盡管他們兩個都只被觀察了一個屬性礁竞。即使對于未觀察的屬性也調(diào)用被觀察的setter,這樣會有性能損耗贪惹,但是Apple顯然覺得:如果每個對象都有一組不同的key被觀察苏章,不生成多個動態(tài)繼承的子類是更合適的。我也覺得這樣是對的奏瞬。

你也會觀察到三個其他的方法枫绅。這就是前面提到的被重寫的-class方法,這個方法希望隱藏掉這個動態(tài)子類的存在硼端。還有一個-dealloc方法來進行清理并淋。另外還有一個神秘的-_isKVOA方法,這個方法看起來像是一個私有方法珍昨,Apple的代碼可以使用這個方法來確定一個獨享是不是服從動態(tài)繼承县耽。

接下來我們打印出了-setX:的實現(xiàn)。使用-methodForSelector:給兩者返回了相同的值镣典。既然在動態(tài)子類中不存在這個方法的重寫兔毙,這肯定意味著-methodForSelector:使用了-class作為內(nèi)部實現(xiàn)的一部分,也因此獲得了錯誤的結(jié)果兄春。

所以我們當然就略過這些東西澎剥,使用了OCruntime來打印出了實現(xiàn),然后我們就能看到不同了赶舆。原始的方法和-methodForSelector:相同哑姚,但第二個完全不同。

作為優(yōu)秀的探索者芜茵,我們在debugger中運行叙量,這樣我們可以清晰的看到第二個函數(shù)是什么:

(gdb) print (IMP)0x96a1a550
$1 = (IMP) 0x96a1a550 <_NSSetIntValueAndNotify>

這是某種私有方法,實現(xiàn)了觀察的通知工作九串。通過使用Foundation中的nm -a绞佩,我們能夠得到私有方法的完整列表。

0013df80 t __NSSetBoolValueAndNotify
    000a0480 t __NSSetCharValueAndNotify
    0013e120 t __NSSetDoubleValueAndNotify
    0013e1f0 t __NSSetFloatValueAndNotify
    000e3550 t __NSSetIntValueAndNotify
    0013e390 t __NSSetLongLongValueAndNotify
    0013e2c0 t __NSSetLongValueAndNotify
    00089df0 t __NSSetObjectValueAndNotify
    0013e6f0 t __NSSetPointValueAndNotify
    0013e7d0 t __NSSetRangeValueAndNotify
    0013e8b0 t __NSSetRectValueAndNotify
    0013e550 t __NSSetShortValueAndNotify
    0008ab20 t __NSSetSizeValueAndNotify
    0013e050 t __NSSetUnsignedCharValueAndNotify
    0009fcd0 t __NSSetUnsignedIntValueAndNotify
    0013e470 t __NSSetUnsignedLongLongValueAndNotify
    0009fc00 t __NSSetUnsignedLongValueAndNotify
    0013e620 t __NSSetUnsignedShortValueAndNotify

這個列表中我們可以發(fā)現(xiàn)很多有趣的東西。首先你可能看到了Apple不得不為它們想支持的每一個原始類型都實現(xiàn)了獨立的函數(shù)征炼。它們對于OC對象(_NSSetObjectValueAndNotify)只需要一個方法析既,但是需要對其余的部分實現(xiàn)一整套方法。這套方法好像不太完整谆奥,其中不包含long double _Bool的方法。甚至對普通指針類型也沒有方法拂玻,就比如CFTypeDef的酸些。盡管對于不同的常見Cocoa structs有幾個函數(shù),顯然這里并沒有其他特別多的structs的函數(shù)檐蚜。這意味著這些類型的屬性不能進行自動的KVO通知魄懂,一定要記住4车凇J欣酢!

KVO是一個很強大的技術(shù)咳短,有時候可能有點過于強大了填帽,尤其是當涉及到自動通知的時候。現(xiàn)在你很清楚它內(nèi)部是怎么工作的了咙好,這個認知會幫助你決定怎么使用它(KVO)篡腌,或者在出錯的時候幫助你進行debug。

如果你準備在你自己的應(yīng)用中使用KVO勾效,你也許想看看我的文章Key-Value Observing Done Right嘹悼。

總結(jié)

以下并非關(guān)鍵內(nèi)容,大地瓜不翻譯了层宫。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末杨伙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子萌腿,更是在濱河造成了極大的恐慌限匣,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哮奇,死亡現(xiàn)場離奇詭異膛腐,居然都是意外死亡,警方通過查閱死者的電腦和手機鼎俘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門哲身,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贸伐,你說我怎么就攤上這事勘天。” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵脯丝,是天一觀的道長商膊。 經(jīng)常有香客問我,道長宠进,這世上最難降的妖魔是什么晕拆? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮材蹬,結(jié)果婚禮上实幕,老公的妹妹穿的比我還像新娘。我一直安慰自己堤器,他們只是感情好昆庇,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著闸溃,像睡著了一般整吆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辉川,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天表蝙,我揣著相機與錄音,去河邊找鬼员串。 笑死勇哗,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的寸齐。 我是一名探鬼主播欲诺,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼渺鹦!你這毒婦竟也來了扰法?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤毅厚,失蹤者是張志新(化名)和其女友劉穎塞颁,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吸耿,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡祠锣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了咽安。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伴网。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖妆棒,靈堂內(nèi)的尸體忽然破棺而出澡腾,到底是詐尸還是另有隱情沸伏,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布动分,位于F島的核電站毅糟,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏澜公。R本人自食惡果不足惜姆另,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望玛瘸。 院中可真熱鬧蜕青,春花似錦、人聲如沸糊渊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渺绒。三九已至,卻和暖如春菱鸥,著一層夾襖步出監(jiān)牢的瞬間宗兼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工氮采, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留殷绍,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓鹊漠,卻偏偏與公主長得像主到,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子躯概,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

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