KVO原理分析

劉小壯發表於2021-11-21

介紹

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開源第三方框架KVOControllerKVOController本質上是對系統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下載原始碼仔細分析一下。

相關文章