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。
iOS中你可能没有完全弄清楚的(二)自己实现一个KVO源码及解析相关推荐
- iOS开发之Masonry框架源码深度解析
Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让 ...
- 从源码角度解析Android中APK安装过程
从源码角度解析Android中APK的安装过程 1. Android中APK简介 Android应用Apk的安装有如下四种方式: 1.1 系统应用安装 没有安装界面,在开机时自动完成 1.2 网络下载 ...
- 超级签名源码_苹果iOS超级签名源码技术解析
随着苹果对于企业分发证书的频繁吊销和日益收紧,代签名行业也随之迭代出了黑科技,即所谓的超级签名源码. 签名原理 签名原理其实就一句话,使用了苹果提供给开发者的Ad-Hoc分发通道,把安装设备当做开发设 ...
- ios 移动社交 app 的demo 附:图文展示,客户端+服务器端源码
原帖地址:http://www.devdiv.com/iOS_iPhone-想学习移动社交APP的童鞋有福了,图文展示,附客户端,服务端源码.-thread-121444-1-1.html 想学习移动 ...
- 从源码角度解析线程池中顶层接口和抽象类
摘要:我们就来看看线程池中那些非常重要的接口和抽象类,深度分析下线程池中是如何将抽象这一思想运用的淋漓尽致的. 本文分享自华为云社区<[高并发]深度解析线程池中那些重要的顶层接口和抽象类> ...
- Android Studio App开发之网络通信中使用POST方式调用HTTP接口实现应用更新功能(附源码 超详细必看)
运行有问题或需要源码请点赞关注收藏后评论区留言~~~ 一.POST方式调用HTTP接口 POST方式把接口地址与请求报文分开,允许使用自定义的报文格式,由此扩大了该方式的应用场景.POST请求与GET ...
- 【Android App】人脸识别中借助摄像头和OpenCV实时检测人脸讲解及实战(附源码和演示 超详细)
需要全部代码请点赞关注收藏后评论区留言私信~~~ 一.借助摄像头实时检测人脸 与Android自带的人脸检测器相比,OpenCV具备更强劲的人脸识别功能,它可以通过摄像头实时检测人脸,实时检测的预览空 ...
- Android App接管手势处理TouchEvnet中单点触摸和多点触控的讲解及实战(附源码 超简单实用)
运行有问题或需要源码请点赞关注收藏后评论区留言~~~ 一.单点触摸 dispatchTouchEvent onInterceptTouchEvent onTouchEvent三个方法的输入参数都是手势 ...
- android ios滑动解锁效果,Android 高仿 IOS7 IPhone 解锁 Slide To Unlock 附源码
0. 源码下载 1. IPhone 解锁 效果图: 在最新的IOS7中,苹果更改了解锁方式,整个屏幕向右滑动都可以解锁,不再局限在一个小的矩形中.这种文字加亮移动的效果还是继承了下来.之前滑动最左边的 ...
最新文章
- RabbitMQ(四)交换机exchange
- 云原生已来,只是分布不均
- runat=server
- wxWidgets:wxToolBar 示例
- 《Linux内核分析》第一周笔记 计算机是如何工作的
- python3中的正则模块
- 【转载保存】HtmlUnit的使用
- openmv串口数据 串口助手_第三课使用pyserial来接收和发送串口数据
- 2019.01.13 bzoj4137: [FJOI2015]火星商店问题(线段树分治+可持久化01trie)
- 最强战队出炉,2020腾讯广告算法大赛圆满落幕
- 二分求浮点数的平方根
- P3244 [HNOI2015]落忆枫音
- 【Qt】解决“ QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to ‘/tmp/runtime-root‘ ”
- 可长期免费使用的国产PLC录波软件(数据采集软件)PLC-Recorder V2.0版新功能
- Bluetooth LMP介绍
- win7交换机共享宽带连接上网
- JAVA-日期类(Date、SimpleDateFormat)
- JVM 上篇(4):虚拟机栈
- Flutter仿网易云音乐 ---基础准备
- 向量旋转(或矢量旋转或坐标轴旋转)后xy坐标重定位(vivado+cordic ip核+matlab) - 适用于数学爱好者