一次有趣的KVO实验+KVO防护
KVO 实验与原理新发现
KVO的原理已经非常多了,看了很多高赞博客,但是自己动手做了一下实验,还是有非常有趣的新发现
这篇博客的目录大致如下
- initialize方法的多次调用
- 派生子类的isa链
- KVO的防护,有自己的思路,简单分析了开源代码FBKVOController
1. initialize方法的调用
先简单复习一下initialize的调用原理
- +initialize方法在类收到第一条消息的时候调用,要注意的是,消息包括类方法
- Category实现initialize会覆盖类的实现
- 子类没实现会调用父类的,子类实现了
- 如果父类也实现了,会优先调用父类的initialize
接下来进入实验,会出现一个非常有趣的现象,initialize方法被调用了两次
@implementation TestObject
+ (void)initialize {
XMLog(@"%@", self);
}
- (void)updateAge:(NSInteger)age {
_age = age;
}
@end
- (void)commonInit {
TestObject *object = [[TestObject alloc] init];
[object addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld context:nil];
[object updateAge:1];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"observe");
}
这是打印结果
TestBaseObject
TestObject
NSKVONotifying_TestObject
-
这里证明了父类的initialize方法会被优先调用
-
当KVO观察的时候,发生了isa swizzling,又调用了一次initialize,只不过这一次是派生子类的initialize,但是还是发生了,也就是说,写在这个方法里的实现可能会因为KVO被重复调用
继续实验,可以看到,如果再次创建实例,观察实例,这个时候initialize方法不会再次调用了
TestObject *object1 = [[TestObject alloc] init];
[object1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
[object1 updateAge:2];
2. 派生子类和原类之间的关系
Class klass = object_getClass(object);
Class metaClass = object_getClass(klass);
NSLog(@"KVO class: %@", object.class);
NSLog(@"KVO real class: %@", klass);
NSLog(@"KVO class superClass: %@", class_getSuperclass(klass));
NSLog(@"KVO metaClass: %@", metaClass);
KVO class: TestObject
KVO real class: NSKVONotifying_TestObject
KVO class superClass: TestObject
KVO metaClass: NSKVONotifying_TestObject
-
KVO之后,object.class方法被重写,还是返回原来的class
-
KVOClass isa KVOMetaClass
-
KVOSuperClass isa OriginalClass
-
原生的Class和它的isa链保持不变
3. 通过KVC修改属性能触发KVO吗?
会触发KVO,KVC时先查对应属性的getter/setter,没有getter/setter的情况下,判断accessInstanceVariablesDirectly的返回值,为true会找对应的ivar,如果都没有找到会抛出异常
+ (BOOL)accessInstanceVariablesDirectly {
return NO;
}
-
KVC调用getter流程:getKey,key,isKey,_key,接着找实例变量_key,_isKey,key,isKey
-
KVC调用setter流程:setKey和_setKey,实例变量顺序相同
-
没找到会走setValue:forUndefinedKey:
总结: 只要KVC是通过setter,就会触发KVO,如果需要触发KVO又要自定义setter命名,需要符合KVC查找规则,才能触发KVC
4. 使用KVO需要注意什么
4.1 哪些情况会Crash?
dealloc时没有移除观察者
-
多次重复移除一个属性,移除了未注册的观察者
-
iOS18的情况下,如果添加和删除是同样的次数,不会crash
-
不是同样次数的情况下,删除多会crash
-
被观察者提前释放,被观察者在dealloc的时候还注册着KVO导致崩溃
-
添加了观察者,但是没有实现observeValueForKeyPath:ofObject:change:context:方法
-
添加或移除时keyPath == nil
4.2 KVO优缺点
优点:
-
观察者模式,支持多对一观察属性,也支持观察者监听不同属性;系统封装,功能和Api齐全
-
能对非自己创建的对象进行观察,而且不需要改变内部对象,使用SDK的时候很有用
-
用keyPath观察属性,可以观察嵌套对象
缺点:
-
硬编码字符串,类型不够安全
-
回调方式单一,被观察者很多的时候容易混乱;不能像NSNotification一样直接移除观察者
-
难以debug,不像block/delegate可以通过编译器搜索,硬编码字符串难以查找
-
回调不一定在主线程
-
需要开发者移除,容易崩溃
4.3 KVO的防护
4.3.1 思路
既然是治理和防护,肯定是从缺点和容易出bug的地方治理
-
防重入
-
防止dealloc多次移除;防止回调方法大量if语句
-
防止keyPath为空
其实思路很简单,分成4次优化
- 实现一个NSObject+XMSafeKVO
```objc
- (NSMapTable)xm_safeKVOMap { // 懒加载,第一次使用的时候,WeakToStrongMap初始化,使用setAssociatedObject赋值 }
-
(void)xm_safeSetObserver:(id):observer forKeyPath:(NSString *)keyPath { // getAssocaitedObject获取safeKVOMap,让observer和keyPath关联 }
-
(void)xm_safeAddObserver:(id)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { // 1. 判断keyPath != nil // 2. debug环境下使用NSParameter, release环境直接return // 3. 判断是否重复,重复就删除旧的,注册新的 }
-
(void)xm_safeRemoveAllObservers { // 遍历map,调用removeObserver:forKeyPath: // 加上必要的防御式编程 } ``` 通过这样的方式,就可以实现防重入+防keyPath为空+一次性移除所有keyPath的观察
问题是:回调方法大量if语句的情况没有解决
- 面向对象编程 上面的思路有点类似面向过程,使用方法(函数)解决问题,如果要面向对象,可以封装一个XMKVOInfo对象
@interface _XMKVOInfo : NSObject
@property (nonatomic, copy) id block;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) NSKeyValueObservingOptions options;
@property (nonatomic, assign) void *context;
@end
注册的时候把keyPath换成_XMKVOInfo就可以了,添加几个新的方法,进行addObserver,addObserver的时候根据参数创建_XMKVOInfo
私有类:防止入侵业务代码
到这里问题基本上都解决了,使用封装的形式可以在回调的时候根据keyPath调用block
- 单一性原则 当业务不使用的时候,XMSafeKVO这个Category是不会入侵业务的,只要不导入头文件,可以做到业务无感知
但是往NSObject里添加Category,破坏了NSObject的单一性原则
- 整体迁移 就像网络请求应该被单独拆分成一个Manager一样,SafeKVO也可以是一个单独的Manager,通过这样的形式来管理关联对象
4.3.2 FBKVOController
简单看了一下FBKVOController的源码
FBKVOController:可以理解为KVOControllerManager,相当于_FBKVOSharedController和_FBKVOInfo的封装
分析最重要的几个方法
FBKVOController实现
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
// observe object with info
[self _observe:object info:info];
}
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// check for info existence
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
// observation info already exists; do not observe it again
// unlock and return
pthread_mutex_unlock(&_lock);
return;
}
// lazilly create set of infos
if (nil == infos) {
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
}
// add info and oberve
[infos addObject:info];
// unlock prior to callout
pthread_mutex_unlock(&_lock);
[[_FBKVOSharedController sharedController] observe:object info:info];
}
- 这里可以看到FBKVOController本质上就是保存了objct到它的info的映射,然后把接下来的事情交给_FBKVOSharedController
_FBKVOSharedController
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// register info
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
// add observer
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
if (info->_state == _FBKVOInfoStateInitial) {
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
// this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
// and the observer is unregistered within the callback block.
// at this time the object has been registered as an observer (in Foundation KVO),
// so we can safely unobserve it.
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
-
把info添加进HashTable,通过kvo观察object,设置info的状态
-
这里要理解一下_state的作用
-
当被注册为NSKeyValueObservingOptionInitial的时候,addObserver会直接调用block,有的业务可能就会在回调里unobserve
-
这个时候下面的代码可能还没有执行,而state会直接变成_FBKVOInfoStateNotObserving
-
在这里移除KVO
-
info已经在unobserve中被删除
-
_FBKVOSharedController:真正处理KVO逻辑的class
- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
{
if (0 == infos.count) {
return;
}
// unregister info
pthread_mutex_lock(&_mutex);
for (_FBKVOInfo *info in infos) {
[_infos removeObject:info];
}
pthread_mutex_unlock(&_mutex);
// remove observer
for (_FBKVOInfo *info in infos) {
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
}
-
删除info,移除KVO,设置info的状态
-
_state标识了info的状态,只有_FBKVOInfoStateObserving状态才会被移除
-
为什么不直接判断_state == _FBKVOInfoStateInitial也移除?而是要分开在两个地方?
-
这里涉及到时序的问题,这一块代码和observe的后续代码,不一定哪一块先执行
-
这样判断无论谁先执行,都能正确移除观察者
-
回调方法处理
上面聊过,_FBKVOSharedController主要负责KVO的注册移除,当然也包括回调
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
_FBKVOInfo *info;
{
// lookup context in registered infos, taking out a strong reference only if it exists
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}
if (nil != info) {
// take strong reference to controller
FBKVOController *controller = info->_controller;
if (nil != controller) {
// take strong reference to observer
id observer = controller.observer;
if (nil != observer) {
// dispatch custom block or action, fall back to default action
if (info->_block) {
NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath) {
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);
} else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
-
context实际上就是info,在observe的时候作为参数传递的
-
这里可以看到,回调实际上就是做了一堆防御式编程相关的判断,然后根据参数调用block/action,如果是直接注册的,就调用observer的实现
-
所以这里要注意的是,即使使用了SDK,如果不使用block和action进行回调,还是需要实现KVO的回调方法,不然就会崩溃
自动解绑
- (void)dealloc
{
[self unobserveAll];
pthread_mutex_destroy(&_lock);
}
5. 其它
- 关闭KVO方法,写在被观察者类里
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;
}else{
return [super automaticallyNotifiesObserversForKey:key];
}
}