KVO原理分析

該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯


配圖

介紹

KVO全稱KeyValueObserving竭宰,是蘋果提供的一套事件通知機(jī)制。允許對象監(jiān)聽另一個對象特定屬性的改變留量,并在改變時接收到事件吼具。由于KVO的實現(xiàn)機(jī)制,所以對屬性才會發(fā)生作用从橘,一般繼承自NSObject的對象都默認(rèn)支持KVO念赶。

KVONSNotificationCenter都是iOS中觀察者模式的一種實現(xiàn)。區(qū)別在于恰力,相對于被觀察者和觀察者之間的關(guān)系叉谜,KVO是一對一的,而不一對多的踩萎。KVO對被監(jiān)聽對象無侵入性停局,不需要手動修改其內(nèi)部代碼即可實現(xiàn)監(jiān)聽。

KVO可以監(jiān)聽單個屬性的變化香府,也可以監(jiān)聽集合對象的變化董栽。通過KVCmutableArrayValueForKey:等方法獲得代理對象,當(dāng)代理對象的內(nèi)部對象發(fā)生改變時企孩,會回調(diào)KVO監(jiān)聽的方法锭碳。集合對象包含NSArrayNSSet

使用

使用KVO分為三個步驟

  1. 通過addObserver:forKeyPath:options:context:方法注冊觀察者勿璃,觀察者可以接收keyPath屬性的變化事件回調(diào)擒抛。
  2. 在觀察者中實現(xiàn)observeValueForKeyPath:ofObject:change:context:方法,當(dāng)keyPath屬性發(fā)生改變后补疑,KVO會回調(diào)這個方法來通知觀察者闻葵。
  3. 當(dāng)觀察者不需要監(jiān)聽時,可以調(diào)用removeObserver:forKeyPath:方法將KVO移除癣丧。需要注意的是槽畔,調(diào)用removeObserver需要在觀察者消失之前,否則會導(dǎo)致Crash胁编。

注冊

在注冊觀察者時厢钧,可以傳入options參數(shù)鳞尔,參數(shù)是一個枚舉類型。如果傳入NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld表示接收新值和舊值早直,默認(rèn)為只接收新值寥假。如果想在注冊觀察者后,立即接收一次回調(diào)霞扬,則可以加入NSKeyValueObservingOptionInitial枚舉糕韧。

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

在調(diào)用addObserver方法后,KVO并不會對觀察者進(jìn)行強(qiáng)引用斧拍。所以需要注意觀察者的生命周期雀扶,否則會導(dǎo)致觀察者被釋放帶來的Crash

監(jiān)聽

觀察者需要實現(xiàn)observeValueForKeyPath:ofObject:change:context:方法肆汹,當(dāng)KVO事件到來時會調(diào)用這個方法愚墓,如果沒有實現(xiàn)會導(dǎo)致Crashchange字典中存放KVO屬性相關(guān)的值昂勉,根據(jù)options時傳入的枚舉來返回浪册。枚舉會對應(yīng)相應(yīng)key來從字典中取出值,例如有NSKeyValueChangeOldKey字段岗照,存儲改變之前的舊值村象。

change中還有NSKeyValueChangeKindKey字段,和NSKeyValueChangeOldKey是平級的關(guān)系谴返,來提供本次更改的信息,對應(yīng)NSKeyValueChange枚舉類型的value咧织。例如被觀察屬性發(fā)生改變時嗓袱,字段為NSKeyValueChangeSetting

如果被觀察對象是集合對象习绢,在NSKeyValueChangeKindKey字段中會包含NSKeyValueChangeInsertion渠抹、NSKeyValueChangeRemovalNSKeyValueChangeReplacement的信息闪萄,表示集合對象的操作方式梧却。

其他觸發(fā)方法

調(diào)用KVO屬性對象時,不僅可以通過點語法和set語法進(jìn)行調(diào)用败去,KVO兼容很多種調(diào)用方式放航。

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

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

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

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

實際應(yīng)用

KVO主要用來做鍵值觀察操作广鳍,想要一個值發(fā)生改變后通知另一個對象荆几,則用KVO實現(xiàn)最為合適。斯坦福大學(xué)的iOS教程中有一個很經(jīng)典的案例赊时,通過KVOModelController之間進(jìn)行通信吨铸。

觸發(fā)

主動觸發(fā)

KVO在屬性發(fā)生改變時的調(diào)用是自動的,如果想要手動控制這個調(diào)用時機(jī)祖秒,或想自己實現(xiàn)KVO屬性的調(diào)用诞吱,則可以通過KVO提供的方法進(jìn)行調(diào)用。

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

可以看到調(diào)用KVO主要依靠兩個方法竭缝,在屬性發(fā)生改變之前調(diào)用willChangeValueForKey:方法房维,在發(fā)生改變之后調(diào)用didChangeValueForKey:方法。但是歌馍,如果不調(diào)用willChangeValueForKey握巢,直接調(diào)用didChangeValueForKey是不生效的,二者有先后順序并且需要成對出現(xiàn)松却。

禁用KVO

如果想禁止某個屬性的KVO暴浦,例如關(guān)鍵信息不想被三方SDK通過KVO的方式獲取,可以通過automaticallyNotifiesObserversForKey方法返回NO來禁止其他地方對這個屬性進(jìn)行KVO晓锻。方法返回YES則表示可以調(diào)用歌焦,如果返回NO則表示不可以調(diào)用。此方法是一個類方法砚哆,可以在方法內(nèi)部判斷keyPath独撇,來選擇這個屬性是否允許被KVO

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

KVC觸發(fā)

KVCKVO有特殊兼容躁锁,當(dāng)通過KVC調(diào)用非屬性的實例變量時纷铣,KVC內(nèi)部也會觸發(fā)KVO的回調(diào),并通過NSKeyValueDidChangeNSKeyValueWillChange向上回調(diào)战转。

下面忽略main函數(shù)向上的系統(tǒng)函數(shù)搜立,只保留關(guān)鍵堆棧。這是通過調(diào)用屬性setter方法的方式回調(diào)的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觸發(fā)的向上回調(diào)啄踊,可以看到正常通過修改屬性的方式觸發(fā)KVO,和通過KVC觸發(fā)的KVO還是有區(qū)別的刁标。通過KVC的方式觸發(fā)KVO颠通,甚至都沒有_NSSetObjectValueAndNotify的調(diào)用。

* 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

實現(xiàn)原理

核心邏輯

KVO是通過isa-swizzling技術(shù)實現(xiàn)的膀懈,這是整個KVO實現(xiàn)的重點顿锰。在運(yùn)行時根據(jù)原類創(chuàng)建一個中間類,這個中間類是原類的子類,并動態(tài)修改當(dāng)前對象的isa指向中間類撵儿。并且將class方法重寫乘客,返回原類的Class。蘋果重寫class方法淀歇,就是為了屏蔽中間類的存在易核。

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

_NSSetObjectValueAndNotify

隨后會修改中間類對應(yīng)的set方法纳决,并且插入willChangeValueForkey方法以及didChangeValueForKey方法碰逸,在兩個方法中間調(diào)用父類的set方法。這個過程阔加,系統(tǒng)將其封裝到_NSSetObjectValueAndNotify函數(shù)中饵史。通過查看這個函數(shù)的匯編代碼,可以看到內(nèi)部封裝的willChangeValueForkey方法和didChangeValueForKey方法的調(diào)用胜榔。

系統(tǒng)并不是只封裝了_NSSetObjectValueAndNotify函數(shù)胳喷,而是會根據(jù)屬性類型,調(diào)用不同的函數(shù)夭织。如果是Int類型就會調(diào)用_NSSetIntValueAndNotify吭露,這些實現(xiàn)都定義在Foundation框架中。具體的可以通過hopper來查看Foundation框架的實現(xiàn)尊惰。

runtime會將新生成的NSKVONotifying_KVOTestsetObject方法的實現(xiàn)讲竿,替換成_NSSetObjectValueAndNotify函數(shù),而不是重寫setObject函數(shù)弄屡。通過下面的測試代碼题禀,可以查看selector對應(yīng)的IMP,并且將其實現(xiàn)的地址打印出來膀捷。

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:)]);

// 打印結(jié)果迈嘹,第一次的方法地址為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

對于系統(tǒng)實現(xiàn)KVO的原理担孔,可以對object_setClass打斷點江锨,或者對objc_allocateClassPair方法打斷點也可以吃警,這兩個方法都是創(chuàng)建類必走的方法糕篇。通過這兩個方法的匯編堆棧拌消,向前回溯氓英。隨后铝阐,可以得到翻譯后如下的匯編代碼。

可以看到有一些類名拼接規(guī)則遍蟋,隨后根據(jù)類名創(chuàng)建新類它呀。如果newCls為空則已經(jīng)創(chuàng)建過纵穿,或者可能為空政恍。如果newCls不為空,則注冊新創(chuàng)建的類宗弯,并且設(shè)置SDTestKVOClassIndexedIvars結(jié)構(gòu)體的一些參數(shù)蒙保。

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的實現(xiàn)方式扁瓢,我們加入下面的測試代碼引几。首先創(chuàng)建一個KVOObject類敞掘,并在里面加入兩個屬性玖雁,然后重寫description方法疯潭,并在內(nèi)部打印一些關(guān)鍵參數(shù)竖哩。

需要注意的是相叁,為了驗證KVO在運(yùn)行時做了什么,我打印了對象的class方法虑润,以及通過runtime獲取對象的類和父類拳喻。在添加KVO監(jiān)聽前后冗澈,都打印一次亚亲,觀察系統(tǒng)做了什么。

@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 @"";
}

創(chuàng)建一個KVOObject對象惜索,在KVO前后分別打印對象的關(guān)鍵信息,看KVO前后有什么變化偿渡。

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

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

[self.object description];

下面是KVO前后打印的關(guān)鍵信息吉拳。

我們發(fā)現(xiàn)對象被KVO后留攒,其真正類型變?yōu)榱?code>NSKVONotifying_KVOObject類,已經(jīng)不是之前的類了剪侮。KVO會在運(yùn)行時動態(tài)創(chuàng)建一個新類杰标,將對象的isa指向新創(chuàng)建的類腔剂,并且將superClass指向原來的類KVOObject掸犬,新創(chuàng)建的類命名規(guī)則是NSKVONotifying_xxx的格式。KVO為了使其更像之前的類胜茧,還會將對象的class實例方法重寫呻顽,使其更像原類廊遍。

添加KVO之后喉前,由于修改了setName方法和setAge方法的IMP卵迂,所以打印這兩個方法的IMP见咒,也是一個新的地址下翎,新的實現(xiàn)在NSKVONotifying_KVOObject中宝当。

這種實現(xiàn)方式對業(yè)務(wù)代碼沒有侵入性郑口,可以在不影響KVOObject其他對象的前提下犬性,對單個對象進(jìn)行監(jiān)聽并修改其方法實現(xiàn)乒裆,在賦值時觸發(fā)KVO回調(diào)鹤耍。

在上面的代碼中還發(fā)現(xiàn)了_isKVOA方法稿黄,這個方法可以當(dāng)做使用了KVO的一個標(biāo)記,系統(tǒng)可能也是這么用的陵珍。如果我們想判斷當(dāng)前類是否是KVO動態(tài)生成的類互纯,就可以從方法列表中搜索這個方法留潦。

// 第一次
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

為什么上面調(diào)用runtimeobject_getClass函數(shù)殖卑,就可以獲取到真正的類呢?

調(diào)用object_getClass函數(shù)后其返回的是一個Class類型屹堰,Classobjc_class定義的一個typedef別名扯键,通過objc_class就可以獲取到對象的isa指針指向的Class荣刑,也就是對象的類對象。

由此可以知道爱只,object_getClass函數(shù)內(nèi)部返回的是對象的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需要是成對的,如果重復(fù)remove則會導(dǎo)致NSRangeException類型的Crash妇拯,如果忘記remove則會在觀察者釋放后再次接收到KVO回調(diào)時Crash宣赔。

蘋果官方推薦的方式是儒将,在init的時候進(jìn)行addObserver钩蚊,在deallocremoveObserver砰逻,這樣可以保證addremove是成對出現(xiàn)的踊东,是一種比較理想的使用方式闸翅。

錯誤檢查

如果傳入一個錯誤的keyPath并不會有錯誤提示坚冀。在調(diào)用KVO時需要傳入一個keyPath,由于keyPath是字符串的形式液南,如果屬性名發(fā)生改變后贺拣,字符串沒有改變?nèi)菀讓?dǎo)致Crash。對于這個問題啥辨,我們可以利用系統(tǒng)的反射機(jī)制將keyPath反射出來陨瘩,這樣編譯器可以在@selector()中進(jìn)行合法性檢查舌劳。

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

不能觸發(fā)回調(diào)

由于KVO的實現(xiàn)機(jī)制甚淡,如果調(diào)用成員變量進(jìn)行賦值资柔,是不會觸發(fā)KVO的贿堰。

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

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

但是羹与,如果通過KVC的方式調(diào)用賦值操作纵搁,則會觸發(fā)KVO的回調(diào)方法捐晶。這是因為KVCKVO有單獨的兼容山上,在KVC的賦值方法內(nèi)部,手動調(diào)用了willChangeValueForKey:didChangeValueForKey:方法妄帘。

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

重復(fù)添加

KVO進(jìn)行重復(fù)addObserver并不會導(dǎo)致崩潰,但是會出現(xiàn)重復(fù)執(zhí)行KVO回調(diào)方法的問題致盟。

[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

通過上面的測試代碼,并且在回調(diào)中打印object所對應(yīng)的Class來看尤慰,并不會重復(fù)創(chuàng)建子類馏锡,始終都是一個類。雖然重復(fù)addobserver不會立刻崩潰伟端,但是重復(fù)添加后在第一次調(diào)用removeObserver時杯道,就會立刻崩潰。從崩潰堆棧來看责蝠,和重復(fù)移除的問題一樣蕉饼,都是系統(tǒng)主動拋出的異常。

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.'

重復(fù)移除

KVO是不允許對一個keyPath進(jìn)行重復(fù)移除的达舒,如果重復(fù)移除贯底,則會導(dǎo)致崩潰胚想。例如下面的測試代碼牙躺。

[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"];

執(zhí)行上面的測試代碼后代虾,會造成下面的崩潰信息环形。從KVO的崩潰堆椢6樱可以看出來,系統(tǒng)為了實現(xiàn)KVOaddObserverremoveObserver,為NSObject添加了一個名為NSKeyValueObserverRegistrationCategory解取,KVOaddObserverremoveObserver的實現(xiàn)都在里面蔗包。

在移除KVO的監(jiān)聽時,系統(tǒng)會判斷當(dāng)前KVOkeyPath是否已經(jīng)被移除哨免,如果已經(jīng)被移除身辨,則主動拋出一個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是一種事件綁定機(jī)制的實現(xiàn)亮隙,在keyPath對應(yīng)的值發(fā)生改變后會回調(diào)對應(yīng)的方法。這種數(shù)據(jù)綁定機(jī)制,在對象關(guān)系很復(fù)雜的情況下,很容易導(dǎo)致不好排查的bug摔笤。例如keyPath對應(yīng)的屬性被調(diào)用的關(guān)系很復(fù)雜,就不太建議對這個屬性進(jìn)行KVO

自己實現(xiàn)KVO

除了上面的缺點,KVO還不支持block語法,需要單獨重寫父類方法愉粤,這樣加上addremove方法就會導(dǎo)致代碼很分散早歇。所以狈癞,我通過runtime簡單的實現(xiàn)了一個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) {
    // 處理業(yè)務(wù)邏輯
}];

self.object.name = @"lxz";

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

調(diào)用代碼很簡單屠缭,直接通過lxz_addObserver:originalSelector:callback:方法就可以添加KVO的監(jiān)聽奄喂,可以通過callbackblock接收屬性發(fā)生改變后的回調(diào)俯树。而且方法的keyPath接收的是一個SEL類型參數(shù),所以可以通過@selector()傳入?yún)?shù)時進(jìn)行方法合法性檢查,如果是未實現(xiàn)的方法直接就會報警告楚堤。

通過lxz_removeObserver:originalSelector:方法傳入觀察者和keyPath疫蔓,當(dāng)觀察者所有keyPath都移除后則從KVO中移除觀察者對象。

如果重復(fù)addObserverremoveObserver也沒事身冬,內(nèi)部有判斷邏輯衅胀。EasyKVO內(nèi)部通過weak對觀察者做引用,并不會影響觀察者的生命周期酥筝,并且在觀察者釋放后不會導(dǎo)致Crash拗小。一次add方法調(diào)用對應(yīng)一個block,如果觀察者監(jiān)聽多個keyPath屬性樱哼,不需要在block回調(diào)中判斷keyPath哀九。

KVOController

想在項目中安全便捷的使用KVO的話,推薦Facebook的一個KVO開源第三方框架KVOController搅幅。KVOController本質(zhì)上是對系統(tǒng)KVO的封裝阅束,具有原生KVO所有的功能,而且規(guī)避了原生KVO的很多問題茄唐,兼容blockaction兩種回調(diào)方式息裸。

源碼分析

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

Category中提供了KVOControllerKVOControllerNonRetaining兩個屬性呼盆,顧名思義第一個會對observer產(chǎn)生強(qiáng)引用,第二個則不會蚁廓。其內(nèi)部代碼就是創(chuàng)建FBKVOController對象的代碼访圃,并將創(chuàng)建出來的對象賦值給Category的屬性,直接通過這個Category就可以懶加載創(chuàng)建FBKVOController對象相嵌。

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

實現(xiàn)原理

FBKVOController中分為三部分腿时,_FBKVOInfo是一個私有類况脆,這個類的功能很簡單,就是以結(jié)構(gòu)化的形式保存FBKVOController所需的各個對象批糟,類似于模型類的功能格了。

還有一個私有類_FBKVOSharedController,這是FBKVOController框架實現(xiàn)的關(guān)鍵徽鼎。從命名上可以看出其是一個單例盛末,所有通過FBKVOController實現(xiàn)的KVO,觀察者都是它否淤。每次通過FBKVOController添加一個KVO時悄但,_FBKVOSharedController都會將自己設(shè)為觀察者,并在其內(nèi)部實現(xiàn)observeValueForKeyPath:ofObject:change:context:方法叹括,將接收到的消息通過blockaction進(jìn)行轉(zhuǎn)發(fā)算墨。

其功能很簡單,通過observe:info:方法添加KVO監(jiān)聽汁雷,并用一個NSHashTable保存_FBKVOInfo信息净嘀。通過unobserve:info:方法移除監(jiān)聽,并從NSHashTable中將對應(yīng)的_FBKVOInfo移除侠讯。這兩個方法內(nèi)部都會調(diào)用系統(tǒng)的KVO方法挖藏。

在外界使用時需要用FBKVOController類,其內(nèi)部實現(xiàn)了初始化以及添加和移除監(jiān)聽的操作厢漩。在調(diào)用添加監(jiān)聽方法后膜眠,其內(nèi)部會創(chuàng)建一個_FBKVOInfo對象,并通過一個NSMapTable對象進(jìn)行持有溜嗜,然后會調(diào)用_FBKVOSharedController來進(jìn)行注冊監(jiān)聽宵膨。

使用FBKVOController的話,不需要手動調(diào)用removeObserver方法炸宵,在被監(jiān)聽對象消失的時候辟躏,會在dealloc中調(diào)用remove方法。如果因為業(yè)務(wù)需求土全,可以手動調(diào)用remove方法捎琐,重復(fù)調(diào)用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的實現(xiàn)很簡單裹匙,所以這里就很簡單的講講瑞凑,具體實現(xiàn)可以去Github下載源碼仔細(xì)分析一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末概页,一起剝皮案震驚了整個濱河市籽御,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖篱蝇,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贺待,死亡現(xiàn)場離奇詭異徽曲,居然都是意外死亡零截,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門秃臣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涧衙,“玉大人,你說我怎么就攤上這事奥此』“ィ” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵稚虎,是天一觀的道長撤嫩。 經(jīng)常有香客問我,道長蠢终,這世上最難降的妖魔是什么序攘? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮寻拂,結(jié)果婚禮上程奠,老公的妹妹穿的比我還像新娘。我一直安慰自己祭钉,他們只是感情好瞄沙,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著慌核,像睡著了一般距境。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上垮卓,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天垫桂,我揣著相機(jī)與錄音,去河邊找鬼扒接。 笑死伪货,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的钾怔。 我是一名探鬼主播碱呼,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼宗侦!你這毒婦竟也來了愚臀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤矾利,失蹤者是張志新(化名)和其女友劉穎姑裂,沒想到半個月后馋袜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡舶斧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年欣鳖,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茴厉。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡泽台,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出矾缓,到底是詐尸還是另有隱情怀酷,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布嗜闻,位于F島的核電站蜕依,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏琉雳。R本人自食惡果不足惜样眠,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咐吼。 院中可真熱鬧吹缔,春花似錦、人聲如沸锯茄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肌幽。三九已至晚碾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間喂急,已是汗流浹背格嘁。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留廊移,地道東北人糕簿。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像狡孔,于是被迫代替她去往敵國和親懂诗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355