探尋KVO本質(zhì)

面試問題:

· iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO?

· 如何手動(dòng)觸發(fā)KVO喂击?

我們通過以下幾個(gè)點(diǎn)來尋找這兩個(gè)問題的答案

KVO基本使用

首先需要了解KVO基本使用剂癌,KVO的全稱 Key-Value Observing,俗稱“鍵值監(jiān)聽”翰绊,可以用于監(jiān)聽某個(gè)對(duì)象屬性值的改變佩谷。

- (void)viewDidLoad {
  [super viewDidLoad];

  Person *p1 = [[Person alloc] init];
  Person *p2 = [[Person alloc] init];

  p1.age = 1;
  p2.age = 2;
  // self 監(jiān)聽 p1的 age屬性
  NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
  [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
  p1.age = 10;

   /*
  // 通過methodForSelector找到方法實(shí)現(xiàn)的地址
  NSLog(@"監(jiān)聽之前: p1 = %@",object_getClass(p1));

  NSLog(@"添加KVO監(jiān)聽之前 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
  [p1 addObserver:self forKeyPath:@"age" options:options context:nil];

  NSLog(@"監(jiān)聽之后: p1 = %@",object_getClass(p1));

  NSLog(@"添加KVO監(jiān)聽之后 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)] ,[p2 methodForSelector: @selector(setAge:)]); //這里獲取到了方法的地址之后,在xcode控制臺(tái)輸入: p (IMP)方法地址 监嗜,控制臺(tái)就會(huì)輸出方法相關(guān)的信息

  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;
}

上述代碼中可以看出谐檀,在添加監(jiān)聽之后,age屬性的值在發(fā)生改變時(shí)裁奇,就會(huì)通知到監(jiān)聽者桐猬,執(zhí)行監(jiān)聽者的observeValueForKeyPath方法。

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

通過上述代碼我們發(fā)現(xiàn)刽肠,一旦age屬性的值發(fā)生改變時(shí)溃肪,就會(huì)通知到監(jiān)聽者,并且我們知道賦值操作都是調(diào)用 set方法音五,我們可以來到Person類中重寫age的set方法惫撰,觀察是否是KVO在set方法內(nèi)部做了一些操作來通知監(jiān)聽者。

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

說明KVO在運(yùn)行時(shí)獲取對(duì)p1對(duì)象做了一些改變坚嗜。相當(dāng)于在程序運(yùn)行過程中夯膀,對(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)分析

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

image

通過上圖我們發(fā)現(xiàn)抓狭,p1對(duì)象執(zhí)行過addObserver操作之后,p1對(duì)象的isa指針由之前的指向類對(duì)象Person變?yōu)橹赶騈SKVONotifying_Person類對(duì)象造烁,而p2對(duì)象沒有任何改變否过。也就是說一旦p1對(duì)象添加了KVO監(jiān)聽以后午笛,其isa指針就會(huì)發(fā)生變化,因此set方法的執(zhí)行效果就不一樣了苗桂。

那么我們先來觀察p2對(duì)象在內(nèi)容中是如何存儲(chǔ)的药磺,然后對(duì)比p2來觀察p1。 首先我們知道煤伟,p2在調(diào)用setAge方法的時(shí)候癌佩,首先會(huì)通過p2對(duì)象中的isa指針找到Person類對(duì)象,然后在類對(duì)象中找到setAge方法便锨。然后找到方法對(duì)應(yīng)的實(shí)現(xiàn)围辙。如下圖所示

image

但是剛才我們發(fā)現(xiàn)p1對(duì)象的isa指針在經(jīng)過KVO監(jiān)聽之后已經(jīng)指向了NSKVONotifying_Person類對(duì)象,NSKVONotifying_Person其實(shí)是Person的子類放案,那么也就是說其superclass指針是指向Person類對(duì)象的姚建,NSKVONotifying_Person是runtime在運(yùn)行時(shí)生成的。那么p1對(duì)象在調(diào)用setAge方法的時(shí)候吱殉,肯定會(huì)根據(jù)p1的isa找到NSKVONotifying_Person掸冤,在NSKVONotifying_Person中找setAge的方法及實(shí)現(xiàn)。

經(jīng)過查閱資料我們可以了解到友雳。 NSKVONotifying_Person中的setAge方法中其實(shí)調(diào)用了 Fundation框架中C語(yǔ)言函數(shù) _NSsetIntValueAndNotify稿湿,_NSsetIntValueAndNotify內(nèi)部做的操作相當(dāng)于,首先調(diào)用willChangeValueForKey 將要改變方法沥阱,之后調(diào)用父類的setAge方法對(duì)成員變量賦值缎罢,最后調(diào)用didChangeValueForKey已經(jīng)改變方法。didChangeValueForKey中會(huì)調(diào)用監(jiān)聽器的監(jiān)聽方法考杉,最終來到監(jiān)聽者的observeValueForKeyPath方法中策精。

如何驗(yàn)證KVO真的如上面所講的方式實(shí)現(xiàn)?

首先經(jīng)過之前打斷點(diǎn)打印isa指針崇棠,我們已經(jīng)驗(yàn)證了咽袜,在執(zhí)行添加監(jiān)聽的方法時(shí),會(huì)將isa指針指向一個(gè)通過runtime創(chuàng)建的Person的子類NSKVONotifying_Person枕稀。 另外我們可以通過打印方法實(shí)現(xiàn)的地址來看一下p1和p2的setAge的方法實(shí)現(xiàn)的地址在添加KVO前后有什么變化询刹。

// 通過methodForSelector找到方法實(shí)現(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:)]);

image

我們發(fā)現(xiàn)在添加KVO監(jiān)聽之前,p1和p2的setAge方法實(shí)現(xiàn)的地址相同萎坷,而經(jīng)過KVO監(jiān)聽之后凹联,p1的setAge方法實(shí)現(xiàn)的地址發(fā)生了變化,我們通過打印方法實(shí)現(xiàn)來看一下前后的變化發(fā)現(xiàn)哆档,確實(shí)如我們上面所講的一樣蔽挠,p1的setAge方法的實(shí)現(xiàn)由Person類方法中的setAge方法轉(zhuǎn)換為了C語(yǔ)言的Foundation框架的_NSsetIntValueAndNotify函數(shù)。

Foundation框架中會(huì)根據(jù)屬性的類型瓜浸,調(diào)用不同的方法澳淑。例如我們之前定義的int類型的age屬性比原,那么我們看到Foundation框架中調(diào)用的_NSsetIntValueAndNotify函數(shù)。那么我們把a(bǔ)ge的屬性類型變?yōu)閐ouble重新打印一遍

image

我們發(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ù)

image

NSKVONotifying_Person內(nèi)部結(jié)構(gòu)

首先我們知道,NSKVONotifying_Person作為Person的子類出革,其superclass指針指向Person類造壮,并且NSKVONotifying_Person內(nèi)部一定對(duì)setAge方法做了單獨(dú)的實(shí)現(xiàn),那么NSKVONotifying_Person同Person類的差別可能就在于其內(nèi)存儲(chǔ)的對(duì)象方法及實(shí)現(xiàn)不同骂束。 我們通過runtime分別打印Person類對(duì)象和NSKVONotifying_Person類對(duì)象內(nèi)存儲(chǔ)的對(duì)象方法

- (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);
}

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

image

通過上述代碼我們發(fā)現(xiàn)NSKVONotifying_Person中有4個(gè)對(duì)象方法耳璧。分別為setAge: 、class 展箱、dealloc旨枯、 _isKVOA,那么至此我們可以畫出NSKVONotifying_Person的內(nèi)存結(jié)構(gòu)以及方法調(diào)用順序混驰。

image

這里NSKVONotifying_Person重寫class方法是為了隱藏NSKVONotifying_Person攀隔。不被外界所看到。我們?cè)趐1添加過KVO監(jiān)聽之后栖榨,分別打印p1和p2對(duì)象的class可以發(fā)現(xiàn)他們都返回Person昆汹。

NSLog(@"%@,%@",[p1 class],[p2 class]);

// 打印結(jié)果 Person,Person

如果NSKVONotifying_Person不重寫class方法,那么當(dāng)對(duì)象要調(diào)用class對(duì)象方法的時(shí)候就會(huì)一直向上找來到NSObject婴栽,而NSObject的class的實(shí)現(xiàn)大致為返回自己isa指向的類满粗,返回p1的isa指向的類那么打印出來的類就是NSKVONotifying_Person,但是apple不希望將NSKVONotifying_Person類暴露出來愚争,并且不希望我們知道NSKVONotifying_Person內(nèi)部實(shí)現(xiàn)映皆,所以在內(nèi)部重寫了class類,直接返回Person類轰枝,所以外界在調(diào)用p1的class對(duì)象方法時(shí)捅彻,是Person類。這樣p1給外界的感覺p1還是Person類鞍陨,并不知道NSKVONotifying_Person子類的存在步淹。

那么我們可以猜測(cè)NSKVONotifying_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");
}

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

image

回答問題:

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)建的子類畅铭,子類擁有自己的set方法實(shí)現(xiàn)氏淑,set方法實(shí)現(xiàn)內(nèi)部會(huì)順序調(diào)用willChangeValueForKey方法、原來的setter方法實(shí)現(xiàn)硕噩、didChangeValueForKey方法假残,而didChangeValueForKey方法內(nèi)部又會(huì)調(diào)用監(jiān)聽器的observeValueForKeyPath:ofObject:change:context:監(jiān)聽方法。

如何手動(dòng)觸發(fā)KVO 炉擅?

答: 被監(jiān)聽的屬性的值被修改時(shí)辉懒,就會(huì)自動(dòng)觸發(fā)KVO。如果想要手動(dòng)觸發(fā)KVO谍失,則需要我們自己調(diào)用willChangeValueForKey和didChangeValueForKey方法即可在不改變屬性值的情況下手動(dòng)觸發(fā)KVO眶俩,并且這兩個(gè)方法缺一不可。

通過以下代碼可以驗(yàn)證

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ā)現(xiàn)快鱼,didChangeValueForKey方法內(nèi)部成功調(diào)用了observeValueForKeyPath:ofObject:change:context:颠印,并且age的值并沒有發(fā)生改變。

image

綜上可以得出以下結(jié)論:

· KVO是基于runtime機(jī)制實(shí)現(xiàn)的

· 當(dāng)某個(gè)類的屬性對(duì)象第一次被觀察時(shí)抹竹,系統(tǒng)就會(huì)在運(yùn)行期動(dòng)態(tài)地創(chuàng)建該類的一個(gè)派生類线罕,在這個(gè)派生類中重寫基類中任何被觀察屬性的setter 方法。派生類在被重寫的setter方法內(nèi)實(shí)現(xiàn)真正的通知機(jī)制

· 如果原類為Person柒莉,那么生成的派生類名為NSKVONotifying_Person

· 每個(gè)類對(duì)象中都有一個(gè)isa指針指向當(dāng)前類闻坚,當(dāng)一個(gè)類對(duì)象的第一次被觀察,那么系統(tǒng)會(huì)偷偷將isa指針指向動(dòng)態(tài)生成的派生類兢孝,從而在給被監(jiān)控屬性賦值時(shí)執(zhí)行的是派生類的setter方法

· 鍵值觀察通知依賴于NSObject 的兩個(gè)方法: willChangeValueForKey: 和didChangevlueForKey:窿凤;在一個(gè)被觀察屬性發(fā)生改變之前, willChangeValueForKey: 一定會(huì)被調(diào)用跨蟹,這就會(huì)記錄舊的值雳殊。而當(dāng)改變發(fā)生后,didChangeValueForKey: 會(huì)被調(diào)用窗轩,繼而 observeValueForKey:ofObject:change:context: 也會(huì)被調(diào)用夯秃。

相關(guān)鏈接iOS底層原理總結(jié) - 探尋KVO本質(zhì) - 簡(jiǎn)書

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子仓洼,更是在濱河造成了極大的恐慌介陶,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件色建,死亡現(xiàn)場(chǎng)離奇詭異哺呜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)箕戳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門某残,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人陵吸,你說我怎么就攤上這事玻墅。” “怎么了壮虫?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵澳厢,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我旨指,道長(zhǎng)赏酥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任谆构,我火速辦了婚禮裸扶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘搬素。我一直安慰自己呵晨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布熬尺。 她就那樣靜靜地躺著摸屠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪粱哼。 梳的紋絲不亂的頭發(fā)上季二,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音揭措,去河邊找鬼胯舷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛绊含,可吹牛的內(nèi)容都是我干的桑嘶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼躬充,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼逃顶!你這毒婦竟也來了讨便?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤以政,失蹤者是張志新(化名)和其女友劉穎霸褒,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盈蛮,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡傲霸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了眉反。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡穆役,死狀恐怖寸五,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情耿币,我是刑警寧澤梳杏,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站淹接,受9級(jí)特大地震影響十性,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜塑悼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一劲适、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧厢蒜,春花似錦霞势、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至巷屿,卻和暖如春固以,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嘱巾。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工憨琳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人浓冒。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓栽渴,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親稳懒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子闲擦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

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