iOS底層原理總結 - 探尋KVO本質

本篇主要是對小碼哥底層視頻學習的總結。方便日后復習。
上一篇《iOS底層原理總結 - 探尋Class的本質》:
http://www.lxweimin.com/p/748bc1d63184

本篇學習總結:

  • 探尋KVO本質
  • KVO底層方法調用順序
  • 代碼求證KVO實現機制
  • 探尋KVC本質
  • KVC賦值和查找順序

好了,帶著問題,我們一一開始閱讀吧 ??

一.探尋KVO本質

1.面試題:iOS用什么方式實現對一個對象的KVO?(KVO的本質是什么?)

看到這個面試題,我們就從頭開始說一下什么是KVO,KVO的全稱 Key-Value Observing,俗稱“鍵值監聽”,可以用于監聽某個對象屬性值的改變。
先來一段代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    self.person2 = [[MJPerson alloc]init];
    self.person2.age = 2;
    // 給person1對象添加KVO監聽
    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 = 10;
    self.person2.age = 20;
    
}
- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
 
}
// 當監聽對象的屬性值發生改變時,就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
//打印結果如下:
監聽到<MJPerson: 0x2830605c0>的age屬性值改變了 - {
    kind = 1;
    new = 10;//新賦值數據
    old = 1;//舊賦值數據 NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
} - 123 //添加監聽時的上下文

上述代碼可以看出,p1添加observer后,一旦age屬性值發生改變,會立刻通知observer,執行observerobserveValueForKeyPath方法。

我們嘗試探尋KVO底層實現原理,我們通過斷點方式探尋。
KVO底層原理分析

p1和p2的age屬性值發生改變,自然會調用-setAge方法,我們把斷點打在-setAge方法這里,斷點確實走到了這里,我們發現p1對象和p2對象調用同樣的setter方法,但是我們發現p1除了調用setter方法之外還會另外執行observerobserveValueForKeyPath方法,說明KVO在運行時獲取對p1對象做了一些改變,使得p1對象在調用setage方法的時候可能做了一些額外的操作,所以問題出在對象身上,兩個對象在內存中肯定不一樣,兩個對象可能本質上并不一樣。接下來來探索KVO內部是怎么實現的。

KVO底層原理實現
我們分別將斷點打到添加監聽之前跟之后,

監聽之前isa指針.png

監聽之后isa指針.png

通過上面倆圖我們會發現,p1對象執行addObserver操作之后,p1實例對象的isa指針指向NSKVONotifyin_Person類對象,而p2實例對象的isa指針還是指向MJPerson類對象。
那么我們先來觀察p2對象在內存中時如何存儲的,然后對比p2觀察p1

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

首先我們知道,p2實例對象在調用-setAge方法的時候,會通過p2實例對象中的isa指針找到MJPerson類對象,然后去類對象的方法列表中查找-setAge方法,進而找到方法對應的實現,如下圖所示:

p2實例對象調用setage方法路徑.png

但是剛才我們發現p1對象添加監聽者之后,isa指針指向了NSKVONotifyin_Person類對象,NSKVONotifyin_Person類對象其實是MJPerson的類對象的子類,NSKVONotifyin_Person類對象的supercalss指針指向MJPerson的類對象。NSKVONotifyin_Person類對象是運行時通過runtime生成的,那么p1實例對象調用setage方法,肯定是通過p1實例對象中的isa指針找到NSKVONotifyin_Person類對象,在NSKVONotifyin_Person類對象找到setage方法及實現。

小碼哥將NSKVONotifyin_Person類對象中的setage方法實現邏輯告訴我們了:

在調用NSKVONotifyin_Person類對象中的setage方法,其實是調用了Foundation框架中C語言函數_NSsetIntValueAndNotify,_NSsetIntValueAndNotify方法內部的邏輯,首先是調用willChangeValueForKey告訴將要改變某一個屬性的值,然后調用父類的setage方法對成員變量賦值,最后調用didChangeValueForKey告訴已經改變了某一個屬性的值,didChangeValueForKey中調用監聽器的監聽方法,最終來到監聽者的observeValueForKeyPath方法中。

到此,整個KVO調用底層流程就搞清楚了,我們需要用代碼求證一下步驟

二.代碼求證KVO實現流程

1.給p1添加observer后,在runtime時期動態創建MJPerson類對象的子類即NSKVONotifyin_Person類對象,修改p1的isa指針指向為NSKVONotifyin_Person類對象,前面已經有圖印證了這點。
2.為了驗證p1添加observer前后調用-setAge方法后實現有什么區別,我們通過打印方法實現的地址來看一下p1和p2的-setAge的方法實現的地址在添加KVO前后有什么變化。

監聽之前的IMP地址.png

監聽之后的IMP地址.png

我們只是看實現的內存地址,好像并不能看出什么,前面不是說p1對象添加observer后,調用-setAge方法的時候,其實調用_NSsetIntValueAndNotify方法了嗎,方法名怎么看呢?我們在終端打印一下方法實現,如下圖:
終端打印方法實現.png

這一步也印證了咱們前面總結的
Foundation框架中會根據屬性的類型,調用不同的方法。例如我們之前定義的int類型的age屬性,那么我們看到Foundation框架中調用的_NSsetIntValueAndNotify函數。那么我們把age的屬性類型變為double重新打印一遍
image.png

補充一點Founddation框架知識:
我們發現調用的函數變為了_NSSetDoubleValueAndNotify,那么這說明Foundation框架中有許多此類型的函數,通過屬性的不同類型調用不同的函數。
那么我們可以推測Foundation框架中還有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函數。
我們可以找到Foundation框架文件,通過命令行查詢關鍵字找到相關函數

相關函數.png

用一張圖來總結一下吧:


NSKVONotifying_MJPerson偽代碼.png
三.NSKVONotifyin_Person內部結構是怎樣的?

首先我們知道,NSKVONotifyin_Person作為MJPerson 類對象的子類,其superclass指針指向MJPerson類對象,并且NSKVONotifyin_Person內部一定對-setAge方法做了單獨的實現,那么NSKVONotifyin_PersonMJPerson* 類的差別可能就在于其內存儲的對象方法及實現不同。
我們通過runtime分別打印MJPerson* 類對象和NSKVONotifyin_Person類對象內存儲的對象方法。
先上代碼:

//MJPerson類
@interface MJPerson : NSObject
@property (assign, nonatomic) int age;
@end


//ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    Person *p1 = [[Person alloc] init];
    p1.age = 1.0;
    Person *p2 = [[Person alloc] init];
    p1.age = 2.0;
    // self 監聽 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);
}

打印結果如下圖:


runtime 打印結果.png

上述兩個類對象方法列表不一樣,我們直接看下圖:


NSKVONotifyin_Person內存結構以及方法調用順序.png

這里有兩點要說明,一是-setAge方法內部調用Foundation框架中的_NSSetIntValueAndNotify方法,這點前面已經講完了,二是重寫class方法,NSKVONotifyin_Person重寫class方法是為了隱藏NSKVONotifyin_Person。不被外界所看到。我們在p1添加過KVO監聽之后,分別打印p1和p2對象的class可以發現他們都返回Person

NSLog(@"p1 = %@,p2 = %@",[self.person1 class],[self.person2 class]);
//打印結果如下:
 p1 = MJPerson,p2 = MJPerson

如果NSKVONotifyin_Person不重寫class方法,那么當對象要調用class對象方法的時候就會一直向上找來到NSObject,而NSObject的class的實現大致為返回自己isa指向的類,返回p1的isa指向的類那么打印出來的類就是NSKVONotifyin_Person,但是apple不希望將NSKVONotifyin_Person類暴露出來,并且不希望我們知道NSKVONotifyin_Person內部實現,所以在內部重寫了class類,直接返回Person類,所以外界在調用p1的class對象方法時,是Person類。這樣p1給外界的感覺p1還是Person類,并不知道NSKVONotifyin_Person子類的存在

那么我們可以猜測NSKVONotifyin_Person內重寫的class內部實現大致為

- (Class) class {
     // 得到類對象,在找到類對象父類
     return class_getSuperclass(object_getClass(self));
}

驗證didChangeValueForKey:內部會調用observer的observeValueForKeyPath:ofObject:change:context:方法
我們在Person類中重寫willChangeValueForKey:和didChangeValueForKey:方法,模擬他們的實現

- (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");
}
調用順序.png
四.探尋KVC本質

先說一下KVC:Key Value Coding(鍵值編碼),獲取或者給對象的某個屬性進行賦值,常用方法如下:

- (nullable id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

key 跟keypath的區別就是:key 是單純當前調用對象的某個特定屬性名字,keypath既可以是某個特定屬性名字,也可以是某個路徑下查找的名字

五.KVC賦值和查找順序
KVC賦值順序.png
KVC取值順序.png

總結一下本篇面試題:

  • 1.iOS 用什么方式實現對一個對象的KVO?(KVO本質是什么?)

答:當一個對象使用了KVO監聽,iOS系統會修改這個對象的isa指針,改為指向一個全新的通過runtime動態創建的子類,子類擁有自己的setter方法實現,setter方法實現內部會調用Foundation框架的_NSSetIntValueAndNotify方法,在_NSSetIntValueAndNotify 方法里面調用willChangeValueForKey方法,然后調用父類的setter方法對成員變量賦值,最后調用didChangeValueForKey方法,最終通知監聽者調用observeValueForKeyPath 方法,
這里我們要特別說明一點:單純給成員變量賦值,沒有調用willChangeValueForKey和didChangeValueForKey方法,最終也不會調用observeValueForKeyPath方法

  • 2.面試題:手動能否觸發實現一個KVO?

答案是可以的,只要知道了KVO底層調用順序,就可以寫出來,以代碼為例說明:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    // 給person1對象添加KVO監聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
    [self.person1 willChangeValueForKey:@"age"];
    self.person1.age = 10000;
    [self.person1 didChangeValueForKey:@"age"];
    
}

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

- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

// observeValueForKeyPath:ofObject:change:context:
// 當監聽對象的屬性值發生改變時,就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
//打印結果如下:
監聽到<MJPerson: 0x280a4c890>的age屬性值改變了 - {
    kind = 1;
    new = 10000;
    old = 1;
} - (null)

  • 3.面試題:KVC改變屬性值回引起KVO變化嗎?

答:可以的,直接來看代碼及打印吧

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    // 給person1對象添加KVO監聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
//    [self.person1 willChangeValueForKey:@"age"];
//    [self.person1 setValue:@(101) forKey:@"age"];
//    [self.person1 didChangeValueForKey:@"age"];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.person1 setValue:@(102) forKey:@"age"];
}

- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

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

本篇學習先記錄到此,感謝閱讀,如有錯誤,不吝賜教。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。