iOS的KVO实现剖析
KVO原理
对于KVO的原理,很多人都比较清楚了。大概是这样子的:
假定我们自己的类是Object
和它的对象 obj
, 当obj
发送addObserverForKeypath:keypath
消息后,系统会做3件事情:
- 动态创建一个
Object
的子类,名字可自定义假设叫做Object_KVONotify
。 - 同时,子类动态增加方法
setKeypath:
,动态添加的方法会绑定到一个c语言的函数。 - 调用
object_setClass
函数,将obj的class设置为Object_KVONotify
。
这样做会相当于建立如下结构:
//Object
@interface Object: NSObject
@property (nonatomic, copy) NSString *keypath;
@end
@implementation Object
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@" --- Object observeValueForKeyPath:%@ ofObject:%@ change:%@ context:%@", keyPath, object, change, context);
}
-(NSString *) description{
return [NSString stringWithFormat: @"This is %@ instance keypath = %@", self.class, self.keypath];
}
@end
//Object_KVONotify
@interface Object_KVONotify: Object
@end
static void dynamicSetKeyPath(id obj, SEL sel, id v){
... ...
}
@implementation Object_KVONotify
-(void) setKeypath:(NSString *)keypath{
dynamicSetKeyPath(self, @selector(setKeyPath:), keypath);
}
@end
//obj
Object *obj = [[Object alloc] init];
object_setClass(obj, Object_KVONotify.class);
//上面2句其实相当于
Object_KVONotify *obj = [[Object_KVONotify alloc] init]
这样一来,当我们调用
obj.keypath = "hello world";
实际上调用的是
dynamicSetKeyPath(self, @selector(setKeypath:), keypath);
此时dynamicSetKeyPath要做2件事情。
- 调用父类的
setKeyPath:
方法。 - 调用
observeValueForKeyPath
方法,触发回调。
所以 dynamicSetKeyPath
函数应该是这样的:
static void dynamicSetKeyPath(id obj, SEL sel, id v){
Method superMethod = class_getInstanceMethod(Object.class, sel);
((void (*)(id, Method, id))method_invoke)(obj, superMethod, v);
NSMutableDictionary * change = [[NSMutableDictionary alloc] init];
change[@"new"] = v;
[obj observeValueForKeyPath:@"keypath" ofObject:obj change:change context:nil];
}
或者这样
static void dynamicSetKeyPath(id obj, SEL sel, id v){
object_setClass(obj, Object.class);
[obj setValue: v forKey: @"keyPath"];
object_setClass(obj, Object_Notify.class);
[(Object *)obj observeValueForKeyPath: @"keypath" ofObject: objChange:@{@"new":v} context: nil];
}
在Object类中添加测试代码
+(void)test{
Object *obj = [[Object alloc] init];
obj.keypath = @"inited";
NSLog(@"%@", obj);
object_setClass(obj, Object_KVONotify.class);
obj.keypath = @"hello world";
}
调用测试代码,产生输入如下
This is Object instance keypath = inited
Object observeValueForKeyPath:keypath ofObject:This is Object_KVONotify instance keypath = hello world change:{
new = "hello world";
} context:(null)
上述过程就是KVO具体流程及测试代码。具体demo代码可以在这里找到。
KVO痛点
大家都知道,系统KVO略有点难用,主要因为这几点:
-
addObserver
后,不会在对象释放时,自动释放,我们只能在dealloc
中手动removeObserver
。这样在疏忽的情况下忘记removeObserver
可能会导致崩溃。另外,这个限制让我们无法在一个类中为其他类对象增加监听。 - 如果没有
addObserver
是不能removeObserver
的,会crash。 - 不支持block。
重新实现KVO
要重新实现KVO,根据KVO原理,我们需要创建一个增加监听的函数,并在函数内做到:
- 动态创建当前类的的子类,名字带固定后缀
_NotifyKVO
。 - 同时,子类动态增加方法
setXXXX:
,动态添加的方法会绑定到一个c语言的函数。 - 调用
object_setClass
函数,将obj的class设置为XXXX_NotifyKVO
。
首先我们创建一个NSObject的分类,添加创建KVO方法。
@implementation NSObject(BlockKVO)
-(void) addObserverForKeyPath:(NSString *)keyPath option:(NSKeyValueObservingOptions)option block:((^)(id obj, NSDictionary<NSKeyValueChangeKey,id> *change))block{
//self.blockKVO是通过associate与NSObject对象绑定的
//这样我们就把所有逻辑转移到了BlockKVO这个类中
[self.blockKVO addObserver:self forKeyPath:keyPath option:option block:block];
}
//这里覆盖了系统的KVO监听,里面仅仅调用了添加监听时的block
//这样做,可以让系统的KVO监听方法也能收到通过blockKVO添加的事件。
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
BlockKVOItem *item = [self.blockKVO itemWithKeyPath:keyPath];
if(item.block) {
item.block(self, keyPath, change);
}
}
@end
由于我们有很多参数和状态需要存储,而OC的category中保存属性是很麻烦的。
所以我们将创建一个新的类来处理所有的绑定逻辑,这就需要将所有参数及对象本身传递到这个类对象中。
请仔细阅读代码中的注释。
@implementation BlockKVO
//这里的参数obj就是需要kvo的对象,这个函数很重要,它做到了2件事
//1 为obj的class 创建一个以`_NotifyKVO`为后缀的子类
//2. 将obj的class指向XXX_NotifyKVO这个子类
//搞这么多幺蛾子的好处是实现了AOP,原有的类没有任何改变,obj仍然能访问原类的所有属性方法,而且obj可以通过扩展XXX_NotifyKVO方法,增加功能,也能修改原来类的行为,而不会影响原来类的结构。
-(void) initKVOClassWithObj:(id) obj{
if(self.srcClass == nil){
self.srcClass = [obj class];
//添加子类
NSString *dynamicClassName = [NSString stringWithFormat:@"%@_NotifyKVO", NSStringFromClass(self.srcClass)];
Class dynamicClass = NSClassFromString(dynamicClassName);
if(!dynamicClass) {
dynamicClass = objc_allocateClassPair(self.srcClass, dynamicClassName.UTF8String, 0);
objc_registerClassPair(dynamicClass);
}
self.dynamicClass = dynamicClass;
//将obj的类换成新创建的子类,否则不会调到dynamicSetKeyPath
object_setClass(obj, dynamicClass);
}
}
//这个方法是从原类中接收参数的,它只做2件事:
//1. 收到参数后,保存到observers字典中。
//2. 根据keyPath,添加setter方法。
-(void) addObserver: (id) obj forKeyPath:(NSString *)keyPath option:(NSKeyValueObservingOptions)option block:(void (^)(id obj, NSString *keyPath, NSDictionary<NSKeyValueChangeKey,id> *change))block{
[self initKVOClassWithObj:obj];
if(self.observers == nil){
self.observers = [[NSMutableDictionary alloc] init];
}
if(self.observers[keyPath] != nil){
return;
}
//添加方法
SEL methodSel = getSetSelector(keyPath);
class_addMethod(self.dynamicClass, methodSel, (IMP)dynamicSetKeyPath, "v@:@");
//保存
BlockKVOItem *item = [[BlockKVOItem alloc] init];
item.obj = obj;
item.keyPath = keyPath;
item.options = option;
item.block = block;
self.observers[keyPath] = item;
}
@end
我们会注意到class_addMethod
方法,最后一个参数是一个奇怪的字符串。这个字符串是为了表示所添加方法的类型,包括返回值类型和所有参数类型。
这东西又叫做 TypeEncoding,为啥有这个东西呢?
我们知道,OC是动态语言,它发送消息是要通过SEL去查找函数的,一旦找到了函数我们再去调用它就不是动态调用了,而是静态调用。
静态调用参数的数量和类型就很重要了。参数数量和类型其中任意一个对不上都会导致程序出错。
对于class_addMethod
函数来说,TypeEncoding
可以为添加的方法标记出它的返回值类型,参数个数和每个参数的类型。
上面的 "v@:@"表示的是,所添加的函数指针,返回值为void,有3个参数,第一个参数是id,第二个参数是SEL,第三个参数是id。很简单。
OC类的property
可以很多种类型,不仅仅是id
。所以如果想为不同类型调用 class_addMethod
,就要编写不同的TypeEncoding
。
列一下常用的TypeEncoding
:(更多细节查阅点这里TypeEncoding)
- "v@:q" => setKeyPath:(long long)
- "v@:c" => setKeyPath:(char)
- "v@:{CGSize=dd}" => setKeypPath:(CGSize)
通过上述代码,当我们的对象再调用setKeyPath:
方法的时候,实际上调用的是dynamicSetKeyPath
函数,我们看一下它的实现:
//这个函数的定义符合我们定义的typeencoding:"v@:@"
static void dynamicSetKeyPath(id obj, SEL sel, id value){
BlockKVO *blockKVO = [obj blockKVO];
//这里肯定不会为空,习惯性防御写法
if(blockKVO != nil) {
//根据SEL获取keyPath
NSString *keypath = getKeyPath(sel);
//获取到注册KVO时传入的参数,包括block啥的。
BlockKVOItem *item = [blockKVO itemWithKeyPath:keypath];
//这里先将obj的class恢复,否则会陷入循环
object_setClass(obj, blockKVO.srcClass);
//获取旧值
id oldValue = [obj valueForKey:keypath];
//设置新值
[obj setValue:value forKey: keypath];
//设置成子类
object_setClass(obj, blockKVO.dynamicClass);
//将oldValue和newValue通过observerValueForKeyPath:ofObject:change:方法通知给调用方(调用了block)
NSMutableDictionary * change = [[NSMutableDictionary alloc] init];
if (item.options & NSKeyValueObservingOptionNew){
change[@"old"] = oldValue;
}
if (item.options & NSKeyValueObservingOptionOld) {
change[@"new"] = value;
}
[obj observeValueForKeyPath:keypath ofObject:obj change:change context:nil];
}
}
这样,每次我们调用 setKeyPath: 的时候,前面注册的KVO监听的block都会被调用。
整个KVO流程就完成了。
当然,如果实现完整的KVO,上面的代码是不够的。你还需要解决如下问题:
- 不同类型的属性支持
-
setValue:forKey:
处理,weak变量可以通过这个函数处理。 - 线程安全(如果你只在主线程使用,则不必要)
- 动态创建类的释放
- 其他可能出现的问题
文内提到的所有代码已提交到github上,点这里查看完整demo。
也可以点击这里查看我在github上的所有repos。