KVO-KVC的原理探究 - KVO篇

關(guān)于KVO的探究

KVO的基本使用

創(chuàng)建Person類,添加屬性age:


@interface Person : NSObject

@property (nonatomic, assign) NSInteger age;

@end

在ViewController中添加屬性@property (nonatomic, strong) Person * person1;

實例化并添加KVO觀察age屬性:


self.person1 = [[Person alloc] init];    

self.person1.age = 1;

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

添加觀察監(jiān)聽回調(diào)并打优涡:


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {

 NSLog(@"被監(jiān)聽的 %@ 的值 %@ 改變?yōu)?%@", object, keyPath, change);

}

此時準(zhǔn)備工作完成涯呻,當(dāng)點擊view時就會修改age的值兄渺,并且回調(diào)打印出監(jiān)聽的結(jié)果鞋屈,這里在ViewController的touchedBegan中修改值:


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

 self.person.age = 11;

}

記得在最后移除鍵值觀察


- (void)dealloc {

 [self.person1 removeObserver:self forKeyPath:@"age"];

}

以上為KVO的基本使用戴卜。

關(guān)于KVO的疑問和分析

再次添加屬性 @property (nonatomic, strong) Person * person2;

實例化person2捣域,在touchedBegan方法中修改值但是不添加KVO:


self.person2 = [[Person alloc] init];

self.person2.age = 2;

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

 self.person.age = 11;

 self.person1.age = 22;

}

點擊view可以看到打印臺的日志為:


2018-07-17 14:09:26.944619+0800 KVO-KVC[36344:935709] 被監(jiān)聽的 <Person: 0x6040000106d0> 的值 age 改變?yōu)?{

 kind = 1;

 new = 11;

 old = 1;

}

這時就可以思考都是修改age屬性值啼染,為什么person1會有回調(diào)而person2沒有,修改的本質(zhì)都是調(diào)用age的set方法焕梅。猜想person1和person2的set方法實現(xiàn)可能不一樣迹鹅,但是實例方法都是存放在class中的,set方法應(yīng)該是一樣的才對贞言,在touchesBegan處打斷點斜棚,然后直接查看person1和person2的isa指針,看看person1和person2的class是否一樣:


(lldb) p self.person1.isa

(Class) $0 = NSKVONotifying_Person

 Fix-it applied, fixed expression was: 

 self.person1->isa

(lldb) p self.person2.isa

(Class) $1 = Person

 Fix-it applied, fixed expression was: 

 self.person2->isa

可以看到person1的class為 NSKVONotifying_Person person2的class為 Person ,isa指針指向的就是instance的class弟蚀,但是為什么person1和person2會不一樣呢蚤霞?我們在添加鍵值觀察之前和之后分別打印person的類型:


NSLog(@"添加前 person1 : %@ person2 : %@", object_getClass(self.person1), object_getClass(self.person2));

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

 [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"添加后 person1 : %@ person2 : %@", object_getClass(self.person1), object_getClass(self.person2));

打印的結(jié)果為


2018-07-17 14:40:59.918227+0800 KVO-KVC[37038:970983] 添加前 person1 : Person person2 : Person

2018-07-17 14:40:59.918636+0800 KVO-KVC[37038:970983] 添加后 person1 : NSKVONotifying_Person person2 : Person

可以看到添加鍵值觀察之后person1的isa指針指向確實被修改了,指向了 NSKVONotifying_Person 類义钉,結(jié)合上面的猜想昧绣,會不會是 NSKVONotifying_Person 這個類重新實現(xiàn)了person1的 setAge: ,否則怎么會和person2不一樣呢捶闸?

我們來驗證一下夜畴,通過 methodForSelector: 來獲取 setAge: 的實現(xiàn):


NSLog(@"添加前 person1 : %p person2 : %p",

 [self.person1 methodForSelector:@selector(setAge:)],

 [self.person2 methodForSelector:@selector(setAge:)]);

NSLog(@"添加后 person1 : %p person2 : %p",

 [self.person1 methodForSelector:@selector(setAge:)],

 [self.person2 methodForSelector:@selector(setAge:)]);

打印的結(jié)果為


2018-07-17 14:46:56.489956+0800 KVO-KVC[37183:978368] 添加前 person1 : 0x102493570 person2 : 0x102493570

2018-07-17 14:46:56.490699+0800 KVO-KVC[37183:978368] 添加后 person1 : 0x1027d9bf4 person2 : 0x102493570

我們知道instance的方法、屬性删壮、協(xié)議等信息都存在與class中贪绘,所以當(dāng)person1和person2調(diào)用 setAge: 時得到的地址應(yīng)該是一樣的,但是在添加鍵值觀察之后person1的調(diào)用方法地址改變了醉锅,為什么會改變呢?讓我們來看看這兩個地址的IMP发绢,在添加鍵值觀察之后斷點硬耍,直接查看兩個地址的IMP:


(lldb) p (IMP)0x100a43570

(IMP) $0 = 0x0000000100a43570 (KVO-KVC -[Person setAge:] at Person.m:13)

(lldb) p (IMP)0x100d89bf4

(IMP) $1 = 0x0000000100d89bf4 (Foundation _NSSetLongLongValueAndNotify)

可以看到添加鍵值觀察之后調(diào)用 setAge: 方法其實就是調(diào)用了 Foundation _NSSetLongLongValueAndNotify

由此可以猜測在添加鍵值觀察之后person1的isa指向了新生成的類 NSKVONotifying_PersonNSKVONotifying_Person 可能繼承自 Person 類边酒,并且重寫了 setAge: 方法经柴,偽代碼如下:


- (void)setAge:(NSInteger)age {

 _NSSetLongLongValueAndNotify();

}

void _NSSetLongLongValueAndNotify() {

 [self willChangeValueForKey:@"age"];

 [super setAge:age];

 [self didChangeValueForKey:@"age"];

}

- (void)didChangeValueForKey:(NSString *)key {

 [observer observeValueForKeyPath:key ofObject:self change:opetions context:nil];

}

綜上我們的猜想KVO的實現(xiàn):instance添加鍵值觀察之后isa指針會被修改為指向 NSKVONotifying_PersonNSKVONotifying_Person 繼承自 Person 并且重寫了 setAge: 方法墩朦,方法實現(xiàn)如上坯认。
在這里就有了那道最經(jīng)典的面試題:如何手動實現(xiàn)KVO,我們只需要在修改值的時候替換 _NSSetLongLongValueAndNotify 方法里面的 [super setAge:age]; 就好了氓涣。

KVO內(nèi)部實現(xiàn)窺探

由上我們猜測出了KVO的實現(xiàn)原理牛哺,下面我們來繼續(xù)探索一下KVO內(nèi)部的實現(xiàn)。

我們分別在添加KVO前后打印person1和person2的class劳吠,這次我們用兩種方式:


NSLog(@"添加前 person1 : %@ -- %@  person2 : %@ -- %@", [self.person1 class], object_getClass(self.person1), [self.person2 class], object_getClass(self.person2));

NSLog(@"添加后 person1 : %@ -- %@  person2 : %@ -- %@", [self.person1 class], object_getClass(self.person1), [self.person2 class], object_getClass(self.person2));

打印出的結(jié)果為:


2018-07-19 11:05:50.553735+0800 KVO-KVC[40616:2560144] 添加前 person1 : Person -- Person  person2 : Person -- Person

2018-07-19 11:05:52.772905+0800 KVO-KVC[40616:2560144] 添加后 person1 : Person -- NSKVONotifying_Person  person2 : Person -- Person

可以看到我們通常用來獲取class的方法在添加前后結(jié)果都是 Person 引润,通過runtime API獲取到的class不相同,怎么回事呢痒玩?我們先來看一下蘋果官方runtime的源碼 這里淳附,當(dāng)然官方的編譯是失敗,要想調(diào)試runtime的請看 這里蠢古。

我們來分析一下源碼:


class方法:

+ (Class)class {

 return self;

}

- (Class)class {

 return object_getClass(self);

}

runtime object_getClass方法:

Class object_getClass(id obj) {

 if (obj) return obj->getIsa();

 else return Nil;

}

class 的類方法或者實例方法最終返回的都是class的self奴曙,而 object_getClass 方法返回的是obj的isa指針,所以通過 object_getClass 獲取的才是當(dāng)前obj的真正class草讶,所以在添加KVO之后person1的isa指針確確實實是被修改了洽糟。

我們再來看一下捕捉到的 NSKVONotifying_Person 到底是個什么鬼?

先來看一下 NSKVONotifying_Person 的meta-class:


NSLog(@"元類對象 person : %@ person1 : %@",

 object_getClass(object_getClass(self.person1)),

 object_getClass(object_getClass(self.person2)));

打印結(jié)果:

2018-07-19 11:39:30.210378+0800 KVO-KVC[41164:2599225] 元類對象 person : NSKVONotifying_Person person1 : Person

NSKVONotifying_Person 的meta-class為 NSKVONotifying_Person

在添加KVO之后打住斷點脊框,借用 DLIntrospection 再來查看一下此時class里面方法都有什么:


(lldb) po [[self.person1 class] instanceMethods]

<__NSArrayI 0x60400023daa0>(

- (void)setAge:(q)arg0 ,

- (q)age

)

(lldb) po [object_getClass(self.person1) instanceMethods]

<__NSArrayI 0x60400025fb30>(

- (void)setAge:(q)arg0 ,

- (class)class,

- (void)dealloc,

- (BOOL)_isKVOA

)

結(jié)果可以看到 NSKVONotifying_Person 重寫了 setAge: 方法颁督,并且還有其他的三個方法,可證上面的猜想確實沒錯浇雹,NSKVONotifying_Person重寫了 setAge: 方法沉御,但是還有一個上面的猜想沒有驗證,那就是 NSKVONotifying_Person 的superClass到底是誰昭灵?

類似isa指針的方式吠裆,我們斷點直接打印:


(lldb) po self.person1.superclass

NSObject

(lldb) po self.person2.superclass

NSObject

咦~~~ 等等烂完,這跟我們猜測的不一樣啊试疙,怎么superclass都是NSObject呢?那我們的猜測是不是都錯了抠蚣?

為了看看superClass里面到底是什么下面我們請出 clang 大神:

clang -rewrite-objc Person.m

可以看出編譯完成后Person類被編譯成了這樣:


struct NSObject_IMPL {

 Class isa;

};

struct Person_IMPL {

 struct NSObject_IMPL NSObject_IVARS;

 NSInteger _age;

};

結(jié)合runtime源碼分析祝旷,Class為 typedef struct objc_class *Class; 類型的結(jié)構(gòu)體,再看下結(jié)構(gòu)體里面的結(jié)構(gòu):


struct objc_object {

private:

 isa_t isa;

 ···

}

struct objc_class : objc_object {

 // Class ISA;

 Class superclass;

 cache_t cache;

 class_data_bits_t bits;

 ···

}

里面確實有superclass嘶窄,仿照runtime的結(jié)構(gòu)我們自己來創(chuàng)建一個類似的結(jié)構(gòu)體:


struct XFPerson_IMPL {

 Class isa;

 Class super_Class;

 NSInteger _age;

};

用我們自己創(chuàng)建的結(jié)構(gòu)體來接收 NSKVONotifying_Person 怀跛,看看他的superclass到底是什么類型:


struct XFPerson_IMPL * xfPerson1 = (__bridge struct XFPerson_IMPL *)(object_getClass(self.person1));

struct XFPerson_IMPL * xfPerson2 = (__bridge struct XFPerson_IMPL *)(object_getClass(self.person2));

NSLog(@"person1--- %@", xfPerson1->super_Class);

NSLog(@"person2--- %@", xfPerson2->super_Class);

打印結(jié)果:

2018-07-19 14:05:38.855549+0800 KVO-KVC[43578:2717734] person1--- Person

2018-07-19 14:05:38.855658+0800 KVO-KVC[43578:2717734] person2--- NSObject

結(jié)果可見是符合我們的猜想的,NSKVONotifying_Person 確實是Person的子類柄冲,但是為什么上面直接打印instance的superclass卻都是NSObject呢吻谋?

回過頭來看一下上面我們找到的 NSKVONotifying_Person 除了 setAge: 還有三個方法,其中就有class方法现横,我們已經(jīng)知道runtime的class的實現(xiàn)漓拾,class返回的就是self,而通過 [self.person1 class] 得到的是 Person 戒祠,這就證明了 NSKVONotifying_Person 重寫了class方法骇两,并且返回的是 Person 類,通過源碼查看runtime的superclass方法的實現(xiàn):


+ (Class)superclass {

 return self->superclass;

}

- (Class)superclass {

 return [self class]->superclass;

}

就是先通過class方法找到class姜盈,然后在根據(jù)class找到superclass脯颜,所以前面直接通過 self.person1.superclass 找到的是 Person,因為此時的class方法返回已經(jīng)被修改了贩据。

蘋果大大可能是因為整個事件中 NSKVONotifying_Person 是個人畜無害的東西栋操,對于開發(fā)者使用KVO是可以不用知道的,所以用這種方式來騙騙開發(fā)者饱亮,真不容易矾芙,還好最近看 白夜追兇 看的整個人都比較有耐心了就是要找到真相,哈(不)哈(要)哈(臉)??近上。

再看看看其他的兩個方法剔宪,dealloc 方法可能就是做一些銷毀現(xiàn)場的事情,畢竟中間動態(tài)創(chuàng)建了 NSKVONotifying_Person ,不用了一定要銷毀葱绒,而 _isKVOA 返回的一定是 YES 感帅,表示當(dāng)前確實是在用KVO,到此關(guān)于KVO的黑科技已經(jīng)探究明白了地淀,好了失球,打完收工,接著去看兩集 白夜追兇帮毁, 哈哈哈实苞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市烈疚,隨后出現(xiàn)的幾起案子黔牵,更是在濱河造成了極大的恐慌,老刑警劉巖爷肝,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件猾浦,死亡現(xiàn)場離奇詭異,居然都是意外死亡灯抛,警方通過查閱死者的電腦和手機(jī)金赦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牧愁,“玉大人素邪,你說我怎么就攤上這事外莲≈戆耄” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵偷线,是天一觀的道長磨确。 經(jīng)常有香客問我,道長声邦,這世上最難降的妖魔是什么乏奥? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮亥曹,結(jié)果婚禮上邓了,老公的妹妹穿的比我還像新娘。我一直安慰自己媳瞪,他們只是感情好骗炉,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛇受,像睡著了一般句葵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天乍丈,我揣著相機(jī)與錄音剂碴,去河邊找鬼。 笑死轻专,一個胖子當(dāng)著我的面吹牛忆矛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播铭若,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼洪碳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了叼屠?” 一聲冷哼從身側(cè)響起瞳腌,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎镜雨,沒想到半個月后嫂侍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡荚坞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年挑宠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颓影。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡各淀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诡挂,到底是詐尸還是另有隱情碎浇,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布璃俗,位于F島的核電站奴璃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏城豁。R本人自食惡果不足惜苟穆,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望唱星。 院中可真熱鬧雳旅,春花似錦、人聲如沸间聊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽甸饱。三九已至沦童,卻和暖如春仑濒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背偷遗。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工墩瞳, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人氏豌。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓喉酌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親泵喘。 傳聞我的和親對象是個殘疾皇子泪电,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

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

  • 面試問題: iOS用什么方式實現(xiàn)對一個對象的KVO? 如何手動觸發(fā)KVO纪铺? KVO簡介 KVO就是鍵值觀測相速。有時候...
    雪山飛狐_91ae閱讀 4,564評論 10 36
  • 問題 iOS用什么方式實現(xiàn)對一個對象的KVO?(KVO的本質(zhì)是什么鲜锚?) 如何手動觸發(fā)KVO 突诬? 首先需要了解KVO...
    hjltony閱讀 574評論 0 2
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,320評論 8 265
  • 1、KVO的基本使用 定義:KVO的全稱是Key-Value-Observing芜繁,俗稱“鍵值監(jiān)聽”旺隙,可以用于監(jiān)聽某...
    Jerky_Guo閱讀 1,720評論 0 5
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,089評論 1 32