【OC底層】KVO原理

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

KVO是Key-value observing的縮寫。設計模式

KVO是Objective-C是使用觀察者設計模式實現的。app

Apple使用了isa混寫(isa-swizzling)來實現KVO。框架

 

咱們能夠經過代碼去探索一下。函數

建立自定義類:XGPersonatom

@interface XGPerson : NSObject

@property (nonatomic,assign) int age;

@property (nonatomic,copy) NSString* name;

@end

咱們的思路就是看看對象添加KVO以前和以後有什麼變化,是否有區別,代碼以下:spa

@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的子類.code

看完對象,接下來咱們來看下屬性,就是被咱們添加了KVO的屬性age,咱們要觸發KVO回調就是去給age設置個值,那它確定就是調用setAge這個方法.orm

下面監聽下這個方法在被添加了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);

   // C語言的函數是須要手動釋放內存的喔
   free(methods);

}

下面使用這個方法打印下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

其實經過上面的代碼你們已經知道了KVO是怎麼觸發的了,那怎麼手動調用呢?很簡單,只要調用兩個方法就好了,以下:

 

    [self.person1 willChangeValueForKey:@"age"];
    [self.person1 didChangeValueForKey:@"age"];

可是上面說調用順序的時候,好像明明KVO是在 didChangeVlaueForKey 裏面調用的,爲何還要調用 willChangeVlaueForKey呢?

那是由於KVO調用的時候會去判斷這個對象有沒有調用 willChangeVlaueForKey 只有調用了這個以後,再調用 didChangeVlaueForKey 才能真正觸發KVO

直接修改爲員變量會觸發KVO嗎?

答案是不會的,爲何呢?由於KVO是經過修改set方法實現來觸發的,一個成員變量都沒有 set 方法,因此確定是不會觸發了.

總結

KVO是經過runtime機制動態的給要添加KVO監聽的對象建立一個子類,而且讓instance對象的isa指向這個全新的子類.

當修改instance對象的屬性時,會調用Foundation的_NSSetXXXValueAndNotify函數,順序以下:

  • willChangeValueForKey:
  • 父類原來的setter
  • didChangeValueForKey:

didChangeValueForKey 內部會觸發監聽器(Oberser)的監聽方法( observeValueForKeyPath:ofObject:change:context:)

經過這個子類重寫一些父類的方法達到觸發KVO回調的目的.

 

補充

KVO是使用了典型的發佈訂閱者設計模式實現事件回調的功能,多個訂閱者,一個發佈者,簡單的實現以下:

1> 訂閱者向發佈者進行訂閱.

2> 發佈者將訂閱者信息保存到一個集合中.

3> 當觸發事件後,發佈者就遍歷這個集合分別調用以前的訂閱者,從而達到1對多的通知.

 

以上已所有完畢,若有什麼不正確的地方你們能夠指出~~ ^_^ 下次再見~~

相關文章
相關標籤/搜索