博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
iOS的KVO实现剖析
阅读量:6528 次
发布时间:2019-06-24

本文共 7329 字,大约阅读时间需要 24 分钟。

KVO原理

对于KVO的原理,很多人都比较清楚了。大概是这样子的:

假定我们自己的类是Object和它的对象 obj, 当obj发送addObserverForKeypath:keypath消息后,系统会做3件事情:

  1. 动态创建一个Object的子类,名字可自定义假设叫做 Object_KVONotify
  2. 同时,子类动态增加方法 setKeypath:,动态添加的方法会绑定到一个c语言的函数。
  3. 调用 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
*)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@endstatic void dynamicSetKeyPath(id obj, SEL sel, id v){ ... ...}@implementation Object_KVONotify-(void) setKeypath:(NSString *)keypath{ dynamicSetKeyPath(self, @selector(setKeyPath:), keypath);}@end//objObject *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件事情。

  1. 调用父类的 setKeyPath: 方法。
  2. 调用 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 = initedObject observeValueForKeyPath:keypath ofObject:This is Object_KVONotify instance keypath = hello world change:{    new = "hello world";} context:(null)复制代码

上述过程就是KVO具体流程及测试代码。具体demo代码可以在找到。

KVO痛点

大家都知道,系统KVO略有点难用,主要因为这几点:

  1. addObserver后,不会在对象释放时,自动释放,我们只能在dealloc中手动removeObserver。这样在疏忽的情况下忘记removeObserver可能会导致崩溃。另外,这个限制让我们无法在一个类中为其他类对象增加监听。
  2. 如果没有addObserver是不能removeObserver的,会crash。
  3. 不支持block。

重新实现KVO

要重新实现KVO,根据KVO原理,我们需要创建一个增加监听的函数,并在函数内做到:

  1. 动态创建当前类的的子类,名字带固定后缀 _NotifyKVO
  2. 同时,子类动态增加方法 setXXXX:,动态添加的方法会绑定到一个c语言的函数。
  3. 调用 object_setClass 函数,将obj的class设置为XXXX_NotifyKVO

首先我们创建一个NSObject的分类,添加创建KVO方法。

@implementation NSObject(BlockKVO)-(void) addObserverForKeyPath:(NSString *)keyPath option:(NSKeyValueObservingOptions)option block:((^)(id obj, NSDictionary
*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
*)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
*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方法,最后一个参数是一个奇怪的字符串。这个字符串是为了表示所添加方法的类型,包括返回值类型和所有参数类型。

这东西又叫做 ,为啥有这个东西呢?

我们知道,OC是动态语言,它发送消息是要通过SEL去查找函数的,一旦找到了函数我们再去调用它就不是动态调用了,而是静态调用。

静态调用参数的数量和类型就很重要了。参数数量和类型其中任意一个对不上都会导致程序出错。

对于class_addMethod函数来说,TypeEncoding可以为添加的方法标记出它的返回值类型,参数个数和每个参数的类型。

上面的 "v@:@"表示的是,所添加的函数指针,返回值为void,有3个参数,第一个参数是id,第二个参数是SEL,第三个参数是id。很简单。

OC类的property可以很多种类型,不仅仅是id。所以如果想为不同类型调用 class_addMethod,就要编写不同的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,上面的代码是不够的。你还需要解决如下问题:

  1. 不同类型的属性支持
  2. setValue:forKey:处理,weak变量可以通过这个函数处理。
  3. 线程安全(如果你只在主线程使用,则不必要)
  4. 动态创建类的释放
  5. 其他可能出现的问题

文内提到的所有代码已提交到github上,。

也可以查看我在github上的所有repos。

转载地址:http://mwxbo.baihongyu.com/

你可能感兴趣的文章
关于unichar字符串的初始化
查看>>
oracle-xe手工创建数据库
查看>>
Cisco交换机 链路聚合
查看>>
我的友情链接
查看>>
好程序员HTML5大前端分享web前端面试题集锦二
查看>>
UG中卸载被占用的DLL
查看>>
eclipse 设置注释模板详解,与导入模板方法介绍总结
查看>>
Cocos2d-x3.2 文字显示
查看>>
估计下星期就能考科目二了
查看>>
20 Useful Commands for Linux Newbies
查看>>
轻松实现localStorage本地存储和本地数组存储
查看>>
mongodb group
查看>>
python+selenium自动化测试(二)
查看>>
(笔记 - 纯手敲)Spring的IOC和AOP 含GIT地址
查看>>
7-设计模式介绍
查看>>
让运维更高效:关于ECS系统事件
查看>>
J2EE分布式框架--单点登录集成方案
查看>>
跨域传递参数
查看>>
android 4.2的新特性layoutRtl,让布局自动从右往左显示
查看>>
iOS tableView 下拉列表的设计
查看>>