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

對小碼哥底層班視頻學習的總結與記錄。面試題部分,經過對面試題的分析探索問題的本質內容。面試

問題bash

  1. iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼?)
  2. 如何手動觸發KVO

首先須要瞭解KVO基本使用,KVO的全稱 Key-Value Observing,俗稱「鍵值監聽」,能夠用於監聽某個對象屬性值的改變。markdown

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p1 = [[Person alloc] init];
    Person *p2 = [[Person alloc] init];
    p1.age = 1;
    p1.age = 2;
    p2.age = 2;
    // self 監聽 p1的 age屬性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    p1.age = 10;
    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監聽到%@的%@改變了%@", object, keyPath,change);
}

// 打印內容
監聽到<Person: 0x604000205460>的age改變了{
    kind = 1;
    new = 10;
    old = 2;
}
複製代碼

上述代碼中能夠看出,在添加監聽以後,age屬性的值在發生改變時,就會通知到監聽者,執行監聽者的observeValueForKeyPath方法。app

探尋KVO底層實現原理

經過上述代碼咱們發現,一旦age屬性的值發生改變時,就會通知到監聽者,而且咱們知道賦值操做都是調用 set方法,咱們能夠來到Person類中重寫age的set方法,觀察是不是KVO在set方法內部作了一些操做來通知監聽者。框架

咱們發現即便重寫了set方法,p1對象和p2對象調用一樣的set方法,可是咱們發現p1除了調用set方法以外還會另外執行監聽器的observeValueForKeyPath方法。函數

說明KVO在運行時獲取對p1對象作了一些改變。至關於在程序運行過程當中,對p1對象作了一些變化,使得p1對象在調用setage方法的時候可能作了一些額外的操做,因此問題出在對象身上,兩個對象在內存中確定不同,兩個對象可能本質上並不同。接下來來探索KVO內部是怎麼實現的。學習

KVO底層實現分析

首先咱們對上述代碼中添加監聽的地方打斷點,看觀察一下,addObserver方法對p1對象作了什麼處理?也就是說p1對象在通過addObserver方法以後發生了什麼改變,咱們經過打印isa指針以下圖所示spa

addObserver對p1對象的處理

經過上圖咱們發現,p1對象執行過addObserver操做以後,p1對象的isa指針由以前的指向類對象Person變爲指向NSKVONotifyin_Person類對象,而p2對象沒有任何改變。也就是說一旦p1對象添加了KVO監聽之後,其isa指針就會發生變化,所以set方法的執行效果就不同了。命令行

那麼咱們先來觀察p2對象在內容中是如何存儲的,而後對比p2來觀察p1。 首先咱們知道,p2在調用setage方法的時候,首先會經過p2對象中的isa指針找到Person類對象,而後在類對象中找到setage方法。而後找到方法對應的實現。以下圖所示3d

未使用KVO監聽的對象放大實現路徑

可是剛纔咱們發現p1對象的isa指針在通過KVO監聽以後已經指向了NSKVONotifyin_Person類對象,NSKVONotifyin_Person實際上是Person的子類,那麼也就是說其superclass指針是指向Person類對象的,NSKVONotifyin_Person是runtime在運行時生成的。那麼p1對象在調用setage方法的時候,確定會根據p1的isa找到NSKVONotifyin_Person,在NSKVONotifyin_Person中找setage的方法及實現。

通過查閱資料咱們能夠了解到。 NSKVONotifyin_Person中的setage方法中其實調用了 Fundation框架中C語言函數 _NSsetIntValueAndNotify,_NSsetIntValueAndNotify內部作的操做至關於,首先調用willChangeValueForKey 將要改變方法,以後調用父類的setage方法對成員變量賦值,最後調用didChangeValueForKey已經改變方法。didChangeValueForKey中會調用監聽器的監聽方法,最終來到監聽者的observeValueForKeyPath方法中。

那麼如何驗證KVO真的如上面所講的方式實現?

首先通過以前打斷點打印isa指針,咱們已經驗證了,在執行添加監聽的方法時,會將isa指針指向一個經過runtime建立的Person的子類NSKVONotifyin_Person。 另外咱們能夠經過打印方法實現的地址來看一下p1和p2的setage的方法實現的地址在添加KVO先後有什麼變化。

// 經過methodForSelector找到方法實現的地址
NSLog(@"添加KVO監聽以前 - 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監聽以後 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
複製代碼

setage的方法實現的地址在添加KVO先後的變化

咱們發如今添加KVO監聽以前,p1和p2的setAge方法實現的地址相同,而通過KVO監聽以後,p1的setAge方法實現的地址發生了變化,咱們經過打印方法實現來看一下先後的變化發現,確實如咱們上面所講的同樣,p1的setAge方法的實現由Person類方法中的setAge方法轉換爲了C語言的Foundation框架的_NSsetIntValueAndNotify函數。

Foundation框架中會根據屬性的類型,調用不一樣的方法。例如咱們以前定義的int類型的age屬性,那麼咱們看到Foundation框架中調用的_NSsetIntValueAndNotify函數。那麼咱們把age的屬性類型變爲double從新打印一遍

_NSSetDoubleValueAndNotify函數
咱們發現調用的函數變爲了_NSSet DoubleValueAndNotify,那麼這說明Foundation框架中有許多此類型的函數,經過屬性的不一樣類型調用不一樣的函數。 那麼咱們能夠推測Foundation框架中還有不少例如 _NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函數。

咱們能夠找到Foundation框架文件,經過命令行查詢關鍵字找到相關函數

相關函數

NSKVONotifyin_Person內部結構是怎樣的?

首先咱們知道,NSKVONotifyin_Person做爲Person的子類,其superclass指針指向Person類,而且NSKVONotifyin_Person內部必定對setAge方法作了單獨的實現,那麼NSKVONotifyin_Person同Person類的差異可能就在於其內存儲的對象方法及實現不一樣。 咱們經過runtime分別打印Person類對象和NSKVONotifyin_Person類對象內存儲的對象方法

- (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);
}
複製代碼

上述打印內容以下

NSKVONotifyin_Person內存儲的對象方法

經過上述代碼咱們發現NSKVONotifyin_Person中有4個對象方法。分別爲setAge: class dealloc _isKVOA,那麼至此咱們能夠畫出NSKVONotifyin_Person的內存結構以及方法調用順序。

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

這裏NSKVONotifyin_Person重寫class方法是爲了隱藏NSKVONotifyin_Person。不被外界所看到。咱們在p1添加過KVO監聽以後,分別打印p1和p2對象的class能夠發現他們都返回Person。

NSLog(@"%@,%@",[p1 class],[p2 class]);
// 打印結果 Person,Person
複製代碼

若是NSKVONotifyin_Person不重寫class方法,那麼當對象要調用class對象方法的時候就會一直向上找來到nsobject,而nsobect的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");
}
複製代碼

再次運行來查看didChangeValueForKey的方法內運行過程,經過打印內容能夠看到,確實在didChangeValueForKey方法內部已經調用了observer的observeValueForKeyPath:ofObject:change:context:方法。

didChangeValueForKey內運行順序

回答問題:

  1. iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼?) 答. 當一個對象使用了KVO監聽,iOS系統會修改這個對象的isa指針,改成指向一個全新的經過Runtime動態建立的子類,子類擁有本身的set方法實現,set方法實現內部會順序調用willChangeValueForKey方法、原來的setter方法實現、didChangeValueForKey方法,而didChangeValueForKey方法內部又會調用監聽器的observeValueForKeyPath:ofObject:change:context:監聽方法。
  1. 如何手動觸發KVO 答. 被監聽的屬性的值被修改時,就會自動觸發KVO。若是想要手動觸發KVO,則須要咱們本身調用willChangeValueForKey和didChangeValueForKey方法便可在不改變屬性值的狀況下手動觸發KVO,而且這兩個方法缺一不可。

經過如下代碼能夠驗證

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"];
複製代碼

打印內容

經過打印咱們能夠發現,didChangeValueForKey方法內部成功調用了observeValueForKeyPath:ofObject:change:context:,而且age的值並無發生改變。


文中若是有不對的地方歡迎指出。我是xx_cc,一隻長大好久但尚未二夠的傢伙。

相關文章
相關標籤/搜索