KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变
一、KVO的本质(ios用什么方法实现对一个对象的KVO)
- 利用runtimeAPI动态生成一个子类,并且让实例对象的isa指针指向这个全新的子类
- 当修改实例对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数(int、double、、、)。
- 在_NSSetXXXValueAndNotify函数内部会调用
1、willChangeValueForKey:
2、父类原来的setter函数
3、didChangeValueForKey:函数,在函数内部会触发监听器的监听方法(observeValueForKeyPath:ofObject:change:context:)
二、如何手动触发KVO
- 手动调用willChangeValueForKey:和didChangeValueForKey:方法。
- 注:在didChangeValueForKey:方法内部会判断是否调用了willChangeValueForKey:方法,如果调用了则会触发监听器的监听方法,否则不会触发,所以willChangeValueForKey:必须调用。
三、直接修改成员变量会出发KVO吗
- 不会触发KVO,但是可以手动调用willChangeValueForKey:和didChangeValueForKey:方法触发KVO
- 通过KVC直接给成员变量赋值会触发KVO
验证KVO的本质
以Person类为例,添加监听方法代码如下:
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end
@interface ViewController ()
@property (strong, nonatomic) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person.age = 1;
// 给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.person setAge:21];
}
// observeValueForKeyPath:ofObject:change:context:
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"age"];
}
@end
首先我们在监听方法前后分别打印person对象isa指针所指向的类对象
NSLog(@"%@",object_getClass(self.person)); //Person
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"%@",object_getClass(self.person)); //NSKVONotifying_Person
由此得知,通过KVO监听之后person对象的isa指针指向了一个全新的类NSKVONotifying_Person
。
接着我们再去对比添加监听前后这个setAge:方法的实现
NSLog(@"%p",[self.person methodForSelector:@selector(setAge:)]); //0x10ee07650
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"%p",[self.person methodForSelector:@selector(setAge:)] ); //0x7fff257023ea
(lldb) p (IMP) 0x10ee07650
(IMP) $0 = 0x000000010ee07650 (Interview01`-[Person setAge:] at Person.m:13)
(lldb) p (IMP) 0x7fff257023ea
(IMP) $2 = 0x00007fff257023ea (Foundation`_NSSetIntValueAndNotify)
由此得知,在监听前后setAge:方法实现的地址不同,并且在添加了监听之后,setAge:方法内部是调用了_NSSetIntValueAndNotify函数。
如何验证Foundationk框架中的_NSSetIntValueAndNotify函数:
- 首先准备一部越狱手机
- 下载iFunBox
- 文件系统 -> System -> Library -> Cashes -> com.apple.dyld -> dyld_shared_cache_arm64,找到dyld_shared_cache_arm64文件
- 执行
./dsc_extractor dyld_shared_cache_arm64 生成文件的文件名
- 找到Fundation.framwork -> Fundation
- 使用Hopper进行反编译,搜索_NSSetIntValueAndNotify,发现确实可以找到
获取一个类对象/元类对象的方法列表
- (NSString *)MethodNamesOfClass:(Class)cls
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
return methodNames;
}
由此,我们去打印NSKVONotifying_Person
的方法,得到setAge:, class, dealloc, _isKVOA,
,四个方法
以对Person类age属性监听为例,产生子类内部实现如下:
@implementation NSKVONotifying_Person
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 屏幕内部实现,隐藏了NSKVONotifying_Person类的存在
- (Class)class
{
return [Person class];
}
- (void)dealloc
{
// 收尾工作、移除监听...
}
- (BOOL)_isKVOA
{
return YES;
}
@end
那么我们如何验证_NSSetIntValueAndNotify
函数内部调用了willChangeValueForKey:
和didChangeValueForKey:
呢?
因为NSKVONotifying_Person
类对象中并没有willChangeValueForKey:
和didChangeValueForKey:
方法,因此我们只要重写父类的这两个方法就可以判断是否调用,代码如下:
@implementation Person
- (void)setAge:(int)age
{
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
当我们点击屏幕,改变age值之后,打印结果如下:
2020-06-15 12:57:24.756224+0800 Interview01[19432:1058626] willChangeValueForKey
2020-06-15 12:57:24.756760+0800 Interview01[19432:1058626] setAge:
2020-06-15 12:57:24.757069+0800 Interview01[19432:1058626] didChangeValueForKey - begin
2020-06-15 12:57:24.757390+0800 Interview01[19432:1058626] 监听到<Person: 0x600001588090>的age属性值改变了 - {
kind = 1;
new = 21;
old = 1;
}
2020-06-15 12:57:24.757537+0800 Interview01[19432:1058626] didChangeValueForKey - end
由此得知,在_NSSetIntValueAndNotify
函数内部,确实调用了willChangeValueForKey:
和didChangeValueForKey: