之前讲过KVO的简单应用,这次来讲解下KVO监听属性值的变化到底是怎么实现的,在监听的时候苹果在背后都做了哪些事情。举个例子:
@interface Person : NSObject
{
@public
NSString * _nickname;
}
@property (nonatomic , copy) NSString *name;
在Person类中添加一个name
属性和一个_nickname
成员变量,两者区别在于添加属性name
时,会生成相应的setter/getter
方法以及一个名为_name
的成员变量;添加 _nickname
的时候是不会生成setter/getter
方法,不明白的请看这里。为了在外部能够访问_nickname
成员变量需要加上@public
将其公开。
设置监听
static NSString * nameKey;
static NSString *nicknameKey;
Person *person = [[Person alloc] init];
_person = person;
[_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:&nameKey];
[_person addObserver:self forKeyPath:@"_nickname" options:NSKeyValueObservingOptionNew context:&nicknameKey];
这里分别对属性name和成员变量_nickname进行监听,观察两者被修改时是否都能被监听到?
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if (context == &nameKey) {
NSLog(@"name改变了");
}else if (context == &nicknameKey){
NSLog(@"nickname改变了");
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int i = 0;
i ++ ;
_person.name = [NSString stringWithFormat:@"name = %d",i];
_person->_nickname = [NSString stringWithFormat:@"nickname = %d",i];
NSLog(@"%@ %@",_person.name,_person->_nickname);
}
当点击屏幕的时候发现name值的改变被监听到了,而_nickname值的改变没有被监听到,这是什么原因呢?这里就要说KVO的到底是怎么实现的。
KVO实现原理
在监听处打个断点,调试看看,这里面有什么猫腻
这个时候看你的控制台:
我们看到这时候的对象person类型还是Person
,这个isa
指针指向的是真实数据类型。然后我们向下走一步会看到:
咦!对象person的数据类型变成了 NSKVONotifying_Person
,这是什么情况??难道苹果默默的做了些不为人知的是事情??答案是肯定的。
首先当我们在添加监听的时候,在其内部会动态(通过objc_allocateClassPair
函数)的创建一个继承自XXX
类且名为NSKVONotifying_XXX
的类,在这个类里面会去重写被观察属性的setter
方法:
- (void)setName:(NSString *)name
{
[self willChangeValueForKey:@"name"];// 老值
[super setName:name];
[self didChangeValueForKey:@"name"]; // 新值
}
重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针指向这个新创建的子类(通过object_setClass
函数改变指针),对象就神奇的变成了新创建的子类的实例。
回到之前的问题,_nickname属性值变化不能够被监听的原因也就出来了,因为 _nickname没有setter方法,也就没办法重写setter方法,最终导致无法监听其值的变化。所以KVO观察属性的变化其实就是观察属性的setter方法。
如果想要观察到成员变量的变化有两个办法:
1、通过KVC修改成员变量值
[_person setValue:@"John" forKeyPath:@"nickname"];
2、通过手动触发KVO
[_person willChangeValueForKey:@"nickname"];
_person -> _nickname = @"John";
[_person didChangeValueForKey:@"nickname"];
手动触发KVO
通常情况下KVO是系统自动触发的,当属性的值发生变化时就会被观察到。如股不想被系统自动触发KVO该怎么做呢?也简单调用类方法automaticallyNotifiesObserversForKey:
:
// 默认返回 YES
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
// 可以通过key判断哪些需要手动触发KVO,哪些需要自动触发KVO
return NO;
}
该方法如果返回YES就是系统自动触发KVO,如果返回NO就是需要手动触发KVO,因为有个参数key
,所以可以将不同的属性设置为不同的触发条件。自动触发很简单,上面已经说过了,那么当返回NO的时候如何手动触发KVO呢?可以调用下面两个方法:
[_person willChangeValueForKey:@"name"];//name即将改变
_person.name = @"玉子";
[_person didChangeValueForKey:@"name"];//name已经改变
keyPathsForValuesAffectingValueForKey:
情景:在Person类中复合一个Teacher类,然后在Teacher类中添加若干属性,如level属性、age属性等。我们的需求是当Teacher类中的属性发生改变时能够被观察到,我们可以这样做:
[_person addObserver:self forKeyPath:@"teacher.level" options:NSKeyValueObservingOptionNew context:nil];
[_person addObserver:self forKeyPath:@"teacher.age" options:NSKeyValueObservingOptionNew context:nil];
// ....
添加若干个监听,这样就可以实现上述需求,但是这样写显得太low,而且代码重复。这时可以使用keyPathsForValuesAffectingValueForKey:
方法:
// 直接监听teacher
[_person addObserver:self forKeyPath:@"teacher" options:NSKeyValueObservingOptionNew context:nil];
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"teacher"]) {
// 添加依赖
keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"_teacher.level",@"_teacher.age"]];
}
return keyPaths;
}
如果不使用keyPathsForValuesAffectingValueForKey:
方法而去直接监听teacher
属性,那么当teacher中的属性修改时,不会被监听到,因为teacher的地址没有改变,不会调用setter方法。
类似的情况还有监听NSMutableArray
,当调用addObject:
方法时不会被监听到,这时候可以使用mutableArrayValueForKey:
,具体方法可查看。
KVO遇到的问题
KVO优点:
- 代码简洁、实现简单;
- 可以实现两个对象间的同步(mode和view的同步);
- 能够提供观察的属性的最新值以及先前值;
- 用
keyPaths
来观察属性,因此也可以观察嵌套对象。
缺点也很明显在使用KVO的时候,系统会在运行时做动态创建类、重写方法等工作,这是非常消耗内存的。
常见情景:
1、监听webView
的estimatedProgress
,估计进度变化,以此来给webView加个进度条:
[webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
2、监听tableView
的contentOffset
、contentSize
等属性:
[_tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) {
[self doSomethingWhenContentOffsetChanges];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
这里有个注意点,当观察的属性不在本类中,而是在superClass或者更上层父类中,这个时候需要加这个if
判断,不至于这次KVO事件的触发被中断。
KVO崩溃场景
被观察者在销毁前,要移除所有的观察者,iOS10以下会崩溃,iOS11以上不会崩溃。
- observe忘记写监听回调方法 observeValueForKeyPath;
- add和remove次数不匹配;
- 监听者和被监听者dealloc之前没有remove(其实也原因2,但是监听者和被监听者的生命周期不同)