0%

KVO的使用及实现原理

KVO使用

KVO,观察者与被观察者.被观察者添加某个观察者,观察自身某一属性的变化.当被观察者的属性值发生变化时,被观察者会直接将变化发送给观察者 (通过给观察者发送observeValueForKeyPath:消息),这也是和通知差别比较大的地方.

1.观察者需要实现observeValueForKeyPath:方法,并在该方法中做出相应行为.如果观察者没有实现observeValueForKeyPath:方法,且其父类也没有实现,那么最终会走到NSObject的实现,而NSObject的默认实现就是崩溃:An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.如果观察者本身没有实现observeValueForKeyPath:…方法,但是其父类实现了,那么执行的就是父类的实现,这也是消息的传递过程.

2.[super observeValueForKeyPath:]

对于网上一种写法:

1
2
3
4
5
6
7
8
9
- (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];
}
}

这里首先要清楚[super method]的作用,[super method]表示的是去执行父类里的实现,消息的接收者还是子类对象本身.并不是说消息的接收者变成了一个父类对象在执行该方法.完整的写法确实应该在else处添加super调用。但由于这种场景很少出现,所以很少有人特意去调super,一般也不会出问题。

3.KVO的context参数取值

context参数虽然可以传入任意值,但如果传入的是一个对象,则必须保证在KVO期间该对象不会被销毁.否则在observeValueForKeyPath方法中将导致野指针访问崩溃.可以传入一些常量比如字符串常量,或类对象.

1
2
3
[self.people addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(self.class)];

[self.people addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"mycontext"];

不应该传入一个临时对象比如

1
[self.people addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)([NSString stringWithFormat:@"%@", NSStringFromClass(self.class)])];

4.removeObserver:

1
2
3
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

作用都是移除观察者. 使用带context的移除方法,可以让你精细控制移除指定场景下的观察者.

1
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);

官方说明:
You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
也就是说当一个对象多次注册观察同一个key path但context不同时,那么使用带context的方法可以让你精确的移除某个context下的观察,而另一个继续工作.

5.KVO坑

5.1 KVO的addObserver和removeObserver要配对使用,否则导致崩溃.

5.1.1 添加次数>移除次数。

常见的是添加了而不调用移除.这种情况崩溃需要一定的条件触发:观察者先于被观察者销毁,被观察者的属性再发生改变。
A添加了B作为观察者,B先于A销毁,B销毁时需要将B从观察者列表中移除。如果不移除,当被观察者的属性发生改变时,系统依然会给这个已销毁的观察者发送observeValueForKeyPath消息,从而导致野指针崩溃.内部实现估计是unsafe_unretain引用。

5.1.2 添加次数<移除次数。

常见的是没有添加却调用移除.这种情况是马上崩溃。
如果B不是A的观察者,那么A就不能去removeObserver:B.否则崩溃:Cannot remove an observer for the key path “highlighted” from because it is not registered as an observer.
看起来是废话,但确实有可能发生,比如B之前是A的观察者,后来A removeObserver:B,此时B就不再是A的观察者,如果后面A又removeObserver:B的话,就会导致崩溃.还有一种场景就是B还没注册成为A的观察者时,B就先销毁了,而B的dealloc里有移除操作,这个更有可能发生.

6.重复注册

子类SecondViewController的viewDidLoad里addObserver:

1
[self.people addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

父类BaseViewController的viewDidLoad里也addObserver:

1
[self.people addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

因为SecondViewController实例注册了两次.当people的name发生变化,系统会调用SecondViewController实例的observeValueForKeyPath方法两次.由于注册了两次,在不需要观察时也要移除两次。

KVO实现原理

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface Tree : NSObject

@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *name;

- (void)ageVarChanger:(NSInteger)age;

+ (void)someClassMethod;

@end

@property (nonatomic, strong) Tree *tree;

- (void)addKVO {
NSLog(@"add前,class:%@, superClass:%@, setAge:%p", object_getClass(self.tree), class_getSuperclass(object_getClass(self.tree)), [self.tree methodForSelector:@selector(setAge:)]);
[self printObjectInstanceMethod:object_getClass(self.tree)];
[self printObjectClassMethod:object_getClass(self.tree)];

[self.tree addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

NSLog(@"add后,class:%@, superClass:%@, setAge:%p", object_getClass(self.tree), class_getSuperclass(object_getClass(self.tree)), [self.tree methodForSelector:@selector(setAge:)]);
[self printObjectInstanceMethod:object_getClass(self.tree)];
[self printObjectClassMethod:object_getClass(self.tree)];
}

打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
2022-11-22 22:23:13.329809+0800 UIObserver[10217:296735] add前,class:Tree, superClass:NSObject, setAge:0x102fa76fc
2022-11-22 22:23:13.329868+0800 UIObserver[10217:296735] 实例方法,ageVarChanger:
2022-11-22 22:23:13.329908+0800 UIObserver[10217:296735] 实例方法,age
2022-11-22 22:23:13.329947+0800 UIObserver[10217:296735] 实例方法,setAge:
2022-11-22 22:23:13.329977+0800 UIObserver[10217:296735] 实例方法,name
2022-11-22 22:23:13.330008+0800 UIObserver[10217:296735] 实例方法,setName:
2022-11-22 22:23:13.330037+0800 UIObserver[10217:296735] 实例方法,.cxx_destruct
2022-11-22 22:23:13.330076+0800 UIObserver[10217:296735] 类方法,someClassMethod
2022-11-22 22:23:13.330198+0800 UIObserver[10217:296735] add后,class:NSKVONotifying_Tree, superClass:Tree, setAge:0x180b5ff00
2022-11-22 22:23:13.330235+0800 UIObserver[10217:296735] 实例方法,setAge:
2022-11-22 22:23:13.330268+0800 UIObserver[10217:296735] 实例方法,class
2022-11-22 22:23:13.330300+0800 UIObserver[10217:296735] 实例方法,dealloc
2022-11-22 22:23:13.330330+0800 UIObserver[10217:296735] 实例方法,_isKVOA

可以看到对象添加观察者后,对象tree的isa指针指向的是类NSKVONotifying_Tree,而不是Tree。并且setAge:方法的实现也变了。NSKVONotifying_Tree是Tree的子类,并且重写了setAge:。

举个运行时创建类的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (void)viewDidLoad {
[super viewDidLoad];

_obj = [ARCPerson new];

Class newClass = objc_allocateClassPair([NSString class],
"NSStringSubclass", 0);
class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:"); objc_registerClassPair(newClass);
id instanceOfNewClass = [[newClass alloc] init];
[instanceOfNewClass performSelector:@selector(report)];
}

void ReportFunction(id self, SEL _cmd) {
NSLog(@" >> This object is %p.", self);
NSLog(@" >> Class is %@, and super is %@.", [self class], [self superclass]);
Class prevClass = NULL;
int count = 1;
for (Class currentClass = [self class]; currentClass; ++count) {
prevClass = currentClass;
NSLog(@" >> Following the isa pointer %d times gives %p", count, currentClass);
currentClass = object_getClass(currentClass);
if (prevClass == currentClass)
break;
}
NSLog(@" >> NSObject's class is %p", [NSObject class]);
NSLog(@" >> NSObject's meta class is %p", object_getClass([NSObject class]));
}

安全的使用KVO

前面分析了KVO的使用添加和移除次数必须匹配,否则很容易崩溃。但是在日常使用中有时还是会出现不匹配的情况,有没有可能从技术层面完全避免呢?有,答案就是Facebook开源的KVOController。KVOController在我看来有下面几个优势:

  1. API更加现代,使用更加方便。提供block和selector,不仅如此竟然还支持在原生方法里处理。
  2. 线程安全。
  3. 可以避免添加和移除次数不匹配的问题。
  4. 观察者销毁时自动被移除(如果需要)。

缺点:

  1. 不支持设置多个context。
  2. 其他一些偏门场景可能会有问题。

如果你不想用KVOController,也有两个简单的方法:

  1. 自己定义一个标志位,添加了设置为true,移除时先判断是不是添加了,添加了才移除。
  2. 使用try-catch包裹移除方法。

相较起来还是KVOController更牛逼。不过封装本身就是越封装使用越方便,但功能细节也丢失得越多。看情况使用吧。

问题

  1. 中间类是什么时候生成的?是在编译期间生成还是运行时生成?

    运行时生成。

    p1对象执行过addObserver操作之后,p1对象的isa指针由之前的指向类对象Person变为指向NSKVONotifyin_Person类对象,而p2对象没有任何改变。也就是说一旦p1对象添加了KVO监听以后,其isa指针就会发生变化,因此set方法的执行效果就不一样了。

    当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。

  2. 中间类和原类的关系?

    生成的中间类NSKVONotifyin_Person是原类Person的子类,并且重写了setAge:,class方法。重写class方法,可以让外部以为使用的还是原类,这样就隐藏了实现细节。当给对象发送其他消息时,由于NSKVONotifyin_Person是Person的子类,而NSKVONotifyin_Person又没有重写这些方法,所以会执行到父类的实现,非常的完美。

参考

iOS底层原理总结 - 探寻KVO本质

觉得文章有帮助可以打赏一下哦!