前几天写了一篇博客(点这里),分析了系统KVO可能实现的方式,并添加了一些简单代码进行验证。
既然系统KVO不适用,我们完全可以按照之前的思路,再造一个可以在项目中使用的KVO轮子。
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' do
pod '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. 检查参数
...
///生成并保存item
AWSimpleKVOItem *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
,然后将它存入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 动态添加子类
添加子类的代码很简单,最主要的代码只需要两行: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函数需要完成两件事:
- 调用父类的
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。