KVO 底層本質(zhì)

  • 一、KVO 的一個(gè)疑惑
  • 二喊式、KVO 的淺層分析
  • 三、KVO 淺層分析驗(yàn)證
  • 四萧朝、KVO 子類內(nèi)部方法
  • 五垃帅、手動(dòng)觸發(fā) KVO

一、KVO 的一個(gè)疑惑

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
    // 給person1對(duì)象添加KVO監(jiān)聽(tīng)
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person1.age = 21;
    self.person2.age = 22;
}
- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"監(jiān)聽(tīng)到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end
@implementation Person
- (void)setAge:(int)age{
    _age = age;
}
@end

上述是一段簡(jiǎn)單的 KVO 使用代碼剪勿,但如果仔細(xì)一想確實(shí)有個(gè)很大的疑惑贸诚。兩個(gè)Person對(duì)象調(diào)用了類似的方法,其中touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event方法中厕吉,兩個(gè) person 對(duì)象賦值的本質(zhì)都是調(diào)用了Person類中的 - (void)setAge:(int)age 方法酱固。唯一的不同點(diǎn)在于person1添加了KVO監(jiān)聽(tīng)。person1 會(huì)走 KVO 監(jiān)聽(tīng)回調(diào)头朱,person2卻不走監(jiān)聽(tīng)回調(diào)运悲。

相信有不少開(kāi)發(fā)者多少了解各大概,是通過(guò)運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建子類實(shí)現(xiàn) KVO 機(jī)制项钮。但是不得不承認(rèn)班眯,蘋(píng)果的這個(gè) KVO 機(jī)制設(shè)計(jì)的非常巧妙,僅僅一行代碼便能實(shí)現(xiàn)意想不到的結(jié)果烁巫。為了更深入的了解 KVO 機(jī)制署隘,下面會(huì)結(jié)合相關(guān)底層源碼分析 KVO 機(jī)制。

二亚隙、KVO 的淺層分析

使用 LLDB 在 touchesBegan 方法內(nèi)斷點(diǎn)打印 person1person2isa指針磁餐,即 p self.person1.isap self.person2.isa ,會(huì)發(fā)現(xiàn)打印結(jié)果不同。其中person1的 isa 指針指向的類為NSKVONotifying_Person,而 person2 的 isa 指針指向的類為Person阿弃。(說(shuō)明: 實(shí)例對(duì)象的 isa 指向類诊霹,類的 isa 指針指向元類羞延,元類的 isa 指針指向根元類,根元類的 isa 指針指向根元類自身)脾还。

結(jié)合上述 LLDB 調(diào)試結(jié)果伴箩,可以進(jìn)一步分析兩個(gè) person 實(shí)例對(duì)象的內(nèi)存布局。首先來(lái)看鄙漏,未使用 KVO 監(jiān)聽(tīng)對(duì)象的內(nèi)存布局嗤谚,即person2對(duì)象。person2 的 isa 指針指向 Person 的 Class 對(duì)象泥张, Person 的 Class 對(duì)象中包含 isa 指針呵恢,superclass指針鞠值,以及age屬性 對(duì)應(yīng)的的 setAge:age 方法媚创。總的來(lái)說(shuō)彤恶,person2 對(duì)象的內(nèi)存布局和普通對(duì)象的內(nèi)存布局無(wú)任何特殊之處钞钙。

未使用 KVO 監(jiān)聽(tīng)對(duì)象的內(nèi)存布局

再來(lái)看看,使用 KVO 監(jiān)聽(tīng)對(duì)象的內(nèi)存布局声离,即 person1 對(duì)象芒炼。

使用 KVO 監(jiān)聽(tīng)對(duì)象的內(nèi)存布局

通過(guò)上述 LLDB 調(diào)試可以看出 person1 的 isa 指向 NSKVONotifying_Person, 該類是借助 runtime 動(dòng)態(tài)生成的類, NSKVONotifying_Person實(shí)際上是 Person的子類术徊,故 superclass 指向 Person 類本刽。 self.person1.age = 21會(huì)調(diào)用 NSKVONotifying_Person 類中的 setAge 方法,setAge 方法中會(huì)調(diào)用 Foundation 框架中的 _NSSetIntValueAndNotify 方法 赠涮, _NSSetIntValueAndNotify 方法內(nèi)部依次調(diào)用willChangeValueForKey 子寓、 super setAgedidChangeValueForKey 三個(gè)方法笋除。 didChangeValueForKey 方法通知監(jiān)聽(tīng)器屬性值發(fā)生變化斜友。大概的偽代碼形式如下:

@implementation NSKVONotifying_Person
- (void)setAge:(int)age{
    _NSSetIntValueAndNotify();
}
// 偽代碼
void _NSSetIntValueAndNotify(){
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
    // 通知監(jiān)聽(tīng)器,某某屬性值發(fā)生了改變
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end

為了證明偽代碼 _NSSetIntValueAndNotify 的內(nèi)部調(diào)用方法執(zhí)行順序垃它,可重寫(xiě) Person 類的如下方法鲜屏,并輸出 log 信息。

- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
}
- (void)setAge:(int)age{
    _age = age;
    NSLog(@"setAge:");
}
- (void)didChangeValueForKey:(NSString *)key{
    NSLog(@"didChangeValueForKey - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey - end");
}

如下打印結(jié)果国拇,可證明偽代碼 _NSSetIntValueAndNotify 的實(shí)現(xiàn)步驟洛史。

willChangeValueForKey
setAge:
didChangeValueForKey - begin
監(jiān)聽(tīng)到<Person: 0x600002a8a900>的age屬性值改變了 - {
    kind = 1;
    new = 21;
    old = 1;
} - 123
didChangeValueForKey - end

三、KVO 淺層分析驗(yàn)證

3.1 驗(yàn)證一

如果在原有工程中酱吝,創(chuàng)建NSKVONotifying_Person類虹菲,運(yùn)行代碼會(huì)報(bào) KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class 錯(cuò)誤,因?yàn)樵泄こ讨幸呀?jīng)存在該類掉瞳,故無(wú)法運(yùn)行時(shí)生成該類毕源。

3.2 驗(yàn)證二

在 person 對(duì)象調(diào)用 addObserver: forKeyPath: options: context: 方法之前和之后添加如下代碼浪漠,打印結(jié)果分別為添加KVO監(jiān)聽(tīng)之前 - Person Person添加KVO監(jiān)聽(tīng)之后 - NSKVONotifying_Person Person

NSLog(@"添加KVO監(jiān)聽(tīng)之前 - %@ %@",
          object_getClass(self.person1),
          object_getClass(self.person2));//添加KVO監(jiān)聽(tīng)之前 - Person Person

NSLog(@"添加KVO監(jiān)聽(tīng)之后 - %@ %@",
          object_getClass(self.person1),
          object_getClass(self.person2));//添加KVO監(jiān)聽(tīng)之后 - NSKVONotifying_Person Person

3.3 驗(yàn)證三

在 person 對(duì)象調(diào)用 addObserver: forKeyPath: options: context: 方法之前和之后添加如下代碼,打印結(jié)果分別為 添加KVO監(jiān)聽(tīng)之前 - 0x106515c90 0x106515c90添加KVO監(jiān)聽(tīng)之后 - 0x10686ffc2 0x106515c90 霎褐。

NSLog(@"添加KVO監(jiān)聽(tīng)之前 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

NSLog(@"添加KVO監(jiān)聽(tīng)之后 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

進(jìn)一步借助 LLDB 調(diào)試 p (IMP)0x106515c90p (IMP) 0x10686ffc2的打印結(jié)果如下:

四址愿、KVO 子類內(nèi)部方法


上圖中可以看出子類 NSKVONotifying_Person 除了重寫(xiě) setAge:方法,還重寫(xiě)了class冻璃、dealloc响谓、以及_isKVOA方法。為了證明NSKVONotifying_Person中存在上面提到的四個(gè)方法省艳,可借助class_copyMethodList方法打印特定類中的方法娘纷。打印結(jié)果為SKVONotifying_Person setAge:, class, dealloc, _isKVOA,

- (void)printMethodNamesOfClass:(Class)cls{
    unsigned int count;
    // 獲得方法數(shù)組
    Method *methodList = class_copyMethodList(cls, &count);
    // 存儲(chǔ)方法名
    NSMutableString *methodNames = [NSMutableString string];
    // 遍歷所有的方法
    for (int i = 0; i < count; i++) {
        // 獲得方法
        Method method = methodList[I];
        // 獲得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    // 釋放
    free(methodList);
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}
  • dealloc 方法不難理解,主要是做一些收尾工作跋炕,解決內(nèi)存問(wèn)題赖晶。
  • class 方法內(nèi)部大概實(shí)現(xiàn)是return [Person class]。試想辐烂,如果不這樣重寫(xiě)class方法遏插,那么 person1 在添加 KVO 監(jiān)聽(tīng)之后,調(diào)用object_getClass(object_getClass(self.person1))[self.person1 class] 將返回NSKVONotifying_Person, 這顯然與實(shí)際情況不符合纠修。因此可以猜測(cè)class 方法內(nèi)部大概實(shí)現(xiàn)是return [Person class]胳嘲,從而屏蔽了 NSKVONotifying_Person 底層類的存在。

五扣草、手動(dòng)觸發(fā) KVO

KVO 的觸發(fā)條件一般是修改監(jiān)聽(tīng)對(duì)象屬性值了牛,但是如何在不修改被監(jiān)聽(tīng)屬性值的情況下觸發(fā) KVO 監(jiān)聽(tīng)回調(diào)。也就是所謂的手動(dòng)觸發(fā) KVO 辰妙,通過(guò)下面兩行代碼可手動(dòng)觸發(fā) KVO, 注意必須同時(shí)調(diào)用下面兩行代碼鹰祸。

[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];

由此也可以知道直接修改成員變量不會(huì)觸發(fā) KVO 監(jiān)聽(tīng)方法,因?yàn)?KVO 的本質(zhì)是重寫(xiě)了 set 方法上岗, set 方法內(nèi)部調(diào)用了willChangeValueForKeydidChangeValueForKey 方法福荸,直接修改成員變量并不會(huì)調(diào)用 set 方法。

另外肴掷,通過(guò) KVC 修改屬性也會(huì)觸發(fā) KVO 監(jiān)聽(tīng)敬锐,因?yàn)?KVC 中setValue:forKey:會(huì)按照setKey_setKey的順序查找方法呆瞻,setValue:forKey: 內(nèi)部調(diào)用順序如下台夺,這里不再展開(kāi)說(shuō)明。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末痴脾,一起剝皮案震驚了整個(gè)濱河市颤介,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖滚朵,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冤灾,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡辕近,警方通過(guò)查閱死者的電腦和手機(jī)韵吨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)移宅,“玉大人归粉,你說(shuō)我怎么就攤上這事÷┓澹” “怎么了糠悼?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)浅乔。 經(jīng)常有香客問(wèn)我倔喂,道長(zhǎng),這世上最難降的妖魔是什么童擎? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任滴劲,我火速辦了婚禮攻晒,結(jié)果婚禮上顾复,老公的妹妹穿的比我還像新娘。我一直安慰自己鲁捏,他們只是感情好芯砸,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著给梅,像睡著了一般假丧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上动羽,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天包帚,我揣著相機(jī)與錄音,去河邊找鬼运吓。 笑死渴邦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拘哨。 我是一名探鬼主播谋梭,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼倦青!你這毒婦竟也來(lái)了瓮床?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎隘庄,沒(méi)想到半個(gè)月后踢步,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡丑掺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年贾虽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吼鱼。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蓬豁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出菇肃,到底是詐尸還是另有隱情地粪,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布琐谤,位于F島的核電站蟆技,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏斗忌。R本人自食惡果不足惜质礼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望织阳。 院中可真熱鬧眶蕉,春花似錦、人聲如沸唧躲。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)弄痹。三九已至饭入,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肛真,已是汗流浹背谐丢。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚓让,地道東北人乾忱。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像凭疮,于是被迫代替她去往敵國(guó)和親饭耳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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

  • 面試題引發(fā)的思考: Q: Category(類別/分類)和Extension(擴(kuò)展)的區(qū)別是什么执解? Extensi...
    hazydream閱讀 385評(píng)論 0 1
  • 面試問(wèn)題: iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO寞肖? 如何手動(dòng)觸發(fā)KVO纲酗? KVO簡(jiǎn)介 KVO就是鍵值觀測(cè)。有時(shí)候...
    雪山飛狐_91ae閱讀 4,564評(píng)論 10 36
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,320評(píng)論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,089評(píng)論 1 32
  • 問(wèn)題 iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO新蟆?(KVO的本質(zhì)是什么觅赊?) 如何手動(dòng)觸發(fā)KVO ? 首先需要了解KVO...
    hjltony閱讀 574評(píng)論 0 2