崩溃???
今天在开发一个列表需求, 由于有非常多因素导致列表变化, 所以需要方法节流(throttle), 直接使用了MessageThrottle 这个第三方库. 然后由于需求本身也使用了 ReactiveObjC 来开发, 当页面的 vm 跟 RAC 绑定, 然后又使用 MessageThrottle 时, 退出页面, RAC开始了释放逻辑, 而 MessageThrottle 防抖模式下, 延时执行被”节流”的方法, 最终导致应用崩溃.
节流 & 防抖 MessageThrottle 简析
第三方库导致的崩溃, 一般直接看源码, Objective-C Message Throttle and Debounce 这篇文章是MessageThrottle作者写的, 有详细的介绍.
我简单描述一下流程
要对目标方法节流或者做防抖, 先得 hook 该方法, 控制方法在不该执行的时候不去执行.
MessageThrottle采用的思路是生成一个私有子类, 并将示例的isa指针指向该子类, 在私有子类 hook 方法不会影响已有的逻辑,KVO也是这样的实现. 打个比方, 有一个对象a, class 是A, 然后创建私有子类Prefix_A, 然后通过object_setClass(a, Prefix_A)来修改 对象a的isa指针, 指向Prefix_A. 现在a对外还是A的实例, 但是isa指向了Prefix_A了.

利用
forwardInvocation转发消息(这个过程作者在另一篇文章MessageThrottle Safety 有详细介绍), 由于新生成的__mt_aaa方法并不在实例a中, 依次触发 OC 的resolveInstanceMethod,forwardingTargetForSelector,forwardInvocation.resolvedInstanceMethod适合给类/对象动态添加一个相应的实现,forwardingTargetForSelector适合将消息转发给其他对象处理,而 MessageThrottle 以及 Aspect 都使用了forwardInvocation来做 hook.真正开始处理执行 NSInvocation. 在
mt_handleInvocation方法中, 判定了当前节流模式, 并做相应处理.MTPerformModeFirstly执行最靠前发送的消息,后面发送的消息会被忽略MTPerformModeLast执行最靠后发送的消息,前面发送的消息会被忽略,执行时间会有延时MTPerformModeDebounce消息发送后延迟一段时间执行,如果在这段时间内继续发送消息,则重新计时
将执行的
invocation的selector换成 hook 之后的__mt_aaa方法, 当前的实例a->isa指向私有子类Prefix_A, 然后延时执行 [invocation invoke], 最终达到 “节流 & 防抖” 的目的.
RAC 为什么会对 MessageThrottle 产生影响?
上面说了, MessageThrottle 的 hook 思路与 KVO 类似, 而 RAC 也是用 KVO 完成的状态监控,
1 | [strongTarget addObserver:RACKVOProxy.sharedProxy forKeyPath:self.keyPath options:options context:(__bridge void *)self]; |
在 RAC 中这句话断点, 执行前后, strongTarget->isa 由 A 变成了 NSKVONotifying_A, 而 MessageThrottle 有一段针对 KVO 的优化, 还是引用作者的文章MessageThrottle Safety:
MessageThrottle 在 hook 一个对象的时候也会动态创建带前缀 MTSubclassPrefix 的子类,但是不会像 KVO 那样无脑创建,而是先判断通过 class 与 objc_getClass() 获取到的类是否相同。如果不同,则说明已经有现成的子类了,直接在 objc_getClass() 获取的类中 hook 就行了。这里是借鉴了 Aspects 的做法。
1 | if ([className hasPrefix:MTSubclassPrefix]) { |
我们再来回顾一下整个过程, 二级页的 ViewModel vm 首先绑定 RAC, vm->isa 由 VM 变成 NSKVONotifying_VM, 然后使用 MessageThrottle 对方法 aaa 进行节流, 在子类 NSKVONotifying_VM 中生成 __mt_aaa 方法, 正常情况下, 在 Invocation 调用的时候, vm->isa 指向子类NSKVONotifying_VM, [invocation invoke] 调用正常. 但是从二级页退出之后, RAC 自动开始解绑, vm 不再受 KVO 的影响, vm->isa 变回 VM(即 vm.class). 由于 MessageThrottle 在部分情况下会延时执行 [invocation invoke], 此时 vm->isa 不再指向子类, 找不到__mt_aaa 方法, 就直接崩溃了.
问题总结 & 解决思路
- RAC 使用
KVO绑定 vm, 并在退出页面后自动改变isa不再指向私有子类. - MessageThrottle 使用了
KVO生成的私有子类, 插入方法, 但是在isa不再指向私有子类后, 延时调用找不到替换的方法, 导致崩溃. - 最简单的解决思路就是 不要让 MessageThrottle 进行优化, 再写一个 object 转发方法, RAC 继续绑定
vm,vm通过 object 控制页面更新, object 使用 MessageThrottle 来节流&防抖. 以此达到 RAC 与 MessageThrottle 互不影响的目的.