iOS MessageThrottle 防抖与 RAC 的冲突
崩溃???
今天在开发一个列表需求, 由于有非常多因素导致列表变化, 所以需要方法节流(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
完成的状态监控,
[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 的做法。
if ([className hasPrefix:MTSubclassPrefix]) {
cls = baseClass;
}
else if (mt_object_isClass(target)) {
cls = target;
}
else if (statedClass != baseClass) {
cls = baseClass;
}
else {
const char *subclassName = [MTSubclassPrefix stringByAppendingString:className].UTF8String;
Class subclass = objc_getClass(subclassName);
if (subclass == nil) {
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
if (subclass == nil) {
NSLog(@"objc_allocateClassPair failed to allocate class %s.", subclassName);
return NO;
}
mt_hookedGetClass(subclass, statedClass);
mt_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
object_setClass(target, subclass);
cls = subclass;
}
我们再来回顾一下整个过程, 二级页的 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 互不影响的目的.
本文作者: wyanassert
本文链接: https://blog.wyan.vip/2021/04/MessageThrottle.html
版权声明: 转载请注明出处!