基本概念
KVO:Key-Value Observing,俗称’键值监听’,用于监听某个对象属性值的改变。
基本使用
自动触发
创建Person对象,并创建属性name:
Person *p = [[Person alloc]init];
//添加观察者
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
添加观察者后,当p的name属性的值发生变化时就会自动调用方法:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
注:要在dealloc方法中移除监听
手动触发
使用场景:当监听的属性改变时不需要自动触发,当需要时再去触发。
解决方法是:先关闭自动触发,在Person.m中重写方法:
//默认YES(自动)
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
然后在改变属性值的时候:
[_P willChangeValueForKey:@"name"];
_P.name = [NSString stringWithFormat:@"%d",a++];
[_P didChangeValueForKey:@"name"];
这样就完成了手动触发。
复杂情况
场景:当Person中的属性是自定义Student 类的实例对象stu,在Student类中有属性 age,如何通过观察p对象来监听age属性?
解决方法一
在添加观察者时:
[p addObserver:self forKeyPath:@"stu.age" options:NSKeyValueObservingOptionNew context:nil];
弊端:当Student中有多个属性需要监听时,一个一个的添加监听者虽然没错但是太麻烦了!
解决方法二
在Person.m中重写该方法:
//返回一个容器
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"stu"]) {
NSArray *addKeyPath = @[@"_stu.no",@"_stu.age"];
keyPaths = [keyPaths setByAddingObjectsFromArray:addKeyPath];
}
return keyPaths;
}
这样只需要对stu监听即可。
注:一定要带下划线
内部实现原理
面试题答案:当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,使其指向通过Runtime动态创建的子类,该子类重写了set方法,内部实现会调用
willChangeValueForKey、父类的setter、didChangeValueForKey。在didChangeValueForKey方法中又会调用监听器的监听方法。
接下来一步步进行验证。
首先我们利用运行时方法分别打印添加监听前后对象的类:
Person *p = [[Person alloc]init];
NSLog(@"添加监听之前%@",object_getClass(p));
//添加观察者
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加监听之后%@",object_getClass(p));
打印结果:
KVODemo01[1749:181970] 添加监听之前Person
KVODemo01[1749:181970] 添加监听之后NSKVONotifying_Person
由此可知再添加监听后,系统创建了一个新的类,p对象的isa指向这个新的类。
下一步我们可以打印添加监听前后的set方法的地址:
NSLog(@"添加监听之前%p",[p methodForSelector:@selector(setName:)]);
NSLog(@"添加监听之后%p",[p methodForSelector:@selector(setName:)]);
打印结果:
添加监听之前0x10570c3a0
添加监听之后0x105a56efe
我们可以利用lldb分别看一下具体的方法实现:
p (IMP)0x10570c3a0
(IMP) $0 = 0x000000010570c3a0 (KVODemo01`-[Person setName:] at Person.h:12)
p (IMP) 0x105a56efe
(IMP) $2 = 0x0000000105a56efe (Foundation`_NSSetObjectValueAndNotify)
由以上证明可知:添加了监听后,新创建的类重写了set方法,新创建的set方法内部调用了_NSSetObjectValueAndNotify
.
最后我们验证 _NSSetObjectValueAndNotify中是否调用了上面提到了函数和他们的执行顺序:
在Person中重写:
在Person中重写:
-(void)willChangeValueForKey:(NSString *)key{
NSLog(@"willChangeValueForKey-begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey-end");
}
-(void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey-begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey-end");
}
打印结果:
2018-03-20 16:06:30.610367+0800 KVODemo01[1892:206083] willChangeValueForKey-begin
2018-03-20 16:06:30.610559+0800 KVODemo01[1892:206083] willChangeValueForKey-end
2018-03-20 16:06:30.610661+0800 KVODemo01[1892:206083] didChangeValueForKey-begin
2018-03-20 16:06:30.610924+0800 KVODemo01[1892:206083] {
kind = 1;
new = 0;
}
2018-03-20 16:06:30.611022+0800 KVODemo01[1892:206083] didChangeValueForKey-end
由此可知,调用Foundation的_NSSetObjectValueAndNotify
的方法后内部依次调用了
willChangeValueForKey、
父类的setter、
didChangeValueForKey。
在didChangeValueForKey方法中又会调用监听器的监听方法
结语
希望以上分享对读者有所帮助,更希望大神不吝赐教!
网友评论