前几天写了一篇blog(点这里),分析了系统KVO可能的实现方式。并添加了简单代码验证。

既然系统KVO不好用,我们完全可以根据之前的思路,再造一个可以在项目中使用的KVO的轮子。

代码已经上传到github: https://github.com/hardman/AWSimpleKVO。

看了觉得有帮助的同学,可以点一下githubstar

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

不支持如下功能:

  • 仅支持 NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld,不支持其他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会将keyPathoptions, 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的一个封装。

下面的伪代码描述了AWSimpleKVOItemContainerAWSimpleKVOItem中的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_allocateClassPairobjc_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中没有找到类似removeMethoddeleteMethod方法,考虑重入等因素。
我们可以使用replaceMethod来代替addMethodremoveMethod的功能。

上面的_childMethod即我们子类setter方法所指向的C函数。

_childMethod 生成和 replaceMethod的使用,都需要对iOSTypeEncoding有所了解,可以看这里的介绍。

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源码及解析相关推荐

  1. iOS开发之Masonry框架源码深度解析

    Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让 ...

  2. 从源码角度解析Android中APK安装过程

    从源码角度解析Android中APK的安装过程 1. Android中APK简介 Android应用Apk的安装有如下四种方式: 1.1 系统应用安装 没有安装界面,在开机时自动完成 1.2 网络下载 ...

  3. 超级签名源码_苹果iOS超级签名源码技术解析

    随着苹果对于企业分发证书的频繁吊销和日益收紧,代签名行业也随之迭代出了黑科技,即所谓的超级签名源码. 签名原理 签名原理其实就一句话,使用了苹果提供给开发者的Ad-Hoc分发通道,把安装设备当做开发设 ...

  4. ios 移动社交 app 的demo 附:图文展示,客户端+服务器端源码

    原帖地址:http://www.devdiv.com/iOS_iPhone-想学习移动社交APP的童鞋有福了,图文展示,附客户端,服务端源码.-thread-121444-1-1.html 想学习移动 ...

  5. 从源码角度解析线程池中顶层接口和抽象类

    摘要:我们就来看看线程池中那些非常重要的接口和抽象类,深度分析下线程池中是如何将抽象这一思想运用的淋漓尽致的. 本文分享自华为云社区<[高并发]深度解析线程池中那些重要的顶层接口和抽象类> ...

  6. Android Studio App开发之网络通信中使用POST方式调用HTTP接口实现应用更新功能(附源码 超详细必看)

    运行有问题或需要源码请点赞关注收藏后评论区留言~~~ 一.POST方式调用HTTP接口 POST方式把接口地址与请求报文分开,允许使用自定义的报文格式,由此扩大了该方式的应用场景.POST请求与GET ...

  7. 【Android App】人脸识别中借助摄像头和OpenCV实时检测人脸讲解及实战(附源码和演示 超详细)

    需要全部代码请点赞关注收藏后评论区留言私信~~~ 一.借助摄像头实时检测人脸 与Android自带的人脸检测器相比,OpenCV具备更强劲的人脸识别功能,它可以通过摄像头实时检测人脸,实时检测的预览空 ...

  8. Android App接管手势处理TouchEvnet中单点触摸和多点触控的讲解及实战(附源码 超简单实用)

    运行有问题或需要源码请点赞关注收藏后评论区留言~~~ 一.单点触摸 dispatchTouchEvent onInterceptTouchEvent onTouchEvent三个方法的输入参数都是手势 ...

  9. android ios滑动解锁效果,Android 高仿 IOS7 IPhone 解锁 Slide To Unlock 附源码

    0. 源码下载 1. IPhone 解锁 效果图: 在最新的IOS7中,苹果更改了解锁方式,整个屏幕向右滑动都可以解锁,不再局限在一个小的矩形中.这种文字加亮移动的效果还是继承了下来.之前滑动最左边的 ...

最新文章

  1. RabbitMQ(四)交换机exchange
  2. 云原生已来,只是分布不均
  3. runat=server
  4. wxWidgets:wxToolBar 示例
  5. 《Linux内核分析》第一周笔记 计算机是如何工作的
  6. python3中的正则模块
  7. 【转载保存】HtmlUnit的使用
  8. openmv串口数据 串口助手_第三课使用pyserial来接收和发送串口数据
  9. 2019.01.13 bzoj4137: [FJOI2015]火星商店问题(线段树分治+可持久化01trie)
  10. 最强战队出炉,2020腾讯广告算法大赛圆满落幕
  11. 二分求浮点数的平方根
  12. P3244 [HNOI2015]落忆枫音
  13. 【Qt】解决“ QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to ‘/tmp/runtime-root‘ ”
  14. 可长期免费使用的国产PLC录波软件(数据采集软件)PLC-Recorder V2.0版新功能
  15. Bluetooth LMP介绍
  16. win7交换机共享宽带连接上网
  17. JAVA-日期类(Date、SimpleDateFormat)
  18. JVM 上篇(4):虚拟机栈
  19. Flutter仿网易云音乐 ---基础准备
  20. 向量旋转(或矢量旋转或坐标轴旋转)后xy坐标重定位(vivado+cordic ip核+matlab) - 适用于数学爱好者

热门文章

  1. K8s简单yaml文件运行例子deployment
  2. hdu-1108 最小公倍数
  3. 16年10月18号2th运算符与流程结构
  4. [转载] linux、Solaris下xdmcp远程桌面服务
  5. 管理索引表:深入研究B树索引--重建,合并,删除(理论篇3)
  6. Apache模块开发helloworld无错版
  7. expires与etag控制页面缓存的优先级
  8. DataGrid删除确认及Item颜色交替
  9. 各种媒体在线播放代码
  10. 不要依赖代码中的异常