iOS中你可能没有完全弄清楚的(二)自己实现一个KVO源码及解析
前几天写了一篇blog(点这里),分析了系统KVO可能的实现方式。并添加了简单代码验证。
既然系统KVO不好用,我们完全可以根据之前的思路,再造一个可以在项目中使用的KVO的轮子。
代码已经上传到github: https://github.com/hardman/AWSimpleKVO。
看了觉得有帮助的同学,可以点一下github
的star
。
1. 功能介绍
支持如下功能:
- 支持
block
回调 - 支持一次添加多参数
- 不需要
removeObserver
,监听会随对象自动删除 - 可设置忽略重复值
- 线程安全
- 仅支持下列类型的监听:
- 所有OC对象
- 基本数据类型:
char
,int
,short
,long
,long long
,unsigned char
,unsigned int
,unsigned short
,unsigned long
,unsigned long long
,float
,double
,bool
- 结构体:
CGSize
,CGPoint
,CGRect
,CGVector
,CGAffineTransform
,UIEdgeInsets
,UIOffset
不支持如下功能:
- 仅支持
NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
,不支持其他options - 不支持多级
keyPath
,如"a.b.c"
- 不支持
weak
变量自动置空监听 context
需使用OC对象- 不支持只有
setter
没有getter
的属性
1.1 引用方法
首先在你的工程Podfile
中添加:
target 'TargetName' dopod 'AWSimpleKVO'
end
然后在命令行中执行:
pod install
打开你的 ProjectName.xcworkspace
就可以使用了。
1.2 使用方法
api
同系统KVO
基本一致,可以看源码demo
中的例子,点这里看demo。
//1. 首先引入头文件
#import <AWSimpleKVO/NSObject+AWSimpleKVO.h>@interface TestSimpleKVO()
@property (nonatomic, unsafe_unretained) int i;
@property (atomic, strong) NSObject *o;
@property (nonatomic, copy) NSString *s;
@property (nonatomic, weak) NSObject *w;
@end@implementation TestSimpleKVO+(void) testCommon{TestSimpleKVO *testObj = [[TestSimpleKVO alloc] init];///1. 添加监听NSLog(@"--before 添加监听");[testObj awAddObserverForKeyPath:@"i" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil block:^(NSObject *observer, NSString *keyPath, NSDictionary *change, void *context) {NSLog(@"keyPath=%@, changed=%@", keyPath, change);}];[testObj awAddObserverForKeyPaths:@[@"o", @"s", @"w"] options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil block:^(NSObject *observer, NSString *keyPath, NSDictionary *change, void *context) {NSLog(@"keyPath=%@, changed=%@", keyPath, change);}];NSLog(@"--after 添加监听");testObj.i = 12030;testObj.o = [[NSObject alloc]init];testObj.s = @"66666";///2. setValue:forKey:NSLog(@"--before setValue:ForKey");[testObj setValue:@12304 forKey:@"i"];NSLog(@"--after setValue:ForKey");///3. 忽略相同赋值NSLog(@"--before awSimpleKVOIgnoreEqualValue to YES");testObj.awSimpleKVOIgnoreEqualValue = YES;[testObj setValue:@12304 forKey:@"i"];[testObj setValue:@12304 forKey:@"i"];NSLog(@"--after awSimpleKVOIgnoreEqualValue to YES");NSLog(@"--before awSimpleKVOIgnoreEqualValue to NO");testObj.awSimpleKVOIgnoreEqualValue = NO;[testObj setValue:@12304 forKey:@"i"];[testObj setValue:@12304 forKey:@"i"];NSLog(@"--after awSimpleKVOIgnoreEqualValue to NO");///4. 移除监听NSLog(@"--before 移除监听");[testObj awRemoveObserverForKeyPath:@"o" context:nil];testObj.o = [[NSObject alloc] init];NSLog(@"--after 移除监听");
}@end
2. 代码解析
2.1 基本思路
代码的基本思路同我之前写的这篇文章 => iOS的KVO实现剖析。
指导思想如下:
- 收集传入参数,保存在字典中
- 动态创建当前类的子类,并把当前对象的
class
设为子类。这样我们调用对象的方法时,会先在子类中查找 - 为子类添加当前监听参数的
setter
方法,这个setter
方法指向一个我们自己编写的C函数。这样我们调用对象的setter
方法时,就会调用我们自定义的C函数 - 在C函数中,调用父类的相同的
setter
方法。然后调用通知block
2.2 具体实现细节
2.2.1 收集参数
添加属性变化监听是调用的 NSObject(AWSimpleKVO)
这个扩展里的方法awAddObserverForKeyPath:options:context:block:
。在它内部,其实调用的是AWSimpleKVO
的同名方法。
我们主要功能都是在类AWSimpleKVO
中实现的,NSObject(AWSimpleKVO)
只是提供了一个包装。
//AWSimpleKVO.m-(BOOL)addObserverForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block:(void (^)(NSObject *observer, NSString *keyPath, NSDictionary *change, void *context)) block{///1. 检查参数...///生成并保存itemAWSimpleKVOItem *item = nil;@synchronized(self){if ([self.itemContainer itemWithKeyPath:keyPath context:context] != nil) {return NO;}item = [self _genKvoItemWithKeyPath:keyPath options:options context:context block:block];[self.itemContainer addItem:item forKeyPath:keyPath context:context];}///生成return [self _addClassAndMethodForItem:item];
}
从上述代码中可以看出,我们通过 _genKvoItemWithKeyPath
方法生成了一个AWSimpleKVOItem
的实例item
,然后将item
存入itemContainer
中。
AWSimpleKVOItem
会将keyPath
,options
, context
, block
这些参数保存起来,然后放入itemContainer
中。
@interface AWSimpleKVOItem: NSObject///监听的key
@property (nonatomic, copy) NSString *keyPath;
///context用于区分监听者,可实现多处监听同一个对象的同一个key
@property (nonatomic, strong) NSMutableDictionary *contextToBlocks;///保存的旧值
@property (nonatomic, strong) id oldValue;///key的类型
@property (nonatomic, unsafe_unretained) AWSimpleKVOSupporedIvarType ivarType;
///key的typeCoding
@property (nonatomic, copy) NSString *ivarTypeCode;//监听选项
@property (nonatomic, unsafe_unretained) NSKeyValueObservingOptions options;... ...@end
从AWSimpleKVOItem
的代码中可以看出,这个类没有方法,全是属性,它就是一个存储数据的model类。当然除了传入参数之外,这个类也会存储一些计算过程中生成的变量。
AWSimpleKVOItemContainer
仅仅对NSDictionary
的一个封装。
下面的伪代码描述了AWSimpleKVOItemContainer
和AWSimpleKVOItem
中的contextToBlocks
的结构。
AWSimpleKVOItemContainer.observerDict = {keyPath0: AWSimpleKVOItem0 {contextToBlocks:{context0: notifyBlock0,context1: notifyBlock1... ... }},keyPath1: AWSimpleKVOItem1 {contextToBlocks:{context0: notifyBlock0,context1: notifyBlock1... ... }},... ...
}
从上面的结构可知,一个keyPath
可以注册多个监听,可使用context
区分不同的block
。
这就是说,我们可以为同一个对象,同一个keyPath
添加多个监听,只要令context
不同即可。
我们可以从AWSimpleKVOItemContainer
中获取到已经添加了监听的所有items
。
2.2.2 动态添加子类
添加子类的代码很简单,最主要的代码只需要2行:objc_allocateClassPair
和 objc_registerClassPair
。
-(Class) addChildObserverClass:(Class) c keyPath:(NSString *)keyPath item:(AWSimpleKVOItem *)item {Class classNew = self.simpleKVOChildClass;if (!classNew) {@synchronized(self.class) {classNew = self.simpleKVOChildClass;if(!classNew) {NSString *classNewName = self.simpleKVOChildClassName;classNew = objc_allocateClassPair(c, classNewName.UTF8String, 0);objc_registerClassPair(classNew);self.simpleKVOChildClass = classNew;self.simpleKVOSuperClass = c;}}}... ...return classNew;
}
添加子类之后,我们需要将当前对象的class
设置为新创建的子类。这需要调用 object_setClass
方法。
-(void) safeThreadSetClass:(Class) cls {if(cls == self.safeThreadGetClass) {return;}@synchronized(self.obj) {object_setClass(self.obj, cls);}
}
这样我们的对象,如果再调用setter
方法时,就会先在我们创建的子类中查找方法了。
2.2.3 为子类添加setter方法
-(Class) addChildObserverClass:(Class) c keyPath:(NSString *)keyPath item:(AWSimpleKVOItem *)item {... ...BOOL needReplace = YES;Method currMethod = class_getInstanceMethod(classNew, item._setSel);if (currMethod != NULL) {IMP currIMP = method_getImplementation(currMethod);needReplace = currIMP != item._childMethod;}if (needReplace) {class_replaceMethod(classNew, item._setSel, item._childMethod, item._childMethodTypeCoding.UTF8String);}... ...return classNew;
}
由于runtime.h
中没有找到类似removeMethod
或deleteMethod
方法,考虑重入等因素。
我们可以使用replaceMethod
来代替addMethod
和removeMethod
的功能。
上面的_childMethod
即我们子类setter
方法所指向的C函数。
_childMethod
生成和 replaceMethod
的使用,都需要对iOS
的TypeEncoding
有所了解,可以看这里的介绍。
2.2.4 setter方法对应的C函数
C函数要做2件事:
- 调用父类的
setter
方法 - 调用
AWSimpleKVOItem
中保存的block
我们的代码中为不同的变量类型分别添加了不同的c函数。它们的逻辑相同,只是参数类型不同。
我们这里只看keyPath
类型为OC
对象的函数实现。
///当key类型为对象(id)时,key的setter方法会指向此方法。
static void _childSetterObj(id obj, SEL sel, id v) {AWSimpleKVOItem *item = _childSetterKVOItem(obj, sel);if([obj awSimpleKVOIgnoreEqualValue] && item.oldValue == v ) {return;}id value = v;if (item.isCopy) {value = [value copy];}if (!item.isNonAtomic) {@synchronized(item) {((void (*)(id, SEL, id))item._superMethod)(obj, sel, value);}}else{((void (*)(id, SEL, id))item._superMethod)(obj, sel, value);}_childSetterNotify(item, obj, item.keyPath, value);
}
最主要的代码就是
///调用父类方法
((void (*)(id, SEL, id))item._superMethod)(obj, sel, value);
///触发为keyPath添加的所有block回调
_childSetterNotify(item, obj, item.keyPath, value);
3. 总结
到这里,我们就完成了一个自己写的KVO,它的功能和系统KVO完全相同,完全可以替代系统的KVO使用。
如果遇到问题,可以留言一起讨论。
如果觉得对自己有帮助,或者学到了东西,请帮忙点赞转发+评论,github+star。
相关文章:

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应用程序是一种使人们在内容和速度上都…

安装部署Spark 1.x Standalone模式集群
Configuration spark-env.sh HADOOP_CONF_DIR/opt/data02/hadoop-2.6.0-cdh5.4.0/etc/hadoop JAVA_HOME/opt/modules/jdk1.7.0_67 SCALA_HOME/opt/modules/scala-2.10.4 ####################################################### #主节点 …

算法(3)简单四则运算
1.0 问题描述 实现10以内四则运算(只包含数字,*/和小括号) 2.0 问题分析 四则运算使用“后缀表达式”算法来计算,后缀表达式可以无需考虑运算符优先级,直接从左至右依次计算。问题分解成2部分,一是将“中…

调用短信接口,先var_dump()看数据类型是object需要json_decode(json_encode( $resp),true)转换成array...
返回的数据.先看类型,如果是object类型 先json_encode, 再json_decode,加true 转换成数组 $resp $c->execute($req); var_dump($resp); object(stdClass)#12 (2) { ["result"]> object(stdClass)#13 (3) { ["err_code"]> string(1) "0"…

nlp文本数据增强_如何使用Texthero为您的NLP项目准备基于文本的数据集
nlp文本数据增强Natural Language Processing (NLP) is one of the most important fields of study and research in today’s world. It has many applications in the business sector such as chatbots, sentiment analysis, and document classification.Preprocessing an…

R语言-基础解析
二、操作基础%%取余%/%整数除法(1)eigen(...)求解方阵的特征值和特征向量(2)solve(D,A)求解DXA(3)data<-list(...)取里面的对象data[["列名称"]];data[[下标]];data$列名称(4)unlist(列表对象)把列表对象转化为向量对象(5)names(数据框)读取…

算法(4)数据结构:堆
1.0 问题描述 实现数据结构:堆。 2.0 问题分析 堆一般使用数组来表示,其中某个节点下标i的两个子节点的下标为 2i1 和 2i2。堆是一棵完全二叉树。堆有3种基本操作:创建,插入,删除。这3种操作都需要通过“调整堆”的…

cookie 和session 的区别详解
转自 https://www.cnblogs.com/shiyangxt/archive/2008/10/07/1305506.html 这些都是基础知识,不过有必要做深入了解。先简单介绍一下。 二者的定义: 当你在浏览网站的时候,WEB 服务器会先送一小小资料放在你的计算机上,Cookie 会…

如何设置Java Spring Boot JWT授权和认证
In the past month, I had a chance to implement JWT auth for a side project. I have previously worked with JWT in Ruby on Rails, but this was my first time in Spring. 在过去的一个月中,我有机会为辅助项目实现JWT auth。 我以前曾在Ruby on Rails中使用…

算法(5)哈希表
1.0 问题描述 实现数据结构:哈希表。 2.0 问题分析 哈希表可以看作我们经常使用的字典(swift)或对象(js),可以让一个key&value对一一对应,可以快速根据key找到value。哈希表内部使用数组…

《面向对象程序设计》c++第五次作业___calculator plus plus
c第五次作业 Calculator plusplus 代码传送门 PS:这次作业仍然orz感谢一位同学与一位学长的windows帮助,同时再次吐槽作业对Mac系统用户的不友好。(没朋友千万别用Mac!!!) 还有想吐槽作业对规范的要求大大超…

联合体union和大小端(big-endian、little-endian)
1.联合体union的基本特性——和struct的同与不同union,中文名“联合体、共用体”,在某种程度上类似结构体struct的一种数据结构,共用体(union)和结构体(struct)同样可以包含很多种数据类型和变量。在成员完全相同的情况下,struct比…

前端面试的作品示例_如何回答任何技术面试问题-包括示例
前端面试的作品示例Technical interviews can be extremely daunting. From the beginning of each question to the end, its important to know what to expect, and to be aware of the areas you might be asked about. 技术面试可能会非常艰巨。 从每个问题的开始到结束&a…