废话不多说先来几个面试题:
一,iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
二,如何手动触发KVO
三,直接修改成员变量会触发KVO吗?
通过挖掘KVO的本质,就会发现,这几个面试题就跟切菜一样
那么什么是KVO呢?
什么是KVO呢?
KVO的全称是Key-Value-Observing,即“键值监听”,可以用于监听某个对象属性值的变化
用一张简单的图就可以表示:
这就是KVO最简单的是使用,那么它到底是怎么实现的呢,下面我们一步步解开这个KVO底层的神秘面纱。
一,代码准备
1. 创建一个Person 类 继承NSObject
// 声明
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
// 实现
@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
2. ViewController代码
// 导入头文件
#import
#import "Person.h"
// 声明person对象
@interface ViewController ()
@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;
@end
- (void)viewDidLoad {
[super viewDidLoad];
person1 = [[Person alloc]init];
person1 = 18;
self.person2 = [[Person alloc]init];
self.person2.age = 28;
// 给person1添加一个KVO
[person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];
}
// 当监听对象的属性值发生改变是,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@",
object, keyPath, change, context);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.person1.age = 20;
}
二,开始研究KVO本质
点击屏幕后,会触发observeValueForKeyPath这监听事件
// 打印结果
监听到的age属性值改变了 - {
kind = 1;
new = 20;
old = 18;
}
发现age的值确实发生了变化
1. 那么age值的改变是否与setAge:方法有关
在touchesBegan给person2重新赋值
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// self.person1.age = 21;
// self.person2.age = 22;
// 上面两句代码,其实就是调用Person的 set 方法,都在调用同一个方法
[self.person1 setAge:21];
[self.person2 setAge:22];
}
当触发touchesBegan事件时,发现调用 [self.person1 setAge:21];后会触发observe监听器,而调用 [self.person2 setAge:22];后不会触发observe监听器。
既然都是在调用 setAge方法,但是为啥只有person1的age发生改变时,才会给observe发通知呢?而person2的age发生改变时,不会通知observe?
先看两张图:
从上面两张图,可以看出来:
其实本质上就是两个实例对象的isa指向不一样
现在看下两个实例的isa有什么不同
lldb结果:
self.person1.isa -> NSKVONotifying_Person
self.person2.isa -> Person
差异:
1. 添加了KVO监听的实例person1,通过isa指向的class是NSKVONotifying_Person
2. 没有添加KVO监听的实例person2,通过isa指向的class是Person
那么,NSKVONotifying_Person这个类是怎么产生的呢?
本质上是利用Runtime动态创建的一个类(可以利用truntime机制自己实现KVO监听)
结论:由此可见,age值的改变与setAge:方法没有关系,而是因为派生出了新的类,此时的setAge:方法的实现也就不一样了,具体有啥不易样的,接着往下看哈。
2. person1添加KVO前后person1和person2对象的变化
验证person1在添加监听前后,person1实例的isa指向的【class】和具体的【对象方法】实现到底发生了哪些变化
- 利用runtime的 object_getClass() 查看person1添加监听前后的,person1和person2的isa指向类对象;
- 利用runtime的 object_getClass() 查看person1添加监听前后的,person1和person2的isa指向类对象的地址;
- 利用runtime的 methodForSelector这个方法来获取setAge:这个实例方法的具体实现
根据以上3点,添加一些打印信息:
NSLog(@"person1添加监听之前类对象:%@, %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加监听之前类对象地址:%p, %p",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加监听之前类的实例方法实现:%p, %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
// 只给person1添加一个KVO
NSKeyValueObservingOptions options =
NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age"
options:options
context:@"123"];
NSLog(@"person1添加监听之后类对象:%@, %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加监听之后类对象地址:%p, %p",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加监听之后类的实例方法实现:%p, %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
分析打印信息:
打印结果:
person1添加监听之前类对象:Person, Person
person1添加监听之后类对象:NSKVONotifying_Person, Person
person1添加监听之前类对象地址:0x1089bd708, 0x1089bd708
person1添加监听之后类对象地址:0x6000021d87e0, 0x1089bd708
结论:
由此可见,person1在添加监听后,isa所指向的类确实改变了,那么setAge:这个对象方法到底是怎么实现的呢,继续分析第三个打印结果
打印结果:
person1添加监听之前类的实例方法实现:0x1061e1ed0, 0x1061e1ed0
person1添加监听之后类的实例方法实现:0x7fff257223da, 0x1061e1ed0
利用lldb查看
(lldb) p (IMP)0x1061e1ed0
(IMP) $0 = 0x00000001061e1ed0 (06-KVO初探`-[Person setAge:] at Person.m:13)
(lldb) p (IMP)0x7fff257223da
(IMP) $1 = 0x00007fff257223da (Foundation`_NSSetIntValueAndNotify)
结论:
由此可见,person1添加监听后,setAge:的实现的确改变了,是在Foundation框架下的_NSSetIntValueAndNotify(c语言方法)实现的,_NSSetIntValueAndNotify具体实现见自己实现的NSKVONotifying_Person 类。
下面可以看下_NSSetIntValueAndNotify实现的伪代码
// 声明一个添加了KVO的派生类 NSKVONotifying_Person
@interface NSKVONotifying_Person : Person
@end
// 实现
@implementation NSKVONotifying_Person
- (void)setAge:(int)age {
_NSSetIntValueAndNotify();
}
// 伪代码 大概流程
void _NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
// 伪代码
// 通知监听器,某某属性发生了改变
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
3. 查看person1添加KVO后,都有哪些调用
在Person类里,实现两个父类的方法,并且给setAge加上打印信息
- (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");
}
触发touchesBegan方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.person1 setAge:21];
// [self.person2 setAge:22];
}
打印结果是:
哥们 == willChangeValueForKey
哥们 == setAge
哥们 == didChangeValueForKey-begin
监听到的age属性值改变了 - {
kind = 1;
new = 21;
old = 1;
} - 123
哥们 == didChangeValueForKey-end
打印结果分析:添加了KVO的对象,属性改变的话,都有下面一些方法调用
当修改instance对象的属性时,先调用setter方法,然后实现Foundation中的_NSSet****ValueAndotify函数
a> willChangeValueForKey:
b> 父类原来的setter
c> didChangeValueForKey:
内部会触发监听器(Observe)的监听方法(observeValueForKeyPath:ofObject:change:context:)
如果person2调用了setAge:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// [self.person1 setAge:21];
[self.person2 setAge:22];
}
打印结果只有一个
哥们 == setAge
说明:没有添加监听的对象,willChangeValueForKey和didChangeValueForKey方法不会调用
4. 查看NSKVONotifying_Person类的元类
验证一下,添加KVO后,新生成的NSKVONotifying_Person类对象的isa指向的元类对象及对象地址是什么
利用runtime的 Class object_getClass(id obj) 来查看对象地址和对象名
1> 传入的obj可能是instance对象、class对象、meta-class对象
2> 返回值
a) 如果是instance对象,返回class对象
b) 如果是class对象,返回meta-class对象
c) 如果是meta-class对象,返回NSObject(基类)的meta-class对象
为了验证在添加KVO后添加一些打印信息
NSLog(@"类对象地址:%p, %p",
// 拿到person1.isa,相当于Person类地址(isa & ISA_MASK)
object_getClass(self.person1),
// 拿到person2.isa
object_getClass(self.person2));
NSLog(@"元类对象地址:%p, %p",
// 拿到person1.isa.isa
object_getClass(object_getClass(self.person1)),
// 拿到person2.isa.isa
object_getClass(object_getClass(self.person2)));
NSLog(@"类对象:%@, %@",
// 拿到person1的isa指向的类
object_getClass(self.person1),
// 拿到person2的isa指向的类
object_getClass(self.person2));
NSLog(@"元类对象:%@, %@",
// 拿到person1的isa指向的类的元类
object_getClass(object_getClass(self.person1)),
// 拿到person2的isa指向的类的元类
object_getClass(object_getClass(self.person2)));
打印信息分析:
类对象地址:0x600001284120, 0x1081ea710
元类对象地址:0x6000012841b0, 0x1081ea6e8
类对象:NSKVONotifying_Person, Person
元类对象:NSKVONotifying_Person, Person
结论:
从打印结果来看,派生出来的NSKVONotifying_Person类的元类就是它本身
三, 派生出来的NSKVONotifying_Person都有哪些方法
先看一张示例图,红框狂起来的部分
1. 利用runtime来获取方法实现
Class cls = object_getClass(self.person1);
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);
}
打印结果:
setAge:
class
dealloc
_isKVOA
所以派生出来的NSKVONotifying_Person类里面,确实存在setAge:/class/dealloc/_isKVOA这些方法
2. 研究下为啥要重写class方法呢?
添加两个打印信息
NSLog(@"类对象:%@, %@", object_getClass(self.person1), object_getClass(self.person2));
NSLog(@"类对象:%@, %@", [self.person1 class], [self.person2 class]);
上面打印信息是利用两种方法来获取person1和person2的类对象
打印结果:
类对象:NSKVONotifying_Person, Person
类对象:Person, Person
结果分析:
只有通过runtime的object_getClass这中方式,取出来的才是person1的isa指向的类对象,
而通过self.person1 class]取出来的,不一定是person1的isa指向的类对象
为什么要重写class呢?
猜测:系统为了直接返回当前类,屏蔽内部实现,
如果不重写,person1找到isa,通过isa找到对应的类对象,在这个类里面发现没有-class的这个方法,那么就通过superclass找到父类,
如果父类还没有,就通过superclass一直找到基类,基类(NSObject)里面就会通过object_getClass(sef)返回当前的类,即Person
四,回答面试题
一,iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
1. 利用runtimeAPI动态生成一个子类,并且让instance的isa指向一个全新的子类
2. 当修改instance对象的属性时,现调用setter方法,然后实现Foundation中的_NSSet****ValueAndotify函数
a> willChangeValueForKey:
b> 父类原来的setter
c> didChangeValueForKey:
内部会触发监听器(Observe)的监听方法(observeValueForKeyPath:ofObject:change:context:)
二,如何手动触发KVO
意思就是不通过改变属性的值,怎么触发KVO
手动调用willChangeValueForKey:和didChangeValueForKey:
三,直接修改成员变量会触发KVO吗?
不会触发KVO,因为只有通过setter方法才能触发,而成员变量不会调用setter方法
要想通过修改成员变量来触发KVO,也很简单
手动依次调用:
1.willChangeValueForKey:
2.修改成员变量
3.didChangeValueForKey: