该文章属于刘小壮原创,转载请注明:刘小壮
介绍
KVO
全称KeyValueObserving
,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO
的实现机制,所以对属性才会发生作用,一般继承自NSObject
的对象都默认支持KVO
。
KVO
和NSNotificationCenter
都是iOS
中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO
是一对一的,而不一对多的。KVO
对被监听对象无侵入性,不需要手动修改其内部代码即可实现监听。
KVO
可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC
的mutableArrayValueForKey:
等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO
监听的方法。集合对象包含NSArray
和NSSet
。
使用
使用KVO
分为三个步骤
- 通过
addObserver:forKeyPath:options:context:
方法注册观察者,观察者可以接收keyPath
属性的变化事件回调。 - 在观察者中实现
observeValueForKeyPath:ofObject:change:context:
方法,当keyPath
属性发生改变后,KVO
会回调这个方法来通知观察者。 - 当观察者不需要监听时,可以调用
removeObserver:forKeyPath:
方法将KVO
移除。需要注意的是,调用removeObserver
需要在观察者消失之前,否则会导致Crash
。
注册
在注册观察者时,可以传入options
参数,参数是一个枚举类型。如果传入NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
表示接收新值和旧值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial
枚举。
还可以通过方法context
传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO
中的一种传值方式。
在调用addObserver
方法后,KVO
并不会对观察者进行强引用。所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash
。
监听
观察者需要实现observeValueForKeyPath:ofObject:change:context:
方法,当KVO
事件到来时会调用这个方法,如果没有实现会导致Crash
。change
字典中存放KVO
属性相关的值,根据options
时传入的枚举来返回。枚举会对应相应key
来从字典中取出值,例如有NSKeyValueChangeOldKey
字段,存储改变之前的旧值。
change
中还有NSKeyValueChangeKindKey
字段,和NSKeyValueChangeOldKey
是平级的关系,来提供本次更改的信息,对应NSKeyValueChange
枚举类型的value
。例如被观察属性发生改变时,字段为NSKeyValueChangeSetting
。
如果被观察对象是集合对象,在NSKeyValueChangeKindKey
字段中会包含NSKeyValueChangeInsertion
、NSKeyValueChangeRemoval
、NSKeyValueChangeReplacement
的信息,表示集合对象的操作方式。
其他触发方法
调用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
教程中有一个很经典的案例,通过KVO
在Model
和Controller
之间进行通信。
触发
主动触发
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触发
KVC
对KVO
有特殊兼容,当通过KVC
调用非属性的实例变量时,KVC
内部也会触发KVO
的回调,并通过NSKeyValueDidChange
和NSKeyValueWillChange
向上回调。
下面忽略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_KVOTest
的setObject
方法的实现,替换成_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
为什么上面调用runtime
的object_getClass
函数,就可以获取到真正的类呢?
调用object_getClass
函数后其返回的是一个Class
类型,Class
是objc_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
KVO
的addObserver
和removeObserver
需要是成对的,如果重复remove
则会导致NSRangeException
类型的Crash
,如果忘记remove
则会在观察者释放后再次接收到KVO
回调时Crash
。
苹果官方推荐的方式是,在init
的时候进行addObserver
,在dealloc
时removeObserver
,这样可以保证add
和remove
是成对出现的,是一种比较理想的使用方式。
错误检查
如果传入一个错误的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
的回调方法。这是因为KVC
对KVO
有单独的兼容,在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
的崩溃堆栈可以看出来,系统为了实现KVO
的addObserver
和removeObserver
,为NSObject
添加了一个名为NSKeyValueObserverRegistration
的Category
,KVO
的addObserver
和removeObserver
的实现都在里面。
在移除KVO
的监听时,系统会判断当前KVO
的keyPath
是否已经被移除,如果已经被移除,则主动抛出一个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
语法,需要单独重写父类方法,这样加上add
和remove
方法就会导致代码很分散。所以,我通过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
的监听,可以通过callback
的block
接收属性发生改变后的回调。而且方法的keyPath
接收的是一个SEL
类型参数,所以可以通过@selector()
传入参数时进行方法合法性检查,如果是未实现的方法直接就会报警告。
通过lxz_removeObserver:originalSelector:
方法传入观察者和keyPath
,当观察者所有keyPath
都移除后则从KVO
中移除观察者对象。
如果重复addObserver
和removeObserver
也没事,内部有判断逻辑。EasyKVO
内部通过weak
对观察者做引用,并不会影响观察者的生命周期,并且在观察者释放后不会导致Crash
。一次add
方法调用对应一个block
,如果观察者监听多个keyPath
属性,不需要在block
回调中判断keyPath
。
KVOController
想在项目中安全便捷的使用KVO
的话,推荐Facebook
的一个KVO
开源第三方框架KVOController。KVOController
本质上是对系统KVO
的封装,具有原生KVO
所有的功能,而且规避了原生KVO
的很多问题,兼容block
和action
两种回调方式。
源码分析
从源码来看还是比较简单的,主要分为NSObject
的Category
和FBKVOController
两部分。
在Category
中提供了KVOController
和KVOControllerNonRetaining
两个属性,顾名思义第一个会对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:
方法,将接收到的消息通过block
或action
进行转发。
其功能很简单,通过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下载源码仔细分析一下。