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
@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件事情。
- 调用父类的
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的类换成新创建的子类,否则不会调到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,上面的代码是不够的。你还需要解决如下问题:
- 不同类型的属性支持
setValue:forKey:
处理,weak变量可以通过这个函数处理。- 线程安全(如果你只在主线程使用,则不必要)
- 动态创建类的释放
- 其他可能出现的问题
文内提到的所有代码已提交到github上,点这里查看完整demo。
也可以点击这里查看我在github上的所有repos。
相关文章:

你真的以为了解java.io吗 呕心沥血 绝对干货 别把我移出首页了
文章结构1 flush的使用场景2 一个java字节流,inputstream 和 outputstream的简单例子3 分别测试了可能抛出java.io.FileNotFoundException,java.io.FileNotFoundException: test (拒绝访问。),java.io.FileNotFoundException: test.txt (系统…

GitHub为所有人免费提供了所有核心功能-这就是您应该关心的原因
Just a couple of days ago, GitHub wrote a blog article stating that it is now free for teams. Heres the official blog article if youre interested. 就在几天前,GitHub写了一篇博客文章,指出它现在对团队免费。 如果您有兴趣,这是官…

什么是ObjCTypes?
先看一下消息转发流程: 在forwardInvocation这一步,你必须要实现一个方法: - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE(""); 该方法用于说明消息的返回值和参数类型。NSMethodSignature是方法签名&#x…

0基础JavaScript入门教程(一)认识代码
1. 环境: JavaScript简称js,后续我们将使用js来代替JavaScript。 认识代码前,需要安装js代码运行环境。 安装nodejs:在https://nodejs.org/zh-cn/ 下载LTS版本,然后安装安装visual studio code:https://…

junit、hamcrest、eclemma的安装与使用
1、junit的安装与使用 1.1 安装步骤 1)从http://www.junit.org/ 下载junit相应的jar包; 2) 在CLASSPATH中加入JAR包所在的路径,如E:\Java\jar\junit\junit-4.10.jar; 3) 将junit-4.10.jar加入到项目的lib文…

如何撰写将赢得客户青睐的自由职业者提案和免费模板
Your prospective client asks you to provide them with a quote. So you just send them the quote, right?您的潜在客户要求您提供报价。 所以您只给他们发送报价吧? Wrong.错误。 If you did, you would be missing out on a massive opportunity here.如果这…

2. 把一幅图像进行平移。
实验二 #include "cv.h" #include<stdio.h> #include "highgui.h" IplImage *PingYi(IplImage *src, int h0, int w0); int main(int argc, char** argv) {IplImage* pImg; //声明IplImage指针IplImage* pImgAfterMove;pImg cvLoadImage("601…

后台的代理nginx部署方法
软件包如下:nginx-1.10.0.tar.gznginx-http-concat-master.zipngx_cache_purge-2.3.tar.gzopenssl-1.0.2h.tar.gzpcre-8.39.tar.gzzlib-1.2.8.tar.gz ngin部署方法:上面的安装包都存放在/apps/svr/soft目录下:cd /apps/svr/softtar -zxf nginx-1.10.0.ta…
iOS中你可能没有完全弄清楚的(一)synthesize
1. 什么是synthesize synthesize中文意思是合成,代码中我们经常这样用。 interface Test: NSObject property (nonatomic, unsafe_unretained) int i; endimplementation Test synthesize i; end 复制代码 使用synthesize的2个步骤: 首先你要有在类声…
framer x使用教程_如何使用Framer Motion将交互式动画和页面过渡添加到Next.js Web应用程序
framer x使用教程The web is vast and its full of static websites and apps. But just because those apps are static, it doesnt mean they have to be boring. 网络非常庞大,到处都是静态的网站和应用。 但是,仅仅因为这些应用程序是静态的…

POJ 2429
思路:a/n*b/nlcm/gcd 所以这道题就是分解ans.dfs枚举每种素数情况。套Miller_Rabin和pollard_rho模板 1 //#pragma comment(linker, "/STACK:167772160")//手动扩栈~~~~hdu 用c交2 #include<cstdio>3 #include<cstring>4 #include<cstdlib…

iOS中你可能没有完全弄清楚的(二)自己实现一个KVO源码及解析
前几天写了一篇blog(点这里),分析了系统KVO可能的实现方式。并添加了简单代码验证。 既然系统KVO不好用,我们完全可以根据之前的思路,再造一个可以在项目中使用的KVO的轮子。 代码已经上传到github: https://github.…

js中的preventDefault与stopPropagation详解
1. preventDefault: 比如<a href"http://www.baidu.com">百度</a>,这是html中最基础的东西,起的作用就是点击百度链接到http://www.baidu.com,这是属于<a>标签的默认行为;preventDefault方法就是可以阻止它的默认行为的发生而发生其他…

angular过滤字符_如何使用Angular和Azure计算机视觉创建光学字符读取器
angular过滤字符介绍 (Introduction) In this article, we will create an optical character recognition (OCR) application using Angular and the Azure Computer Vision Cognitive Service. 在本文中,我们将使用Angular和Azure计算机视觉认知服务创建一个光学字…

javascript函数全解
0.0 概述 本文总结了js中函数相关的大部分用法,对函数用法不是特别清晰的同学可以了解一下。 1.0 简介 同其他语言不同的是,js中的函数有2种含义。 普通函数:同其他语言的函数一样,是用于封装语句块,执行多行语句的…
MYSQL explain详解[转载]
explain显示了mysql如何使用索引来处理select语句以及连接表。可以帮助选择更好的索引和写出更优化的查询语句。 虽然这篇文章我写的很长,但看起来真的不会困啊,真的都是干货啊!!!! 先解析一条sql语句&…

CodeForces 157A Game Outcome
A. Game Outcometime limit per test2 secondsmemory limit per test256 megabytesinputstandard inputoutputstandard outputSherlock Holmes and Dr. Watson played some game on a checkered board n n in size. During the game they put numbers on the boards squares…

我使用Python和Django在自己的网站上建立了一个会员专区。 这是我学到的东西。
I decided it was time to upgrade my personal website in order to allow visitors to buy and access my courses through a new portal. 我认为是时候升级我的个人网站了,以允许访问者通过新的门户购买和访问我的课程 。 Specifically, I wanted a place for v…

详解AFNetworking的HTTPS模块
0.0 简述 文章内容包括: AFNetworking简介ATS和HTTPS介绍AF中的证书验证介绍如何创建服务端和客户端自签名证书如何创建简单的https服务器对CA正式证书和自签名证书的各种情况进行代码验证 文中所涉及的文件和脚本代码请看这里。 1.0 AFNetworking简介 AFNetwo…

字符串专题:map POJ 1002
第一次用到是在‘校内赛总结’扫地那道题里面,大同小异 map<string,int>str 可以专用做做字符串的匹配之类的处理 string donser; str [donser] 自动存donser到map并且值加一,如果发现重复元素不新建直接加一, map第一个参数是key&…

【洛谷P1508】吃吃吃
题目背景 问世间,青春期为何物? 答曰:“甲亢,甲亢,再甲亢;挨饿,挨饿,再挨饿!” 题目描述 正处在某一特定时期之中的李大水牛由于消化系统比较发达,最近一直处…
前端和后端开发人员比例_前端开发人员vs后端开发人员–实践中的定义和含义
前端和后端开发人员比例Websites and applications are complex! Buttons and images are just the tip of the iceberg. With this kind of complexity, you need people to manage it, but which parts are the front end developers and back end developers responsible fo…

Linux 创建子进程执行任务
Linux 操作系统紧紧依赖进程创建来满足用户的需求。例如,只要用户输入一条命令,shell 进程就创建一个新进程,新进程运行 shell 的另一个拷贝并执行用户输入的命令。Linux 系统中通过 fork/vfork 系统调用来创建新进程。本文将介绍如何使用 fo…

metasploit-smb扫描获取系统信息
1.msfconsle 2.use auxiliary/scanner/smb/smb_version 3. msf auxiliary(smb_version) > set RHOSTS 172.16.62.1-200RHOSTS > 172.16.62.1-200msf auxiliary(smb_version) > set THREADS 100THREADS > 100msf auxiliary(smb_version) > run 4.扫描结果&#x…

算法(1)斐波那契数列
1.0 问题描述 实现斐波那契数列,求第N项的值 2.0 问题分析 斐波那契数列最简单的方法是使用递归,递归和查表法同时使用,可以降低复杂度。根据数列特点,同时进行计算的数值其实只有3个,所以可以使用3个变量循环递进计…

主键SQL教程–如何在数据库中定义主键
Every great story starts with an identity crisis. Luke, the great Jedi Master, begins unsure - "Who am I?" - and how could I be anyone important? It takes Yoda, the one with the Force, to teach him how to harness his powers.每个伟大的故事都始于…

算法(2)KMP算法
1.0 问题描述 实现KMP算法查找字符串。 2.0 问题分析 “KMP算法”是对字符串查找“简单算法”的优化。字符串查找“简单算法”是源字符串每个字符分别使用匹配串进行匹配,一旦失配,模式串下标归0,源字符串下标加1。可以很容易计算字符串查…

告别无止境的增删改查:Java代码生成器
对于一个比较大的业务系统,我们总是无止境的增加,删除,修改,粘贴,复制,想想总让人产生一种抗拒的心里。那有什么办法可以在正常的开发进度下自动生成一些类,配置文件,或者接口呢&…

Maven国内源设置 - OSChina国内源失效了,别更新了
Maven国内源设置 - OSChina国内源失效了,别更新了 原文:http://blog.csdn.net/chwshuang/article/details/52198932 最近在写一个Spring4.x SpringMVCMybatis零配置的文章,使用的源配的是公司的私有仓库,但是为了让其他人能够通过…
如何使用Next.js创建动态的Rick and Morty Wiki Web App
Building web apps with dynamic APIs and server side rendering are a way to give people a great experience both with content and speed. How can we use Next.js to easily build those apps?使用动态API和服务器端渲染来构建Web应用程序是一种使人们在内容和速度上都…