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