关于iOS开发:KVO原理分析

3次阅读

共计 17419 个字符,预计需要花费 44 分钟才能阅读完成。

介绍

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

正文完
 0