KVO原理分析

語言: CN / TW / HK

介紹

KVO 全稱 KeyValueObserving ,是蘋果提供的一套事件通知機制。允許對象監聽另一個對象特定屬性的改變,並在改變時接收到事件。由於 KVO 的實現機制,所以對屬性才會發生作用,一般繼承自 NSObject 的對象都默認支持 KVO

KVONSNotificationCenter 都是 iOS 中觀察者模式的一種實現。區別在於,相對於被觀察者和觀察者之間的關係, KVO 是一對一的,而不一對多的。 KVO 對被監聽對象無侵入性,不需要手動修改其內部代碼即可實現監聽。

KVO 可以監聽單個屬性的變化,也可以監聽集合對象的變化。通過 KVCmutableArrayValueForKey: 等方法獲得代理對象,當代理對象的內部對象發生改變時,會回調 KVO 監聽的方法。集合對象包含 NSArrayNSSet

使用

使用 KVO 分為三個步驟

  1. 通過 addObserver:forKeyPath:options:context: 方法註冊觀察者,觀察者可以接收 keyPath 屬性的變化事件回調。
  2. 在觀察者中實現 observeValueForKeyPath:ofObject:change:context: 方法,當 keyPath 屬性發生改變後, KVO 會回調這個方法來通知觀察者。
  3. 當觀察者不需要監聽時,可以調用 removeObserver:forKeyPath: 方法將 KVO 移除。需要注意的是,調用 removeObserver 需要在觀察者消失之前,否則會導致 Crash

註冊

在註冊觀察者時,可以傳入 options 參數,參數是一個枚舉類型。如果傳入 NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld 表示接收新值和舊值,默認為只接收新值。如果想在註冊觀察者後,立即接收一次回調,則可以加入 NSKeyValueObservingOptionInitial 枚舉。

還可以通過方法 context 傳入任意類型的對象,在接收消息回調的代碼中可以接收到這個對象,是 KVO 中的一種傳值方式。

在調用 addObserver 方法後, KVO 並不會對觀察者進行強引用。所以需要注意觀察者的生命週期,否則會導致觀察者被釋放帶來的 Crash

監聽

觀察者需要實現 observeValueForKeyPath:ofObject:change:context: 方法,當 KVO 事件到來時會調用這個方法,如果沒有實現會導致 Crashchange 字典中存放 KVO 屬性相關的值,根據 options 時傳入的枚舉來返回。枚舉會對應相應 key 來從字典中取出值,例如有 NSKeyValueChangeOldKey 字段,存儲改變之前的舊值。

change 中還有 NSKeyValueChangeKindKey 字段,和 NSKeyValueChangeOldKey 是平級的關係,來提供本次更改的信息,對應 NSKeyValueChange 枚舉類型的 value 。例如被觀察屬性發生改變時,字段為 NSKeyValueChangeSetting

如果被觀察對象是集合對象,在 NSKeyValueChangeKindKey 字段中會包含 NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement 的信息,表示集合對象的操作方式。

其他觸發方法

調用 KVO 屬性對象時,不僅可以通過點語法和 set 語法進行調用, KVO 兼容很多種調用方式。

// 直接調用set方法,或者通過屬性的點語法間接調用
[account setName:@"Savings"];

// 使用KVC的setValue:forKey:方法
[account setValue:@"Savings" forKey:@"name"];

// 使用KVC的setValue:forKeyPath:方法
[document setValue:@"Savings" forKeyPath:@"account.name"];

// 通過mutableArrayValueForKey:方法獲取到代理對象,並使用代理對象進行操作
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

實際應用

KVO 主要用來做鍵值觀察操作,想要一個值發生改變後通知另一個對象,則用 KVO 實現最為合適。斯坦福大學的 iOS 教程中有一個很經典的案例,通過 KVOModelController 之間進行通信。

觸發

主動觸發

KVO 在屬性發生改變時的調用是自動的,如果想要手動控制這個調用時機,或想自己實現 KVO 屬性的調用,則可以通過 KVO 提供的方法進行調用。

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}

可以看到調用 KVO 主要依靠兩個方法,在屬性發生改變之前調用 willChangeValueForKey: 方法,在發生改變之後調用 didChangeValueForKey: 方法。但是,如果不調用 willChangeValueForKey ,直接調用 didChangeValueForKey 是不生效的,二者有先後順序並且需要成對出現。

禁用KVO

如果想禁止某個屬性的 KVO ,例如關鍵信息不想被三方 SDK 通過 KVO 的方式獲取,可以通過 automaticallyNotifiesObserversForKey 方法返回 NO 來禁止其他地方對這個屬性進行 KVO 。方法返回 YES 則表示可以調用,如果返回 NO 則表示不可以調用。此方法是一個類方法,可以在方法內部判斷 keyPath ,來選擇這個屬性是否允許被 KVO

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

KVC觸發

KVCKVO 有特殊兼容,當通過 KVC 調用非屬性的實例變量時, KVC 內部也會觸發 KVO 的回調,並通過 NSKeyValueDidChangeNSKeyValueWillChange 向上回調。

下面忽略 main 函數向上的系統函數,只保留關鍵堆棧。這是通過調用屬性 setter 方法的方式回調的 KVO 堆棧。

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 38.1
* frame #0: 0x0000000101bc3a15 TestKVO`::-[ViewController observeValueForKeyPath:ofObject:change:context:](self=0x00007f8419705890, _cmd="observeValueForKeyPath:ofObject:change:context:", keyPath="object", object=0x0000604000015b00, change=0x0000608000265540, context=0x0000000000000000) at ViewController.mm:84
frame #1: 0x000000010327e820 Foundation`NSKeyValueNotifyObserver + 349
frame #2: 0x000000010327e0d7 Foundation`NSKeyValueDidChange + 483
frame #3: 0x000000010335f22b Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:] + 778
frame #4: 0x000000010324b1b4 Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 61
frame #5: 0x00000001032a7b79 Foundation`_NSSetObjectValueAndNotify + 255
frame #6: 0x0000000101bc3937 TestKVO`::-[ViewController viewDidLoad](self=0x00007f8419705890, _cmd="viewDidLoad") at ViewController.mm:70

這是通過 KVC 觸發的向上回調,可以看到正常通過修改屬性的方式觸發 KVO ,和通過 KVC 觸發的 KVO 還是有區別的。通過 KVC 的方式觸發 KVO ,甚至都沒有 _NSSetObjectValueAndNotify 的調用。

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 37.1
* frame #0: 0x0000000106be1a85 TestKVO`::-[ViewController observeValueForKeyPath:ofObject:change:context:](self=0x00007fe68ac07710, _cmd="observeValueForKeyPath:ofObject:change:context:", keyPath="object", object=0x0000600000010c80, change=0x000060c000262780, context=0x0000000000000000) at ViewController.mm:84
frame #1: 0x000000010886d820 Foundation`NSKeyValueNotifyObserver + 349
frame #2: 0x000000010886d0d7 Foundation`NSKeyValueDidChange + 483
frame #3: 0x000000010894d422 Foundation`NSKeyValueDidChangeWithPerThreadPendingNotifications + 148
frame #4: 0x0000000108879b47 Foundation`-[NSObject(NSKeyValueCoding) setValue:forKey:] + 292
frame #5: 0x0000000106be19aa TestKVO`::-[ViewController viewDidLoad](self=0x00007fe68ac07710, _cmd="viewDidLoad") at ViewController.mm:70

實現原理

核心邏輯

KVO 是通過 isa-swizzling 技術實現的,這是整個 KVO 實現的重點。在運行時根據原類創建一箇中間類,這個中間類是原類的子類,並動態修改當前對象的 isa 指向中間類。並且將 class 方法重寫,返回原類的 Class 。蘋果重寫 class 方法,就是為了屏蔽中間類的存在。

所以,蘋果建議在開發中不應該依賴 isa 指針,而是通過 class 實例方法來獲取對象類型,來避免被 KVO 或者其他 runtime 方法影響。

_NSSetObjectValueAndNotify

隨後會修改中間類對應的 set 方法,並且插入 willChangeValueForkey 方法以及 didChangeValueForKey 方法,在兩個方法中間調用父類的 set 方法。這個過程,系統將其封裝到 _NSSetObjectValueAndNotify 函數中。通過查看這個函數的彙編代碼,可以看到內部封裝的 willChangeValueForkey 方法和 didChangeValueForKey 方法的調用。

系統並不是只封裝了 _NSSetObjectValueAndNotify 函數,而是會根據屬性類型,調用不同的函數。如果是 Int 類型就會調用 _NSSetIntValueAndNotify ,這些實現都定義在 Foundation 框架中。具體的可以通過 hopper 來查看 Foundation 框架的實現。

runtime 會將新生成的 NSKVONotifying_KVOTestsetObject 方法的實現,替換成 _NSSetObjectValueAndNotify 函數,而不是重寫 setObject 函數。通過下面的測試代碼,可以查看 selector 對應的 IMP ,並且將其實現的地址打印出來。

KVOTest *test = [[KVOTest alloc] init];
[test setObject:[[NSObject alloc] init]];
NSLog(@"%p", [test methodForSelector:@selector(setObject:)]);
[test addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];
[test setObject:[[NSObject alloc] init]];
NSLog(@"%p", [test methodForSelector:@selector(setObject:)]);

// 打印結果,第一次的方法地址為0x100c8e270,第二次的方法地址為0x7fff207a3203
(lldb) p (IMP)0x100c8e270
(IMP) $0 = 0x0000000100c8e270 (DemoProject`-[KVOTest setObject:] at KVOTest.h:11)
(lldb) p (IMP)0x7fff207a3203
(IMP) $1 = 0x00007fff207a3203 (Foundation`_NSSetObjectValueAndNotify)

_NSKVONotifyingCreateInfoWithOriginalClass

對於系統實現 KVO 的原理,可以對 object_setClass 打斷點,或者對 objc_allocateClassPair 方法打斷點也可以,這兩個方法都是創建類必走的方法。通過這兩個方法的彙編堆棧,向前回溯。隨後,可以得到翻譯後如下的彙編代碼。

可以看到有一些類名拼接規則,隨後根據類名創建新類。如果 newCls 為空則已經創建過,或者可能為空。如果 newCls 不為空,則註冊新創建的類,並且設置 SDTestKVOClassIndexedIvars 結構體的一些參數。

Class _NSKVONotifyingCreateInfoWithOriginalClass(Class originalClass) {
    const char *clsName = class_getName(originalClass);
    size_t len = strlen(clsName);
    len += 0x10;
    char *newClsName = malloc(len);
    const char *prefix = "NSKVONotifying_";
    __strlcpy_chk(newClsName, prefix, len);
    __strlcat_chk(newClsName, clsName, len, -1);
    Class newCls = objc_allocateClassPair(originalClass, newClsName, 0x68);
    if (newCls) {
        objc_registerClassPair(newCls);
        SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(newCls);
        indexedIvars->originalClass = originalClass;
        indexedIvars->KVOClass = newCls;
        CFMutableSetRef mset = CFSetCreateMutable(nil, 0, kCFCopyStringSetCallBacks);
        indexedIvars->mset = mset;
        CFMutableDictionaryRef mdict = CFDictionaryCreateMutable(nil, 0, nil, kCFTypeDictionaryValueCallBacks);
        indexedIvars->mdict = mdict;
        pthread_mutex_init(indexedIvars->lock);
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            bool flag = true;
            IMP willChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, @selector(willChangeValueForKey:));
            IMP didChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, @selector(didChangeValueForKey:));
            if (willChangeValueForKeyImp == _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange && didChangeValueForKeyImp == _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange) {
                flag = false;
            }
            indexedIvars->flag = flag;
            NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(_isKVOA), NSKVOIsAutonotifying, nil);
            NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(dealloc), NSKVODeallocate, nil);
            NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(class), NSKVOClass, nil);
        });
    } else {
        return nil;
    }
    return newCls;
}

驗證

為了驗證 KVO 的實現方式,我們加入下面的測試代碼。首先創建一個 KVOObject 類,並在裏面加入兩個屬性,然後重寫 description 方法,並在內部打印一些關鍵參數。

需要注意的是,為了驗證 KVO 在運行時做了什麼,我打印了對象的 class 方法,以及通過 runtime 獲取對象的類和父類。在添加 KVO 監聽前後,都打印一次,觀察系統做了什麼。

@interface KVOObject : NSObject
@property (nonatomic, copy  ) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

- (NSString *)description {
    IMP nameIMP = class_getMethodImplementation(object_getClass(self), @selector(setName:));
    IMP ageIMP = class_getMethodImplementation(object_getClass(self), @selector(setAge:));
    NSLog(@"object setName: IMP %p object setAge: IMP %p \n", nameIMP, ageIMP);
    
    Class objectMethodClass = [self class];
    Class objectRuntimeClass = object_getClass(self);
    Class superClass = class_getSuperclass(objectRuntimeClass);
    NSLog(@"objectMethodClass : %@, ObjectRuntimeClass : %@, superClass : %@ \n", objectMethodClass, objectRuntimeClass, superClass);
    
    NSLog(@"object method list \n");
    unsigned int count;
    Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
    for (NSInteger i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"method Name = %@\n", methodName);
    }
    
    return @"";
}

創建一個 KVOObject 對象,在 KVO 前後分別打印對象的關鍵信息,看 KVO 前後有什麼變化。

self.object = [[KVOObject alloc] init];
[self.object description];

[self.object addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

[self.object description];

下面是 KVO 前後打印的關鍵信息。

我們發現對象被 KVO 後,其真正類型變為了 NSKVONotifying_KVOObject 類,已經不是之前的類了。 KVO 會在運行時動態創建一個新類,將對象的 isa 指向新創建的類,並且將 superClass 指向原來的類 KVOObject ,新創建的類命名規則是 NSKVONotifying_xxx 的格式。 KVO 為了使其更像之前的類,還會將對象的 class 實例方法重寫,使其更像原類。

添加 KVO 之後,由於修改了 setName 方法和 setAge 方法的 IMP ,所以打印這兩個方法的 IMP ,也是一個新的地址,新的實現在 NSKVONotifying_KVOObject 中。

這種實現方式對業務代碼沒有侵入性,可以在不影響 KVOObject 其他對象的前提下,對單個對象進行監聽並修改其方法實現,在賦值時觸發 KVO 回調。

在上面的代碼中還發現了 _isKVOA 方法,這個方法可以當做使用了 KVO 的一個標記,系統可能也是這麼用的。如果我們想判斷當前類是否是 KVO 動態生成的類,就可以從方法列表中搜索這個方法。

// 第一次
object address : 0x604000239340
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age

// 第二次
object address : 0x604000239340
object setName: IMP 0x10ea8defe object setAge: IMP 0x10ea94106
objectMethodClass : KVOObject, ObjectRuntimeClass : NSKVONotifying_KVOObject, superClass : KVOObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA

object_getClass

為什麼上面調用 runtimeobject_getClass 函數,就可以獲取到真正的類呢?

調用 object_getClass 函數後其返回的是一個 Class 類型, Classobjc_class 定義的一個 typedef 別名,通過 objc_class 就可以獲取到對象的 isa 指針指向的 Class ,也就是對象的類對象。

由此可以知道, object_getClass 函數內部返回的是對象的 isa 指針。

typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif
}

注意點

Crash

KVOaddObserverremoveObserver 需要是成對的,如果重複 remove 則會導致 NSRangeException 類型的 Crash ,如果忘記 remove 則會在觀察者釋放後再次接收到 KVO 回調時 Crash

蘋果官方推薦的方式是,在 init 的時候進行 addObserver ,在 deallocremoveObserver ,這樣可以保證 addremove 是成對出現的,是一種比較理想的使用方式。

錯誤檢查

如果傳入一個錯誤的 keyPath 並不會有錯誤提示。在調用 KVO 時需要傳入一個 keyPath ,由於 keyPath 是字符串的形式,如果屬性名發生改變後,字符串沒有改變容易導致 Crash 。對於這個問題,我們可以利用系統的反射機制將 keyPath 反射出來,這樣編譯器可以在 @selector() 中進行合法性檢查。

NSString *keyPath = NSStringFromSelector(@selector(isFinished));

不能觸發回調

由於 KVO 的實現機制,如果調用成員變量進行賦值,是不會觸發 KVO 的。

@interface TestObject : NSObject {
    @public
    NSObject *object;
}
@end

// 錯誤的調用方式
self.object = [[TestObject alloc] init];
[self.object addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];
self.object->object = [[NSObject alloc] init];

但是,如果通過 KVC 的方式調用賦值操作,則會觸發 KVO 的回調方法。這是因為 KVCKVO 有單獨的兼容,在 KVC 的賦值方法內部,手動調用了 willChangeValueForKey:didChangeValueForKey: 方法。

// KVC的方式調用
self.object = [[TestObject alloc] init];
[self.object addObserver:self forKeyPath:@"object" options:NSKeyValueObservingOptionNew context:nil];
[self.object setValue:[[NSObject alloc] init] forKey:@"object"];

重複添加

KVO 進行重複 addObserver 並不會導致崩潰,但是會出現重複執行 KVO 回調方法的問題。

[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
self.testLabel.text = @"test";
[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
self.testLabel.text = @"test";

// 輸出
2018-08-03 11:48:49.502450+0800 KVOTest[5846:412257] test
2018-08-03 11:48:52.975102+0800 KVOTest[5846:412257] test
2018-08-03 11:48:53.547145+0800 KVOTest[5846:412257] test
2018-08-03 11:48:54.087171+0800 KVOTest[5846:412257] test
2018-08-03 11:48:54.649244+0800 KVOTest[5846:412257] test

通過上面的測試代碼,並且在回調中打印 object 所對應的 Class 來看,並不會重複創建子類,始終都是一個類。雖然重複 addobserver 不會立刻崩潰,但是重複添加後在第一次調用 removeObserver 時,就會立刻崩潰。從崩潰堆棧來看,和重複移除的問題一樣,都是系統主動拋出的異常。

Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <UILabel 0x7f859b547490> for the key path "text" from <UILabel 0x7f859b547490> because it is not registered as an observer.'

重複移除

KVO 是不允許對一個 keyPath 進行重複移除的,如果重複移除,則會導致崩潰。例如下面的測試代碼。

[self.testLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
self.testLabel.text = @"test";
[self.testLabel removeObserver:self forKeyPath:@"text"];
[self.testLabel removeObserver:self forKeyPath:@"text"];
[self.testLabel removeObserver:self forKeyPath:@"text"];

執行上面的測試代碼後,會造成下面的崩潰信息。從 KVO 的崩潰堆棧可以看出來,系統為了實現 KVOaddObserverremoveObserver ,為 NSObject 添加了一個名為 NSKeyValueObserverRegistrationCategoryKVOaddObserverremoveObserver 的實現都在裏面。

在移除 KVO 的監聽時,系統會判斷當前 KVOkeyPath 是否已經被移除,如果已經被移除,則主動拋出一個 NSException 的異常。

2018-08-03 10:54:27.477379+0800 KVOTest[4939:286991] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <ViewController 0x7ff6aee31600> for the key path "text" from <UILabel 0x7ff6aee2e850> because it is not registered as an observer.'
*** First throw call stack:
(
    0   CoreFoundation                      0x000000010db2312b __exceptionPreprocess + 171
    1   libobjc.A.dylib                     0x000000010cc6af41 objc_exception_throw + 48
    2   CoreFoundation                      0x000000010db98245 +[NSException raise:format:] + 197
    3   Foundation                          0x0000000108631f15 -[NSObject(NSKeyValueObserverRegistration) _removeObserver:forProperty:] + 497
    4   Foundation                          0x0000000108631ccb -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:] + 84
    5   KVOTest                             0x0000000107959a55 -[ViewController viewDidAppear:] + 373
    // .....
    20  UIKit                               0x000000010996d5d6 UIApplicationMain + 159
    21  KVOTest                             0x00000001079696cf main + 111
    22  libdyld.dylib                       0x000000010fb43d81 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

排查鏈路

KVO 是一種事件綁定機制的實現,在 keyPath 對應的值發生改變後會回調對應的方法。這種數據綁定機制,在對象關係很複雜的情況下,很容易導致不好排查的 bug 。例如 keyPath 對應的屬性被調用的關係很複雜,就不太建議對這個屬性進行 KVO

自己實現KVO

除了上面的缺點, KVO 還不支持 block 語法,需要單獨重寫父類方法,這樣加上 addremove 方法就會導致代碼很分散。所以,我通過 runtime 簡單的實現了一個 KVO ,源碼放在我的 Github 上,叫做EasyKVO。

self.object = [[KVOObject alloc] init];
[self.object lxz_addObserver:self originalSelector:@selector(name) callback:^(id observedObject, NSString *observedKey, id oldValue, id newValue) {
    // 處理業務邏輯
}];

self.object.name = @"lxz";

// 移除通知
[self.object lxz_removeObserver:self originalSelector:@selector(name)];

調用代碼很簡單,直接通過 lxz_addObserver:originalSelector:callback: 方法就可以添加 KVO 的監聽,可以通過 callbackblock 接收屬性發生改變後的回調。而且方法的 keyPath 接收的是一個 SEL 類型參數,所以可以通過 @selector() 傳入參數時進行方法合法性檢查,如果是未實現的方法直接就會報警吿。

通過 lxz_removeObserver:originalSelector: 方法傳入觀察者和 keyPath ,當觀察者所有 keyPath 都移除後則從 KVO 中移除觀察者對象。

如果重複 addObserverremoveObserver 也沒事,內部有判斷邏輯。 EasyKVO 內部通過 weak 對觀察者做引用,並不會影響觀察者的生命週期,並且在觀察者釋放後不會導致 Crash 。一次 add 方法調用對應一個 block ,如果觀察者監聽多個 keyPath 屬性,不需要在 block 回調中判斷 keyPath

KVOController

想在項目中安全便捷的使用 KVO 的話,推薦 Facebook 的一個 KVO 開源第三方框架KVOController。 KVOController 本質上是對系統 KVO 的封裝,具有原生 KVO 所有的功能,而且規避了原生 KVO 的很多問題,兼容 blockaction 兩種回調方式。

源碼分析

從源碼來看還是比較簡單的,主要分為 NSObjectCategoryFBKVOController 兩部分。

Category 中提供了 KVOControllerKVOControllerNonRetaining 兩個屬性,顧名思義第一個會對 observer 產生強引用,第二個則不會。其內部代碼就是創建 FBKVOController 對象的代碼,並將創建出來的對象賦值給 Category 的屬性,直接通過這個 Category 就可以懶加載創建 FBKVOController 對象。

- (FBKVOController *)KVOControllerNonRetaining
{
  id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey);
  
  if (nil == controller) {
    controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
    self.KVOControllerNonRetaining = controller;
  }
  
  return controller;
}

實現原理

FBKVOController 中分為三部分, _FBKVOInfo 是一個私有類,這個類的功能很簡單,就是以結構化的形式保存 FBKVOController 所需的各個對象,類似於模型類的功能。

還有一個私有類 _FBKVOSharedController ,這是 FBKVOController 框架實現的關鍵。從命名上可以看出其是一個單例,所有通過 FBKVOController 實現的 KVO ,觀察者都是它。每次通過 FBKVOController 添加一個 KVO 時, _FBKVOSharedController 都會將自己設為觀察者,並在其內部實現 observeValueForKeyPath:ofObject:change:context: 方法,將接收到的消息通過 blockaction 進行轉發。

其功能很簡單,通過 observe:info: 方法添加 KVO 監聽,並用一個 NSHashTable 保存 _FBKVOInfo 信息。通過 unobserve:info: 方法移除監聽,並從 NSHashTable 中將對應的 _FBKVOInfo 移除。這兩個方法內部都會調用系統的 KVO 方法。

在外界使用時需要用 FBKVOController 類,其內部實現了初始化以及添加和移除監聽的操作。在調用添加監聽方法後,其內部會創建一個 _FBKVOInfo 對象,並通過一個 NSMapTable 對象進行持有,然後會調用 _FBKVOSharedController 來進行註冊監聽。

使用 FBKVOController 的話,不需要手動調用 removeObserver 方法,在被監聽對象消失的時候,會在 dealloc 中調用 remove 方法。如果因為業務需求,可以手動調用 remove 方法,重複調用 remove 方法不會有問題。

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
    NSMutableSet *infos = [_objectInfosMap objectForKey:object];

    _FBKVOInfo *existingInfo = [infos member:info];
    if (nil != existingInfo) {
      return;
    }

    if (nil == infos) {
      infos = [NSMutableSet set];
      [_objectInfosMap setObject:infos forKey:object];
    }

    [infos addObject:info];

    [[_FBKVOSharedController sharedController] observe:object info:info];
}

因為 FBKVOController 的實現很簡單,所以這裏就很簡單的講講,具體實現可以去Github下載源碼仔細分析一下。