iOS KVO (验证Object-C实现流程)
前言
在Object-C中有一种观察者模式,即是Key-Value-Observing(KVO)。
利用KVO可以很容易实现视图组件和数据模型的分离。当数据模型的值改变时,会马上触发视图组件,更新视图组件。
在Objc中要实现KVO,必须实现NSKeyValueObServing协议,所幸的是NSObject已经实现该协议,也就是说,几乎所有的Objc对象都可以使用KVO。
本质是:
- 重写set方法
- set方法内部会调用
willChangeValueForKey
- set方法内部会调用
didChangeValueForKey
一般使用
- 被监听者通过 addObserver:forKeyPath:options:context: 方法,添加监听
- 监听者重写 observeValueForKeyPath:ofObject:change:context: 方法,实现监听
- 被监听者移除监听
KVO 的整个实现基本就是这样的,接下来我们来验证这一系列的东西。
- Runtime有没有创建
NSKVONotifying_DataModel
这个类 -
NSKVONotifying_DataModel
有没有重写这个set方法 -
NSKVONotifying_DataModel
重写set方法做了些什么 - 怎么知道set 方法内部调用了
_NSSetObjectValueAndNotify
这个方法 -
_NSSetObjectValueAndNotify
这个方法内部又做了写什么
当我们对某一个对象的属性监听时,
- (void)viewDidLoad {
[super viewDidLoad];
self.dataModel1 = [DataModel new];
self.dataModel1.name = @"like";
self.dataModel2 = [DataModel new];
self.dataModel2.name = @"like";
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.dataModel1 addObserver:self forKeyPath:@"name" options:options context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.dataModel1.name = @"like124";
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"----%@",change);
}
我们创建两个实例对象self.dataModel1 self.dataModel2
但是只监听了self.dataModel1.name
。当触发-touchesBegan
方法时改变name的值,对应就会触发-observeValueForKeyPath
。当我们把断点设置到NSLog(@"----%@",change);
用LLDB查看一下各个对象的指针是什么情况。
(lldb) p self.dataModel1.isa
(Class) $4 = NSKVONotifying_DataModel
Fix-it applied, fixed expression was:
self.dataModel1->isa
(lldb) p self.dataModel2.isa
(Class) $5 = DataModel
Fix-it applied, fixed expression was:
self.dataModel2->isa
(lldb)
这里会发现self.dataModel1
的isa 指向的并不是DataModel
,而是指向一个新的类NSKVONotifying_DataModel
。
而self.dataModel2
的isa 指向的是DataModel
。对于self.dataModel2
的isa 我们感觉就是应该这样的。但是对于 self.dataModel1
还是有疑问的。因为我们项目中并没有NSKVONotifying_DataModel
这个类,现在却指向它。
图1中示例图说是Runtime 创建了一个对开发者而言的隐藏类。因为是使用了KVO 之后才有这个类的NSKVONotifying_DataModel
,假设它的父类是DataModel
。稍后我们可以Runtime 来验证一下它的父类。
我们不妨在Xcode中手动创建一个NSKVONotifying_DataModel
类。看看是什么情况
2018-09-22 14:37:48.885883+0800 Test_KVO[13678:432482]
[general] KVO failed to allocate class pair for name NSKVONotifying_DataModel,
automatic key-value observing will not work for this class
这就说明NSKVONotifying_DataModel
没有成功,因为Runtime 在运行时就帮我们创建了一个。
这验证了Runtime 确实创建了一个NSKVONotifying_DataModel
我们也不妨用Runtime查看一下这个两个类内部的方法都是什么
- (void)viewDidLoad {
[super viewDidLoad];
self.dataModel1 = [DataModel new];
self.dataModel1.name = @"like";
self.dataModel2 = [DataModel new];
self.dataModel2.name = @"like";
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.dataModel1 addObserver:self forKeyPath:@"name" options:options context:nil];
NSLog(@"0----%s",object_getClassName(self.dataModel1));
[self getMethodWithClass:object_getClass(self.dataModel1)];
NSLog(@"0----%s",object_getClassName(self.dataModel2));
[self getMethodWithClass:object_getClass(self.dataModel2)];
NSLog(@"%@ %@",[self.dataModel1 class],[self.dataModel2 class]);
}
- (void)getMethodWithClass:(Class)cls {
unsigned int count;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString * methodStr = [NSMutableString string];
for (int i =0 ; i< count; i++) {
Method method = methods[i];
NSString * str = NSStringFromSelector(method_getName(method));
[methodStr appendString:str];
[methodStr appendString:@" "];
}
free(methods);
NSLog(@"方法--%@",methodStr);
}
打印的结果
Test_KVO[15009:524851] 0----NSKVONotifying_DataModel
Test_KVO[15009:524851] 方法--setName: class dealloc _isKVOA
Test_KVO[15009:524851] 0----DataModel
Test_KVO[15009:524851] 方法--.cxx_destruct name setName:
Test_KVO[15009:524851] DataModel DataModel
从这个结果也能说明一个问题NSKVONotifying_DataModel
的父类应该是DataModel
DataModel | NSKVONotifying_DataModel |
---|---|
setName: | setName: |
name | dealloc |
.cxx_destruct | _isKVOA |
class |
可以看出来NSKVONotifying_DataModel
不但重写了属性的set方法,还重写了-class -dealloc
还新加了一个-_isKVOA
方法。
NSKVONotifying_DataModel
关于重写 class方法
我们去调用[self.dataModel1 class]
发现打印的还是DataModel
这个类,可能内部返回就是DataModel
类,是为了不让外界知道有NSKVONotifying_DataModel
这个类的存在。
_isKVOA
这个方法可能是个BOOL 值,返回YES说明这个类是一个Runtime自动生成的类。dealloc
可能就是做一些其他的事情,销毁自己的时候把一些方法移除掉。
DataModel
.cxx_destruct
ARC下对象实例变量的释放过程在 .cxx_destruct内完成 ,详细可以查看
剩下的类就是自己添加的和属性的get set 方法
到现在为止我们已经验证了 Runtime这个过程的操作,但是我们怎么知道重写set 方法内部做了些什么呢?
其实我们还要用到强大的Runtime 还了解一下
- (void)viewDidLoad {
[super viewDidLoad];
self.dataModel1 = [DataModel new];
self.dataModel1.name = @"like";
self.dataModel2 = [DataModel new];
self.dataModel2.name = @"like";
NSLog(@"添加KVO前的方法\n %p--%p",
[self.dataModel1 methodForSelector:@selector(setName:)],
[self.dataModel2 methodForSelector:@selector(setName:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.dataModel1 addObserver:self forKeyPath:@"name" options:options context:nil];
NSLog(@"添加KVO后的方法\n %p--%p",
[self.dataModel1 methodForSelector:@selector(setName:)],
[self.dataModel2 methodForSelector:@selector(setName:)]);
NSLog(@"添加");
}
打印结果如下:
Test_KVO[15415:562521] 添加KVO前的方法
0x10717b430--0x10717b430
Test_KVO[15415:562521] 添加KVO后的方法
0x1074bda7a--0x10717b430
// 断点下 进行调试
(lldb) p (IMP)0x10717b430
(IMP) $0 = 0x000000010717b430 (Test_KVO`-[DataModel setName:] at DataModel.h:12)
(lldb) p (IMP)0x1074bda7a
(IMP) $1 = 0x00000001074bda7a (Foundation`_NSSetObjectValueAndNotify)
(lldb)
由此结果可以看出,添加KVO前后set方法的指针已经变了,这里也说明set方法被重写了。
这验证了NSKVONotifying_DataModel
确实重写了set 方法
我们用IMP 可以看到各自内部的set方法。我们关注NSKVONotifying_DataModel
这个内部的实现是调用Foundation
框架中的_NSSetObjectValueAndNotify
的C语言方法
但是我们是如何知道_NSSetObjectValueAndNotify
内部是怎么样的呢?
这里需要使用逆向开发了,通过越狱手机可以获取Foundation
框架,使用Hopper来解析源码生成的是汇编语言,看看汇编源码会发现_NSSetObjectValueAndNotify
内部注释有提示说调用了didChangeValueForKey
那我们需要怎么验证这个问题呢?
- 我们需要手动触发一个KVO
言外之意我们不触发set 方法,用_name = @'LiMing'
可以不触发set 方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.dataModel1 willChangeValueForKey:@"name"];
[self.dataModel1 didChangeValueForKey:@"name"];
}
我们直接调用这连个方法发现确实触发了KVO-(void)observeValueForKeyPath
方法。
这里还验证了另一个问题如果我们直接调用didChangeValueForKey
会发现不能触发KVO,如果前面加上willChangeValueForKey
就可以触发KVO,说明调用didChangeValueForKey
的时候会检测 是否调用了willChangeValueForKey
,可见OC内部代码的严谨性。
这验证了_NSSetObjectValueAndNotify
的一些内部操作
总结
- 添加属性的KVO 会触发Runtime 创建一个
NSKVONotifying_XXX
的内部隐藏类 -
NSKVONotifying_XXX
的set 方法会调用Foundation
的_NSSetObjectValueAndNotify
方法 -
_NSSetObjectValueAndNotify
内部会调用
willChangeValueForKey
[ super setName:]
didChangeValueForKey
-
didChangeValueForKey
会发消息给KVOobserveValueForKeyPath
方法