<简书 — 刘小壮> https://www.jianshu.com/p/badf5cac0130git
咱们在工做中常常会用到
KVO
,可是系统原生的KVO
并很差用,很容易致使Crash
。并且编写代码时,须要编写大量KVO
相关的代码,因为不支持block
的形式,代码会写的很分散。github本篇文章对
KVO
的实现原理进行了详细的分析,而且简单的实现了一个KVO
,来当作技术交流。因为系统提供的KVO
存在不少问题,在文章的最下面给出了解决方案。安全
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
枚举。ui
还能够经过方法context
传入任意类型的对象,在接收消息回调的代码中能够接收到这个对象,是KVO中的一种传值方式。atom
在调用addObserver
方法后,KVO
并不会对观察者进行强引用,因此须要注意观察者的生命周期,不然会致使观察者被释放带来的Crash
。spa
观察者须要实现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
的addObserver
和removeObserver
须要是成对的,若是重复remove
则会致使NSRangeException
类型的Crash
,若是忘记remove
则会在观察者释放后再次接收到KVO
回调时Crash
。
苹果官方推荐的方式是,在init
的时候进行addObserver
,在dealloc
时removeObserver
,这样能够保证add
和remove
是成对出现的,是一种比较理想的使用方式。
KVO
在属性发生改变时的调用是自动的,若是想要手动控制这个调用时机,或想本身实现KVO
属性的调用,则能够经过KVO
提供的方法进行调用。
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
复制代码
能够看到调用KVO
主要依靠两个方法,在属性发生改变以前调用willChangeValueForKey:
方法,在发生改变以后调用didChangeValueForKey:
方法。
若是想控制当前对象的自动调用过程,也就是由上面两个方法发起的KVO
调用,则能够重写下面方法。方法返回YES
则表示能够调用,若是返回NO
则表示不能够调用。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
复制代码
KVO
是经过isa-swizzling
技术实现的(这句话是整个KVO
实现的重点)。在运行时根据原类建立一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa
指向中间类。而且将class
方法重写,返回原类的Class
。因此苹果建议在开发中不该该依赖isa
指针,而是经过class
实例方法来获取对象类型。
为了测试KVO
的实现方式,咱们加入下面的测试代码。首先建立一个KVOObject
类,并在里面加入两个属性,而后重写description
方法,并在内部打印一些关键参数。
@interface KVOObject : NSObject
@property (nonatomic, copy ) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation KVOObject
- (NSString *)description {
NSLog(@"object address : %p \n", self);
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
先后分别打印两个对象的关键信息,看KVO
先后有什么变化。
@property (nonatomic, strong) KVOObject *object1;
@property (nonatomic, strong) KVOObject *object2;
self.object1 = [[KVOObject alloc] init];
self.object2 = [[KVOObject alloc] init];
[self.object1 description];
[self.object2 description];
[self.object1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object1 description];
[self.object2 description];
self.object1.name = @"lxz";
self.object1.age = 20;
复制代码
下面是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 : 0x604000237920
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 address : 0x604000237920
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
复制代码
咱们发现对象被KVO
后,其真正类型变为了NSKVONotifying_KVOObject
类,已经不是以前的类了。KVO
会在运行时动态建立一个新类,将对象的isa
指向新建立的类,新类是原类的子类,命名规则是NSKVONotifying_xxx
的格式。KVO
为了使其更像以前的类,还会将对象的class
实例方法重写,使其更像原类。
在上面的代码中还发现了_isKVOA
方法,这个方法能够当作使用了KVO
的一个标记,系统可能也是这么用的。若是咱们想判断当前类是不是KVO
动态生成的类,就能够从方法列表中搜索这个方法。
KVO
会重写keyPath
对应属性的setter
方法,没有被KVO
的属性则不会重写其setter
方法。在重写的setter
方法中,修改值以前会调用willChangeValueForKey:
方法,修改值以后会调用didChangeValueForKey:
方法,这两个方法最终都会被调用到observeValueForKeyPath:ofObject:change:context:
方法中。
为何上面调用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
}
复制代码
苹果提供的KVO
自身存在不少问题,首要问题在于,KVO
若是使用不当很容易崩溃。例如重复add
和remove
致使的Crash
,Observer
被释放致使的崩溃,keyPath
传错致使的崩溃等。
在调用KVO
时须要传入一个keyPath
,因为keyPath
是字符串的形式,因此其对应的属性发生改变后,字符串没有改变容易致使Crash
。咱们能够利用系统的反射机制将keyPath
反射出来,这样编译器能够在@selector()
中进行合法性检查。
NSStringFromSelector(@selector(isFinished))
复制代码
KVO
是一种事件绑定机制的实现,在keyPath
对应的值发生改变后会回调对应的方法。这种数据绑定机制,在对象关系很复杂的状况下,很容易致使很差排查的bug
。例如keyPath
对应的属性被调用的关系很复杂,就不太建议对这个属性进行KVO
,能够想一下RAC
的信号脑补一下。
除了上面的缺点,KVO
还不支持block
语法,须要单独重写父类方法,这样加上add
和remove
方法就会致使代码很分散。因此,我经过runtime
简单的实现了一个KVO
,源码放在个人Github
上,叫作EasyKVO。
self.object1 = [[KVOObject alloc] init];
[self.object1 lxz_addObserver:self originalSelector:@selector(name) callback:^(id observedObject, NSString *observedKey, id oldValue, id newValue) {
// callback
}];
self.object1.name = @"lxz";
[self.object1 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
。
须要注意的是,EasyKVO
只是作技术交流,不建议在项目中使用。由于KVO
实现须要考虑不少状况,继承关系、多个观察者等不少问题。
想在项目中安全便捷的使用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下载源码仔细分析一下。