KVO原理分析

2021年11月23日 阅读数:13
这篇文章主要向大家介绍KVO原理分析,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

介绍

KVO全称KeyValueObserving,是苹果提供的一套事件通知机制。容许对象监听另外一个对象特定属性的改变,并在改变时接收到事件。因为KVO的实现机制,因此对属性才会发生做用,通常继承自NSObject的对象都默认支持KVOc++

KVONSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO对被监听对象无侵入性,不须要手动修改其内部代码便可实现监听。segmentfault

KVO能够监听单个属性的变化,也能够监听集合对象的变化。经过KVCmutableArrayValueForKey:等方法得到代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArrayNSSet安全

使用

使用KVO分为三个步骤app

  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。例如被观察属性发生改变时,字段为NSKeyValueChangeSettingatom

若是被观察对象是集合对象,在NSKeyValueChangeKindKey字段中会包含NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement的信息,表示集合对象的操做方式。spa

其余触发方法

调用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下载源码仔细分析一下。