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<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
@endstatic 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件事情。

  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 = inited
Object 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<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的类换成新创建的子类,否则不会调到dynamicSetKeyPathobject_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获取keyPathNSString *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上,点这里查看完整demo。

也可以点击这里查看我在github上的所有repos。

iOS的KVO实现剖析相关推荐

  1. iOS视频播放器之ZFPlayer剖析

    2019独角兽企业重金招聘Python工程师标准>>> 引言 本文主要针对ZFPlayer的功能实现来剖析,以及总结一下大家遇到的问题和解决方案 首先ZFPlayer现在拥有的功能: ...

  2. iOS下KVO使用过程中的陷阱

    KVO,全称为Key-Value Observing,是iOS中的一种设计模式,用于检测对象的某些属性的实时变化情况并作出响应.网上广为流传普及的一个例子是利用KVO检测股票价格的变动,例如这里.这个 ...

  3. Ios KVC KVO

    一.KVC与KVO *"KVC":key value Coding(键值编码) *目的:间接的修改或获取对象的属性,降低程序(类与类)之间的耦合度. *"KVO" ...

  4. ios 监听数组个数的变化_【iOS】KVO方式监听数组的变化动态刷新tableView

    写作本文来由:   iOS默认不支持对数组的KVO,因为普通方式监听的对象的地址的变化,而数组地址不变,而是里面的值发生了改变 整个过程需要三个步骤 (与普通监听一致) /* *第一步建立观察者及观察 ...

  5. iOS底层-KVO分析与自定义

    KVO分析与自定义 背景 准备 KVO一些细节 KVO探索分析 KVO 底层原理 小结: KVO自定义 自定义KVO要知道: 1,KVO是对setter方法进行观察,过滤实例方法 2,添加KVO(核心 ...

  6. 8 iOS中KVO 的本质

    前言本质 Automatic key-value observing is implemented using a technique called isa-swizzling 这计划的意思就是 自动 ...

  7. iOS中KVO模式的解析与应用

    最近老翁在项目中多处用到了KVO,深感这种模式的好处.现总结如下: 一.概述 KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知.简单 ...

  8. iOS中你可能没有完全弄清楚的(二)自己实现一个KVO源码及解析

    前几天写了一篇blog(点这里),分析了系统KVO可能的实现方式.并添加了简单代码验证. 既然系统KVO不好用,我们完全可以根据之前的思路,再造一个可以在项目中使用的KVO的轮子. 代码已经上传到gi ...

  9. [iOS]-KVO+KVC

    目录: 参考的博客: KVO 什么是KVO KVO的基本使用 KVO使用注意事项 手动调用KVO KVO本质 NSKVONotifying_Apple内部实现 setter的实现不同 KVO部分相关问 ...

最新文章

  1. (转)java 中的try catch finally 语句中含有return语句的执行情况(总结版)
  2. Flask之请求钩子
  3. 图像识别python cnn_MINIST深度学习识别:python全连接神经网络和pytorch LeNet CNN网络训练实现及比较(一)...
  4. 中国移动互联网趋势报告:教育、金融类App留存率更高
  5. 二级c语言上机编程技巧,二级C语言上机编程题技巧总结
  6. 生物科学数据分析和数据管理本体论
  7. 计算机暑期实践相关内容,计算机暑假社会实践报告5000范文
  8. selenium自动化测试框架之PO设计模式
  9. php中$t=date()函数参数意义及时间更改
  10. 初开:什么是系统思考
  11. 查看Jetson系列产品JetPack的版本信息
  12. macOS上专业的5款HTML文本代码编辑器
  13. 手机地图导航哪个好?手机导航地图推荐
  14. initialization on demand holder
  15. Java源文件编译出错:类文件包含错误的类 请删除该文件或确保文件位于正确的类路径子目录中
  16. 生成模型与判别模型详解
  17. 【渝粤题库】广东开放大学 经济法实务 形成性考核
  18. 如何将扫描PDF文件转换成word,两个超简单的方法一看就会
  19. 关于OnCreate和OnDraw
  20. HostMonitor监控软件

热门文章

  1. python全栈开发基础【第十七篇】面向对象反射和内置方法
  2. delphi自定义事件处理
  3. 3.分支结构与循环结构
  4. Windows搭建以太坊的私有链环境
  5. RadASM的测试工程!
  6. Adding a QR Code Reader in Flex on Android
  7. 电路实验1-电容充放电
  8. 给网站管理员的建议:创建可利用的、可抓取的网站
  9. C# Idioms: Enum还是Enum Class(枚举类)
  10. 从DataView中生成Excel报表的方案(C#)