KVO 面试必问,非常经典
引子: 小灰面试小白
- 小灰问: “什么是 KVO?”
小白答: “键值观察,观察者模式”
- 再问: “什么时候用到 KVO?”
答: “ 值修改,同时改变饼图和柱状图的显示 ”
小灰呵呵,这不就是书上写的?
好一点的答案: AVFoundation
1, 音频应用: AVAudioSession 监测音量 outputVolume
再来个例子
2, 做相机的时候,用到曝光那块
AVCaptureDevice 监测曝光完成 adjustingExposure
- 最后一问: “怎样手动实现 KVO?”
答: “用两个代理吧”
“一般 KVO 的场景就是,”
“A 修改,触发 B 同步”
“B 修改,触发 A 同步”
“整两个代理,也成”
小灰觉得,真是骨骼精奇
从解决问题的角度,有点意思
面试,不是讨论解决问题。面试,是比划套路
小灰总结
我们问 KVO , 实际上是问 runtime
iOS 三个面试重点: runloop, runtime, 多线程
三个面试重点,万变不离
runtime 部分
问 kvo, 是问 runtime
问 OC 项目,转 swift , 是问 runtime
问 dynamic 关键字,是问 runtime
多线程
问线程之间的通信,是问线程安全
小白一听,觉得通信啥,属性不就是爱咋访问
KVO 的 runtime 实现
我们要什么?
类的一个属性变化,会调用一个方法
这个方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
实现思路
简单版
- 添加监测,就把该属性的 setter 方法 hook 了,
在交换的方法中,调用原本的 setter 方法,和 observeValueForKeyPath
- 移除监测,就把方法,再交换回来
简单场景,这个是 OK 的
如果同时存在该类的多个对象,有的要观察,有的不要观察
就需要做到对象粒度的 hook
苹果版,静悄悄
苹果希望静悄悄地,把事情给做了。调用的开发者,对这些无感知
这就要求,不能对该类,有任何的影响
所以,苹果用了一个子类
苹果版的做法,分为两部分
做事情的部分
静悄悄,用户无感知的部分
做事情的部分
- 添加观察者,就是添加该类的子类,
将观察的对象,指向子类
- 给子类,添加观察的属性的
setter
方法, 即自定制的 setter 方法
自定制的 setter 方法中,调用
- (void)yourObserveValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- 给 NSObject 添加分类
分类中,添加方法的定义
- (void)yourObserveValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
为了上一步的顺利
用户无感知的部分
添加观察后,该类的对象,指定为其子类
给其子类添加 class
方法,指向该类 ( 其子类的父类 )
障眼法
开发者调 [obj class]
, 还是原来的
无感知 ,就像什么都没有发生
- 该类的其他对象,调用该属性的 setter 方法, 无影响
其他
关于 block,简单,略
关于上下文,context, 略
关于一个对象,观察多个属性,需要管理状态,略
代码部分
仅列举部分
其子类的 setter 方法,
该方法中,要正常调用父类的 setter 方法,
还要调用
- (void)yourObserveValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
这里的
yourObserveValueForKeyPath
, 是以 block 的形式
#pragma mark **- Overridden Methods**
static void kvo_setter(id self, SEL _cmd, id newValue)
{
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = getterForSetter(setterName);
// 判定属性存在
if (!getterName) {
NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:reason
userInfo:nil];
return;
}
id oldValue = [self valueForKey:getterName];
struct objc_super superclazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
// cast our pointer so the compiler won't complain
void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
// call super's setter, which is original class's setter method
// 调父类的方法
objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
// look up observers and call the blocks
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kKVOAssociatedObservers));
for (ObservationInfo *each in observers) {
if ([each.key isEqualToString:getterName]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 调观察属性变更的 block
each.block(self, getterName, oldValue, newValue);
});
}
}
}
移除观察者,方法
在这里,移除观察者,就是把关联到的,观察信息中,属性的那一条移除
就算把观察信息,全部移完了,该对象依旧指向其子类
保留部分观察效果
因为调用观察的方法 / block, 是依据记录的观察信息
没有观察信息,就不会调用相关的方法
所以没有影响
- (void)_removeObserver:(NSObject *)observer forKey:(NSString *)key
{
NSMutableArray* observers = objc_getAssociatedObject(self, (__bridge const void *)(kKVOAssociatedObservers));
ObservationInfo *infoToRemove;
for (ObservationInfo* info in observers) {
if (info.observer == observer && [info.key isEqual:key]) {
infoToRemove = info;
break;
}
}
[observers removeObject:infoToRemove];
}
自动移除,观察者
就是对于简单的场景,不需要调用 - (void)_removeObserver:
通过 runtime 选一个时机
其子类的对象释放的时候,
也就是待观察的对象释放的时候,
调用移除观察者的方法
hook 掉,其子类的 dealloc
方法
- (Class)makeKvoClassWithOriginalClassName:
方法中,
给其子类,添加 dealloc
方法
// 添加 dealloc
SEL deallocSel = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod(originalClazz, deallocSel);
const char * deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(kvoClazz, deallocSel, (IMP)yourDealloc, deallocTypes);
objc_registerClassPair(kvoClazz);
yourDealloc
的实现,很简单
被观察的对象,都没有
添加的观察信息,全部移除
void yourDealloc(id self, SEL _cmd){
NSMutableArray* observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
[observers removeAllObjects];
}