1. 程式人生 > >【OC底層】KVO原理

【OC底層】KVO原理

KVO的原理是什麼?底層是如何實現的?

我們可以通過程式碼去探索一下。

建立自定義類:XGPerson

@interface XGPerson : NSObject

@property (nonatomic,assign) int age;

@property (nonatomic,copy) NSString* name;

@end

我們的思路就是看看物件新增KVO之前和之後有什麼變化,是否有區別,程式碼如下:

@interface ViewController ()

@property (strong, nonatomic) XGPerson 
*person1; @property (strong, nonatomic) XGPerson *person2; @end - (void)viewDidLoad { [super viewDidLoad]; self.person1 = [[XGPerson alloc]init]; self.person2 = [[XGPerson alloc]init]; self.person1.age = 1; self.person2.age = 10; // 新增監聽之前,獲取類物件,通過兩種方式分別獲取 p1 和 p2的類物件
NSLog(@"before getClass--->> p1:%@ p2:%@",object_getClass(self.person1),object_getClass(self.person2)); NSLog(@"before class--->> p1:%@ p2:%@",[self.person1 class],[self.person2 class]); // 新增KVO監聽 NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person1 addObserver:self forKeyPath:
@"age" options:option context:nil]; // 新增監聽之後,獲取類物件 NSLog(@"after getClass--->> p1:%@ p2:%@",object_getClass(self.person1),object_getClass(self.person2)); NSLog(@"after class--->> p1:%@ p2:%@",[self.person1 class],[self.person2 class]); }

輸出:

2018-11-02 15:16:13.276167+0800 KVO原理[4083:170379] before getClass--->> p1:XGPerson  p2:XGPerson
2018-11-02 15:16:13.276271+0800 KVO原理[4083:170379] before class--->> p1:XGPerson  p2:XGPerson


2018-11-02 15:16:13.276712+0800 KVO原理[4083:170379] after getClass--->> p1:NSKVONotifying_XGPerson  p2:XGPerson
2018-11-02 15:16:13.276815+0800 KVO原理[4083:170379] after class--->> p1:XGPerson  p2:XGPerson

從上面可以看出,object_getClass 和 class 方式分別獲取到的 類物件竟然不一樣,在物件添加了KVO之後,使用object_getClass的方式獲取到的物件和我們自定義的物件不一樣,而是NSKVONotifying_XGPerson,可以懷疑 class 方法可能被篡改了.

最終發現NSKVONotifying_XGPerson是使用Runtime動態建立的一個類,是XGPerson的子類.

看完物件,接下來我們來看下屬性,就是被我們添加了KVO的屬性age,我們要觸發KVO回撥就是去給age設定個值,那它肯定就是呼叫setAge這個方法.

下面監聽下這個方法在被添加了KVO之後有什麼不一樣.

    NSLog(@"person1新增KVO監聽之前 - %p %p",
              [self.person1 methodForSelector:@selector(setAge:)],
              [self.person2 methodForSelector:@selector(setAge:)]);


    // 新增KVO監聽
    NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil];

    NSLog(@"person1新增KVO監聽之後 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

輸出:

2018-11-02 15:16:13.276402+0800 KVO原理[4083:170379] person1新增KVO監聽之前 - 0x10277c3e0 0x10277c3e0

2018-11-02 15:16:17.031319+0800 KVO原理[4083:170379] person1新增KVO監聽之後 - 0x102b21f8e 0x10277c3e0

看輸出我們能發現,在監聽之前兩個物件的方法所指向的實體地址都是一樣的,新增監聽後,person1物件的setAge方法就變了,這就說明一個問題,這個方法的實現變了,我們再通過Xcode斷點除錯列印看下到底呼叫什麼方法

斷點後,在偵錯程式中使用 po 列印物件

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

  (Foundation`_NSSetIntValueAndNotify)

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

  (KVO原理`-[XGPerson setAge:] at XGPerson.m:13)

通過輸出結果可以發現person1的setAge已經被重寫了,改成了呼叫Foundation框架中C語言寫的 _NSSetIntValueAndNotify 方法,

還有一點,監聽的屬性值型別不同,呼叫的方法也不同,如果是NSString的,就會呼叫 _NSSetObjectValueAndNotify 方法,會有幾種型別

大家都知道蘋果的程式碼是不開源的,所以我們也不知道 _NSSetIntValueAndNotify 這個方法裡面到底呼叫了些什麼,那我們可以試著通過其它的方式去猜一下里面是怎麼呼叫的。

KVO底層的呼叫順序

我們先對我們自定義的類下手,重寫下類裡面的幾個方法:

類實現:

#import "XGPerson.h"

@implementation XGPerson

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

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

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

重寫上面3個方法來監聽我們的值到底是怎麼被改的,KVO的通知回撥又是什麼時候呼叫的

我們先設定KVO的監聽回撥

// KVO監聽回撥
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    NSLog(@"監聽到%@的%@屬性值改變了 - %@", object, keyPath, change[@"new"]);
}

我們直接修改person1的age值,觸發一下KVO,輸出如下:

2018-11-02 15:38:24.788395+0800 KVO原理[4298:186471] willChangeValueForKey
2018-11-02 15:38:24.788573+0800 KVO原理[4298:186471] XGPerson setAge
2018-11-02 15:38:24.788696+0800 KVO原理[4298:186471] didChangeValueForKey - begin
2018-11-02 15:38:24.788893+0800 KVO原理[4298:186471] 監聽到<XGPerson: 0x60400022f420>的age屬性值改變了 - 2
2018-11-02 15:38:24.789014+0800 KVO原理[4298:186471] didChangeValueForKey - end

從結果中可以看出KVO是在哪個時候觸發回撥的,就是在 didChangeValueForKey 這個方法裡面觸發的

NSKVONotifying_XGPerson子類的研究

接下來我們再來研究下之前上面說的那個 NSKVONotifying_XGPerson 子類,可能大家會很好奇這裡面到底有些什麼東西,下面我們就使用runtime將這個子類的所有方法都打印出來

我們先寫一個方法用來列印一個類物件的所有方法,程式碼如下:

// 獲取一個物件的所有方法
- (void)getMehtodsOfClass:(Class)cls{
    
    unsigned int count;
    Method* methods = class_copyMethodList(cls, &count);
    
    NSMutableString* methodList = [[NSMutableString alloc]init];
    for (int i=0; i < count; i++) {
        Method method = methods[i];
        NSString* methodName = NSStringFromSelector(method_getName(method));
        [methodList appendString:[NSString stringWithFormat:@"| %@",methodName]];
    }
    NSLog(@"%@物件-所有方法:%@",cls,methodList);
}

下面使用這個方法列印下person1的所有方法,順便我們再對比下 object_getClass 和 class

    // 一定要使用 object_getClass去獲取類物件,不然獲取到的不是真正的那個子類,而是XGPperson這個類
    [self getMehtodsOfClass:object_getClass(self.person1)];   // 使用 class屬性獲取的類物件
    [self getMehtodsOfClass:[self.person1 class]];

輸出:

2018-11-02 15:45:07.918209+0800 KVO原理[4369:190437] NSKVONotifying_XGPerson物件-所有方法:| setAge:| class| dealloc| _isKVOA
2018-11-02 15:45:07.918371+0800 KVO原理[4369:190437] XGPerson物件-所有方法:| .cxx_destruct| name| willChangeValueForKey:| didChangeValueForKey:| setName:| setAge:| age

通過結果可以看出,這個子類裡面就是重寫了3個父類方法,還有一個私有的方法,我們XGPerson這個類還有一個name屬性,這裡為什麼沒有setName呢?因為我們沒有給 name 屬性新增KVO,所以就不會重寫它,這裡面確實有那個 class 方法,確實被重寫了,所以當我們使用 [self.person1 class] 的方式的時候它內部怎麼返回的就清楚了。

NSKVONotifying_XGPerson 虛擬碼實現

通過上面的研究,我們大概也能清楚NSKVONotifying_XGPerson這個子類裡面是如何實現的了,大概的程式碼如下:

標頭檔案:

@interface NSKVONotifying_XGPerson : XGPerson

@end

實現:

#import "NSKVONotifying_XGPerson.h"

// KVO的原理虛擬碼實現
@implementation NSKVONotifying_XGPerson

- (void)setAge:(int)age{
    
    _NSSetIntValueAndNotify();
}

- (void)_NSSetIntValueAndNotify{
    
    // KVO的呼叫順序
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    // KVO會在didChangeValueForKey裡面呼叫age屬性變更的通知回撥
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key{
    // 通知監聽器,某某屬性值發生了改變
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

// 會重寫class返回父類的class
// 原因:1.為了隱藏這個動態的子類  2.為了讓開發者不那麼迷惑
- (Class)class{
    
    return [XGPerson class];
}

- (void)dealloc{
    
    // 回收工作
}

- (BOOL)_isKVOA{
    
    return YES;
}

總結

KVO是通過runtime機制動態的給要新增KVO監聽的物件建立一個子類,通過這個子類重寫一些父類的方法達到觸發KVO回撥的目的.

補充

KVO是使用了典型的釋出訂閱者設計模式實現事件回撥的功能,多個訂閱者,一個釋出者,簡單的實現如下:

1> 訂閱者向釋出者進行訂閱.

2> 釋出者將訂閱者資訊儲存到一個集合中.

3> 當觸發事件後,釋出者就遍歷這個集合分別呼叫之前的訂閱者,從而達到1對多的通知.

以上已全部完畢,如有什麼不正確的地方大家可以指出~~ ^_^ 下次再見~~