困而知之:KVO 为什么经典?

KVO 面试必问,非常经典

引子: 小灰面试小白

  • 小灰问: “什么是 KVO?”

小白答: “键值观察,观察者模式”

  • 再问: “什么时候用到 KVO?”

答: “ 值修改,同时改变饼图和柱状图的显示 ”

小灰呵呵,这不就是书上写的?

好一点的答案: AVFoundation

1, 音频应用: AVAudio​Session 监测音量 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];

}

github repo

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值