03 iOS底層原理 - KVO本質(zhì)探究

廢話不多說先來幾個面試題:

一,iOS用什么方式實現(xiàn)對一個對象的KVO?(KVO的本質(zhì)是什么?)
二,如何手動觸發(fā)KVO
三,直接修改成員變量會觸發(fā)KVO嗎?

通過挖掘KVO的本質(zhì),就會發(fā)現(xiàn),這幾個面試題就跟切菜一樣

那么什么是KVO呢?

什么是KVO呢?
KVO的全稱是Key-Value-Observing,即“鍵值監(jiān)聽”,可以用于監(jiān)聽某個對象屬性值的變化

用一張簡單的圖就可以表示:

image.png

這就是KVO最簡單的是使用,那么它到底是怎么實現(xiàn)的呢,下面我們一步步解開這個KVO底層的神秘面紗。

一,代碼準(zhǔn)備

1. 創(chuàng)建一個Person 類 繼承NSObject
// 聲明
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end

// 實現(xiàn)
@implementation Person
- (void)setAge:(int)age {
    _age = age;
    NSLog(@"哥們 == setAge");
}

- (void)willChangeValueForKey:(NSString *)key {
    [super willChangeValueForKey:key];
    NSLog(@"哥們 == willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"哥們 == didChangeValueForKey-begin");
    [super didChangeValueForKey:key];
    NSLog(@"哥們 == didChangeValueForKey-end");
}
@end
2. ViewController代碼
// 導(dǎo)入頭文件
#import <objc/runtime.h>
#import "Person.h"

// 聲明person對象
@interface ViewController ()
@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    person1 = [[Person alloc]init];
    person1 = 18;

    self.person2 = [[Person alloc]init];
    self.person2.age = 28;
    // 給person1添加一個KVO
    [person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];
}

// 當(dāng)監(jiān)聽對象的屬性值發(fā)生改變是,就會調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"監(jiān)聽到%@的%@屬性值改變了 - %@ - %@", 
object, keyPath, change, context);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person1.age = 20;
}

二,開始研究KVO本質(zhì)

點擊屏幕后,會觸發(fā)observeValueForKeyPath這監(jiān)聽事件
// 打印結(jié)果

監(jiān)聽到<Person: 0x600002e2c510>的age屬性值改變了 - {
         kind = 1;
         new = 20;
         old = 18;
 }

發(fā)現(xiàn)age的值確實發(fā)生了變化

1. 那么age值的改變是否與setAge:方法有關(guān)

在touchesBegan給person2重新賦值

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    self.person1.age = 21;
//    self.person2.age = 22;
//  上面兩句代碼,其實就是調(diào)用Person的 set 方法,都在調(diào)用同一個方法
    [self.person1 setAge:21];
    [self.person2 setAge:22];
}

當(dāng)觸發(fā)touchesBegan事件時,發(fā)現(xiàn)調(diào)用 [self.person1 setAge:21];后會觸發(fā)observe監(jiān)聽器,而調(diào)用 [self.person2 setAge:22];后不會觸發(fā)observe監(jiān)聽器。
既然都是在調(diào)用 setAge方法,但是為啥只有person1的age發(fā)生改變時,才會給observe發(fā)通知呢?而person2的age發(fā)生改變時,不會通知observe?
先看兩張圖:

image.png
image.png
從上面兩張圖,可以看出來:     
其實本質(zhì)上就是兩個實例對象的isa指向不一樣
現(xiàn)在看下兩個實例的isa有什么不同
lldb結(jié)果:
self.person1.isa -> NSKVONotifying_Person
self.person2.isa -> Person
差異:
1. 添加了KVO監(jiān)聽的實例person1,通過isa指向的class是NSKVONotifying_Person
2. 沒有添加KVO監(jiān)聽的實例person2,通過isa指向的class是Person
那么,NSKVONotifying_Person這個類是怎么產(chǎn)生的呢?
本質(zhì)上是利用Runtime動態(tài)創(chuàng)建的一個類(可以利用truntime機制自己實現(xiàn)KVO監(jiān)聽)

結(jié)論:由此可見,age值的改變與setAge:方法沒有關(guān)系,而是因為派生出了新的類,此時的setAge:方法的實現(xiàn)也就不一樣了,具體有啥不易樣的,接著往下看哈。

2. person1添加KVO前后person1和person2對象的變化

驗證person1在添加監(jiān)聽前后,person1實例的isa指向的【class】和具體的【對象方法】實現(xiàn)到底發(fā)生了哪些變化

  1. 利用runtime的 object_getClass() 查看person1添加監(jiān)聽前后的,person1和person2的isa指向類對象;
  2. 利用runtime的 object_getClass() 查看person1添加監(jiān)聽前后的,person1和person2的isa指向類對象的地址;
  3. 利用runtime的 methodForSelector這個方法來獲取setAge:這個實例方法的具體實現(xiàn)
    根據(jù)以上3點,添加一些打印信息:
    NSLog(@"person1添加監(jiān)聽之前類對象:%@, %@",
          object_getClass(self.person1),
          object_getClass(self.person2));
    NSLog(@"person1添加監(jiān)聽之前類對象地址:%p, %p",
          object_getClass(self.person1),
          object_getClass(self.person2));
    NSLog(@"person1添加監(jiān)聽之前類的實例方法實現(xiàn):%p, %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

    // 只給person1添加一個KVO
    NSKeyValueObservingOptions options =
    NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age"
                      options:options
                      context:@"123"];
    
    NSLog(@"person1添加監(jiān)聽之后類對象:%@, %@",
          object_getClass(self.person1),
          object_getClass(self.person2));
    NSLog(@"person1添加監(jiān)聽之后類對象地址:%p, %p",
          object_getClass(self.person1),
          object_getClass(self.person2));
    NSLog(@"person1添加監(jiān)聽之后類的實例方法實現(xiàn):%p, %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

分析打印信息:
打印結(jié)果:

person1添加監(jiān)聽之前類對象:Person, Person
person1添加監(jiān)聽之后類對象:NSKVONotifying_Person, Person

person1添加監(jiān)聽之前類對象地址:0x1089bd708, 0x1089bd708
person1添加監(jiān)聽之后類對象地址:0x6000021d87e0, 0x1089bd708

結(jié)論:
由此可見,person1在添加監(jiān)聽后,isa所指向的類確實改變了,那么setAge:這個對象方法到底是怎么實現(xiàn)的呢,繼續(xù)分析第三個打印結(jié)果

打印結(jié)果:

person1添加監(jiān)聽之前類的實例方法實現(xiàn):0x1061e1ed0, 0x1061e1ed0
person1添加監(jiān)聽之后類的實例方法實現(xiàn):0x7fff257223da, 0x1061e1ed0

利用lldb查看
(lldb) p (IMP)0x1061e1ed0
(IMP) $0 = 0x00000001061e1ed0 (06-KVO初探`-[Person setAge:] at Person.m:13)
(lldb) p (IMP)0x7fff257223da
(IMP) $1 = 0x00007fff257223da (Foundation`_NSSetIntValueAndNotify)

結(jié)論:
由此可見,person1添加監(jiān)聽后,setAge:的實現(xiàn)的確改變了,是在Foundation框架下的_NSSetIntValueAndNotify(c語言方法)實現(xiàn)的,_NSSetIntValueAndNotify具體實現(xiàn)見自己實現(xiàn)的NSKVONotifying_Person 類。

下面可以看下_NSSetIntValueAndNotify實現(xiàn)的偽代碼

// 聲明一個添加了KVO的派生類 NSKVONotifying_Person
@interface NSKVONotifying_Person : Person
@end

// 實現(xiàn)
@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)聽器,某某屬性發(fā)生了改變
    [observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
3. 查看person1添加KVO后,都有哪些調(diào)用

在Person類里,實現(xiàn)兩個父類的方法,并且給setAge加上打印信息

- (void)setAge:(int)age {
    _age = age;
    NSLog(@"哥們 == setAge");
}

- (void)willChangeValueForKey:(NSString *)key {
    [super willChangeValueForKey:key];
    NSLog(@"哥們 == willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"哥們 == didChangeValueForKey-begin");
    [super didChangeValueForKey:key];
    NSLog(@"哥們 == didChangeValueForKey-end");
}

觸發(fā)touchesBegan方法

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.person1 setAge:21];
//    [self.person2 setAge:22];
}

打印結(jié)果是:

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

打印結(jié)果分析:添加了KVO的對象,屬性改變的話,都有下面一些方法調(diào)用

當(dāng)修改instance對象的屬性時,先調(diào)用setter方法,然后實現(xiàn)Foundation中的_NSSet****ValueAndotify函數(shù)
a> willChangeValueForKey:
b> 父類原來的setter
c> didChangeValueForKey:
內(nèi)部會觸發(fā)監(jiān)聽器(Observe)的監(jiān)聽方法(observeValueForKeyPath:ofObject:change:context:)

如果person2調(diào)用了setAge:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // [self.person1 setAge:21];
    [self.person2 setAge:22];
}

打印結(jié)果只有一個

哥們 == setAge

說明:沒有添加監(jiān)聽的對象,willChangeValueForKey和didChangeValueForKey方法不會調(diào)用

4. 查看NSKVONotifying_Person類的元類

驗證一下,添加KVO后,新生成的NSKVONotifying_Person類對象的isa指向的元類對象及對象地址是什么

利用runtime的 Class object_getClass(id obj) 來查看對象地址和對象名
1> 傳入的obj可能是instance對象、class對象、meta-class對象
2> 返回值
a) 如果是instance對象,返回class對象
b) 如果是class對象,返回meta-class對象
c) 如果是meta-class對象,返回NSObject(基類)的meta-class對象

為了驗證在添加KVO后添加一些打印信息

NSLog(@"類對象地址:%p, %p",
// 拿到person1.isa,相當(dāng)于Person類地址(isa & ISA_MASK)
          object_getClass(self.person1), 
// 拿到person2.isa
          object_getClass(self.person2)); 

NSLog(@"元類對象地址:%p, %p",
// 拿到person1.isa.isa
          object_getClass(object_getClass(self.person1)), 
// 拿到person2.isa.isa
          object_getClass(object_getClass(self.person2))); 
    
NSLog(@"類對象:%@, %@",
// 拿到person1的isa指向的類
          object_getClass(self.person1), 
// 拿到person2的isa指向的類
          object_getClass(self.person2)); 

NSLog(@"元類對象:%@, %@",
 // 拿到person1的isa指向的類的元類
          object_getClass(object_getClass(self.person1)),
 // 拿到person2的isa指向的類的元類
          object_getClass(object_getClass(self.person2)));

打印信息分析:

類對象地址:0x600001284120, 0x1081ea710
元類對象地址:0x6000012841b0, 0x1081ea6e8

類對象:NSKVONotifying_Person, Person
元類對象:NSKVONotifying_Person, Person

結(jié)論:
從打印結(jié)果來看,派生出來的NSKVONotifying_Person類的元類就是它本身

三, 派生出來的NSKVONotifying_Person都有哪些方法

先看一張示例圖,紅框狂起來的部分

image.png
1. 利用runtime來獲取方法實現(xiàn)
Class cls = object_getClass(self.person1);
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    // 遍歷方法
    for (int i = 0; i < count; i++) {
        // 獲得方法
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"methodName == %@", methodName);
    }

打印結(jié)果:

setAge:
class
dealloc
_isKVOA

所以派生出來的NSKVONotifying_Person類里面,確實存在setAge:/class/dealloc/_isKVOA這些方法

2. 研究下為啥要重寫class方法呢?

添加兩個打印信息

NSLog(@"類對象:%@, %@", object_getClass(self.person1), object_getClass(self.person2));
NSLog(@"類對象:%@, %@", [self.person1 class], [self.person2 class]);

上面打印信息是利用兩種方法來獲取person1和person2的類對象
打印結(jié)果:

類對象:NSKVONotifying_Person, Person
類對象:Person, Person

結(jié)果分析:
只有通過runtime的object_getClass這中方式,取出來的才是person1的isa指向的類對象,
而通過self.person1 class]取出來的,不一定是person1的isa指向的類對象

為什么要重寫class呢?
猜測:系統(tǒng)為了直接返回當(dāng)前類,屏蔽內(nèi)部實現(xiàn),

如果不重寫,person1找到isa,通過isa找到對應(yīng)的類對象,在這個類里面發(fā)現(xiàn)沒有-class的這個方法,那么就通過superclass找到父類,
如果父類還沒有,就通過superclass一直找到基類,基類(NSObject)里面就會通過object_getClass(sef)返回當(dāng)前的類,即Person

四,回答面試題

一,iOS用什么方式實現(xiàn)對一個對象的KVO?(KVO的本質(zhì)是什么?)
 1. 利用runtimeAPI動態(tài)生成一個子類,并且讓instance的isa指向一個全新的子類
 2. 當(dāng)修改instance對象的屬性時,現(xiàn)調(diào)用setter方法,然后實現(xiàn)Foundation中的_NSSet****ValueAndotify函數(shù)
 a> willChangeValueForKey:
 b> 父類原來的setter
 c> didChangeValueForKey:
    內(nèi)部會觸發(fā)監(jiān)聽器(Observe)的監(jiān)聽方法(observeValueForKeyPath:ofObject:change:context:)
 
 二,如何手動觸發(fā)KVO
 意思就是不通過改變屬性的值,怎么觸發(fā)KVO
 手動調(diào)用willChangeValueForKey:和didChangeValueForKey:
 
 三,直接修改成員變量會觸發(fā)KVO嗎?
 不會觸發(fā)KVO,因為只有通過setter方法才能觸發(fā),而成員變量不會調(diào)用setter方法
 要想通過修改成員變量來觸發(fā)KVO,也很簡單
 手動依次調(diào)用:
 1.willChangeValueForKey:
 2.修改成員變量
 3.didChangeValueForKey:
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,006評論 2 374

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