概述
KVO
的全称是 Key Value Observing
,俗称 键值监听
,可以用于监听某个对象属性值的改变。
基本使用
使用 KVO
监听 person
对象的 age
属性改变:
@interface JFPerson : NSObject
@property (nonatomic, assign) int age;
@end
@implementation JFPerson
@end
@interface ViewController ()
@property (nonatomic, strong) JFPerson *person;
@end
@implementation ViewController
- (void)dealloc
{
// 移除KVO监听
[self.person removeObserver:self forKeyPath:@"age"];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[JFPerson alloc] init];
self.person.age = 10;
[self setupPersonAgeObserving];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// .语法修改属性值,相当于执行了setter方法 [self.person setAge:20];
self.person.age = 20;
}
// 监听person对象的age属性
- (void)setupPersonAgeObserving
{
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
}
// 监听到属性改变后,就会执行
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性改变:%@", object, keyPath, change);
NSLog(@"new=%@", change[NSKeyValueChangeNewKey]);
NSLog(@"old=%@", change[NSKeyValueChangeOldKey]);
}
@end
修改 person
对象的 age
属性,打印日志:
监听到<JFPerson: 0x60000042c7e0>的age属性改变:{
kind = 1;
new = 20;
old = 10;
}
new=20
old=10
KVO实现原理
创建两个对象,只给 person1
添加 KVO
监听:
self.person1 = [[JFPerson alloc] init];
self.person2 = [[JFPerson alloc] init];
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
然后通过 lldb
打印 isa
,发现添加 KVO
监听后的对象的 类对象
改变了:
(lldb) p self.person1->isa
(Class) $0 = NSKVONotifying_JFPerson
(lldb) p self.person2->isa
(Class) $1 = JFPerson
其实 NSKVONotifying_JFPerson
是 Runtime
动态创建的一个继承自 JFPerson
的子类。
在 NSKVONotifying_JFPerson
类中,重写了 setAge:
方法,下面是大概伪代码:
@implementation NSKVONotifying_JFPerson
- (void)setAge:(int)age
{
// 调用Foundation框架中的函数
_NSSetIntValueAndNotify();
}
// 伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
// 伪代码
- (void)didChangeValueForKey:(NSString *)key
{
// 通知监听器,某属性发生了改变
[observer observeValueForKeyPath:key ofObject:self change:change context:nil];
}
@end
手动触发KVO
只要手动调用 willChangeValueForKey
和 didChangeValueForKey
方法,就可以触发 KVO
:
[self.person willChangeValueForKey:@"age"];
self.person->_age = 10;
[self.person didChangeValueForKey:@"age"];
KVO验证
只对 person1
添加 KVO
监听,打印 setAge:
函数的实现(IMP
)的地址:
NSLog(@"%p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
// 0x1b15d7280 0x102d861e8
然后通过 lldb
可以看出添加了 KVO
监听后,setAge:
实现变成了 Foundation
框架中的 _NSSetIntValueAndNotify
函数:
(lldb) p (IMP)0x1b15d7280
(IMP) $0 = 0x00000001b15d7280 (Foundation`_NSSetIntValueAndNotify)
(lldb) p (IMP)0x102d861e8
(IMP) $1 = 0x0000000102d861e8 (KVO的本质`-[JFPerson setAge:] at JFPerson.m:12)
提取Foundation框架
越狱手机,可以通过爱思助手从下面路径获取到 动态库共享缓存
文件:
/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e
在 iOS
系统中,是通过 usr/lib/dyld
来加载动态库的,我们可以从 dyld
的源码中获取到提取动态库的代码,自己编译一个提取工具。
源码地址:https://opensource.apple.com/tarballs/dyld/
下载源码后,找到 dyld3/shared-cache/dec_extractor.cpp
文件,保留红框部分的源码,其他全删除:
然后通过 clang++
编译,最终得到可执行文件 dsc_extractor
:
clang++ -o dsc_extractor dsc_extractor.cpp
开始提取:
./dsc_extractor dyld_shared_cache_arm64e test
提取完成后,就可以获得所有动态库的二进制文件,也就是 mach-o
文件:
然后通过反编译工具,比如 IDA
或 Hopper
,反编译 Foundation
后,就可以找到方法和函数的实现的汇编代码:
通过伪代码也可以大致分析出函数体或方法体里的逻辑。
打印方法名
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 遍历所有方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"methodName=%@", methodName);
}
free(methodList);
}
添加 KVO
前后分别调用上面方法,传入类对象:
// 添加KVO前
methodName=age
methodName=setAge:
// 添加KVO后
methodName=setAge:
methodName=class
methodName=dealloc
methodName=_isKVOA