KVO的使用及原理

概述

KVO 全稱KeyValueObserving鍵值監(jiān)聽,是蘋果提供的一套事件通訊機制。允許對象監(jiān)聽另一個對象特定屬性的改變,并在改變時接收到事件。一般繼承自NSObject的對象都默認支持KVO。

對象的屬性是否發(fā)生變化肯定會調(diào)用其setter方法,而KVO的本質(zhì)是監(jiān)聽對象有沒有調(diào)用被監(jiān)聽屬性的setter方法。

常用方法

使用KVO分三個步驟:

  • 添加觀察者(注冊)。
  • 觀察者中實現(xiàn)回調(diào)。
  • 移除觀察者(刪除)。
/*
注冊監(jiān)聽器
監(jiān)聽器對象為observer,被監(jiān)聽對象為消息的發(fā)送者即方法的調(diào)用者在回調(diào)函數(shù)中會被回傳
監(jiān)聽的屬性路徑為keyPath支持點語法的嵌套
監(jiān)聽類型為options支持按位或來監(jiān)聽多個事件類型
監(jiān)聽上下文context主要用于在多個監(jiān)聽器對象監(jiān)聽相同keyPath時進行區(qū)分
添加監(jiān)聽器只會保留監(jiān)聽器對象的地址,不會增加引用,也不會在對象釋放后置空,因此需要自己持有監(jiān)聽對象的強引用,該參數(shù)也會在回調(diào)函數(shù)中回傳
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

/*
刪除監(jiān)聽器
監(jiān)聽器對象為observer,被監(jiān)聽對象為消息的發(fā)送者即方法的調(diào)用者,應與addObserver方法匹配
監(jiān)聽的屬性路徑為keyPath,應與addObserver方法的keyPath匹配
監(jiān)聽上下文context,應與addObserver方法的context匹配
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

/*
與上一個方法相同,只是少了context參數(shù)
推薦使用上一個方法,該方法由于沒有傳遞context可能會產(chǎn)生異常結果
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/*
監(jiān)聽器對象的監(jiān)聽回調(diào)方法
keyPath即為監(jiān)聽的屬性路徑
object為被監(jiān)聽的對象
change保存被監(jiān)聽的值產(chǎn)生的變化
context為監(jiān)聽上下文,由add方法回傳
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

簡單實現(xiàn)

我們創(chuàng)建一個 Person類,然后在類中添加一個name屬性和sex屬性。

@interface Person : NSObject
@property (nonatomic, strong) NSString* name;
@property (nonatomic, strong) NSString* sex;
@end
 
@implementation Person

-(instancetype)init{
    self = [super init];
    if (self) {
        _name = @"liangtong";
        _sex = @"M";
    }
    return self;
}

然后我們觀察這個Person實例對象的name屬性

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    _person = [[Person alloc] init];
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [_person addObserver:self forKeyPath:@"name" options:options context:@"123"];
    _person.name = @"joker";
}

// 當監(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)dealloc{
    [_person removeObserver:self forKeyPath:@"name"];
    _person = nil;
}

執(zhí)行程序,然后log信息如下

2019-01-26 15:53:08.545313+0800 runtime_kvo[5666:1724236] 監(jiān)聽到isa : NSKVONotifying_Person 
 superclass : Person 
 setName IMP : 0x1096ea63a 
 setSex IMP: 0x1093914d0的name屬性值改變了 - {
    kind = 1;
    new = joker;
    old = liangtong;
} - 123

注冊觀察者后,當我們通過.給對象屬性進行賦值時,最終會通知觀察者具體的改變。例子中,我們會在監(jiān)聽回調(diào)中得到keypath 為 name,context為123的object變更。

實現(xiàn)原理

為了能夠看到更多的細節(jié),我們重寫Person類的description方法。

/****
 * 重寫description,展示更多信息
 ***/
-(NSString*)description{
    
    NSString* className = NSStringFromClass(object_getClass(self));
    NSString* superclass = NSStringFromClass(class_getSuperclass(object_getClass(self)));

    IMP setNameIMP = [self methodForSelector:@selector(setName:)];
    IMP setSexIMP = [self methodForSelector:@selector(setSex:)];
    
    NSString* desc = [NSString stringWithFormat:@"isa : %@ \n  \
                      superclass : %@ \n \
                      setName IMP : %p \n \
                      setSex IMP: %p",
                      className,superclass,setNameIMP,setSexIMP];
    return desc;
}

在注冊觀察者前后分別打印_person實例對象的信息,如下

2019-01-26 15:59:23.528750+0800 runtime_kvo[5734:1745208] Before Observe---------------->
 isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x100dca430 
                       setSex IMP: 0x100dca490
2019-01-26 15:59:23.529090+0800 runtime_kvo[5734:1745208] After Observe---------------->
 isa : NSKVONotifying_Person 
                        superclass : Person 
                       setName IMP : 0x10112363a 
                       setSex IMP: 0x100dca490
2019-01-26 15:59:23.529278+0800 runtime_kvo[5734:1745208] 監(jiān)聽到isa : NSKVONotifying_Person 
                        superclass : Person 
                       setName IMP : 0x10112363a 
                       setSex IMP: 0x100dca490的name屬性值改變了 - {
    kind = 1;
    new = joker;
    old = liangtong;
} - 123

我們可以看到,注冊KVO監(jiān)聽后,_person對象的isa指針由Person類變成了NSKVONotifying_Person類。而superclassNSObject變成了Person類。setName:的方法實現(xiàn)發(fā)生了變更(由0x100dca430變成了0x10112363a)而setSex:的未發(fā)生變更。

我們大致可以猜到KVO是通過isa-swizzling技術實現(xiàn)的

  • 在運行期間根據(jù)原類創(chuàng)建一個中間類(NSKVONotifying_xxx),這個中間類是原類的子類。
  • 動態(tài)修改了對象的isa指向中間類。
  • 中間類重寫了被監(jiān)聽屬性的setter方法,沒有監(jiān)聽的屬性setter方法則不會被重寫。
    • 重寫屬性的setter方法在修改之前會調(diào)用willChangeValueForKey:方法。
    • 重寫屬性的setter方法在修改之后會調(diào)用didChangeValueForKey:方法。
    • 通過添加斷點,我們可以看到在修改之后,會調(diào)用NSKeyValueNotifyObserver
    • 最終會調(diào)用到observeValueForKeyPath:ofObject:change:context:方法中。
  • 重寫delloc方法,銷毀新生成的NSKVONotifying_Person類。
斷點.png

猜測與驗證

通過以上,我們猜測如果阻止系統(tǒng)自動調(diào)用屬性的willChangeValueForKey:didChangeValueForKey:方法,可能會阻止KVO的事件傳遞。于是我們在Person類中重寫以下方法

/**
 * 當key未name時候,不自動觸發(fā)相關的setter
 **/
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

繼續(xù)運行剛才的代碼,結果如下

2019-01-26 16:28:54.434002+0800 runtime_kvo[6071:1853052] Before Observe---------------->
 isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x104ea4400 
                       setSex IMP: 0x104ea4460
2019-01-26 16:28:54.434228+0800 runtime_kvo[6071:1853052] After Observe---------------->
 isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x104ea4400 
                       setSex IMP: 0x104ea4460

果真未自動觸發(fā)KVO!!

那么問題來了,我們可以通過手動出發(fā)KVO嗎?如果我們手動調(diào)用被阻止的兩個方法,可以出發(fā)KVO嗎?為了測試我們的猜想,我們給Person類的sex屬性的setter中,添加相關代碼。

-(void)setSex:(NSString *)sex{
    _sex = sex;
    //通過手動調(diào)用setter,測試能否觸發(fā)KVO
    [self willChangeValueForKey:@"name"];
    _name = @"Hello Ketty";
    [self didChangeValueForKey:@"name"];
}

修改下運行的代碼,之前是通過name的setter進行操作,現(xiàn)在我們換成調(diào)用sex的setter,如下

    _person.sex = @"F";

結果如下:

2019-01-26 16:35:28.746294+0800 runtime_kvo[6174:1872068] Before Observe---------------->
 isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x10b9c2400 
                       setSex IMP: 0x10b9c2320
2019-01-26 16:35:28.746496+0800 runtime_kvo[6174:1872068] After Observe---------------->
 isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x10b9c2400 
                       setSex IMP: 0x10b9c2320
2019-01-26 16:35:28.746700+0800 runtime_kvo[6174:1872068] 監(jiān)聽到isa : Person 
                        superclass : NSObject 
                       setName IMP : 0x10b9c2400 
                       setSex IMP: 0x10b9c2320的name屬性值改變了 - {
    kind = 1;
    new = "Hello Ketty";
    old = liangtong;
} - 123

sexsetter方法中,我們手動調(diào)用willChangeValueForKey:didChangeValueForKey:方法對name進行設置,成功觸發(fā)了KVO操作!

總結

經(jīng)過以上代碼,我們大致了解了KVO的本質(zhì)。

  • 1、isa-swizzling,利用RuntimeApi動態(tài)生成一個子類(NSKVONotifying_xxx),并讓instance對象的isa指向這個全新的子類。
  • 2、當修改對象的被監(jiān)聽屬性時候,會依次調(diào)用子類(NSKVONotifying_xxx)的以下方法
    • willChangeValueForKey:
    • 父類原來的setter
    • didChangeValueForKey:
    • 最終觸發(fā)observeValueForKeyPath:ofObject:change:context:

當我們重寫automaticallyNotifiesObserversForKey:方法,對name的相關自動調(diào)用willChangeValueForKey:didChangeValueForKey:`方法返回NO時,KVO未觸發(fā),表明直接修改成員變量的值不會觸發(fā)KVO。

經(jīng)過以上猜測部分,我們也知道了如何手動觸發(fā)KVO。手動調(diào)用以下兩個方法:

  • 手動調(diào)用 willChangeValueForKey:
  • 手動調(diào)用didChangeValueForKey:

KVO通知依賴以上兩個方法,在屬性變更之前通過調(diào)用 willChangeValueForKey:記錄舊值;而屬性發(fā)生改變之后通過調(diào)用didChangeValueForKey:保存新值。繼而 observeValueForKey:ofObject:change:context:也會被調(diào)用。

KVO的監(jiān)聽移除

  • 添加與移除成對出現(xiàn)

  • 不移除會造成內(nèi)存泄漏

  • 多次移除會造成崩潰

    •    @try {
            [object removeObserver:self forKeyPath:@"keyPath"];
         }
         @catch (NSException * __unused exception) {}
      
    • 系統(tǒng)為了實現(xiàn)KVO,為NSObject添加了一個名為NSKeyValueObserverRegistration的Category,KVO的add和remove的實現(xiàn)都在里面。在移除的時候,系統(tǒng)會判斷當前KVO的key是否已經(jīng)被移除,如果已經(jīng)被移除,則主動拋出一個NSException的異常

Demo

https://github.com/liangtongdev/Demo-runtime_kvo

參照

KVO原理分析及使用進階:http://m.itdecent.cn/p/badf5cac0130
KVO :https://github.com/SunshineBrother/JHBlog/blob/master/iOS知識點/iOS底層/3、KVO.md
iOS-KVO 實現(xiàn)原理:http://m.itdecent.cn/p/0e75d99c3480

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容