iOS 16 又又崩了

背景

iOS 16 崩了: juejin.cn/post/715360…

iOS 16 又崩了: juejin.cn/post/722551…

本文分析的崩溃同样只在 iOS16 系统会触发,我们的 APP 每天有 2k+ 崩溃上报。

崩溃原因:

Cannot form weak reference to instance (0x1107c6200) of class _UIRemoteInputViewController. It is possible that this object was over-released, or is in the process of deallocation.

无法 weak 引用类型为 _UIRemoteInputViewController 的对象。可能是因为这个对象被过度释放了,或者正在被释放。weak 引用已经释放或者正在释放的对象会 crash,这种崩溃业务侧经常见于在 dealloc 里面使用 __weak 修饰 self。

_UIRemoteInputViewController 明显和键盘相关,看了下用户的日志也都是在弹出键盘后崩了。

崩溃堆栈:

0	libsystem_kernel.dylib	___abort_with_payload()
1	libsystem_kernel.dylib	_abort_with_payload_wrapper_internal()
2	libsystem_kernel.dylib	_abort_with_reason()
3	libobjc.A.dylib	_objc_fatalv(unsigned long long, unsigned long long, char const*, char*)()
4	libobjc.A.dylib	_objc_fatal(char const*, ...)()
5	libobjc.A.dylib	_weak_register_no_lock()
6	libobjc.A.dylib	_objc_storeWeak()
7	UIKitCore	__UIResponderForwarderWantsForwardingFromResponder()
8	UIKitCore	___forwardTouchMethod_block_invoke()
9	CoreFoundation	___NSSET_IS_CALLING_OUT_TO_A_BLOCK__()
10	CoreFoundation	-[__NSSetM enumerateObjectsWithOptions:usingBlock:]()
11	UIKitCore	_forwardTouchMethod()
12	UIKitCore	-[UIWindow _sendTouchesForEvent:]()
13	UIKitCore	-[UIWindow sendEvent:]()
14	UIKitCore	-[UIApplication sendEvent:]()
15	UIKitCore	___dispatchPreprocessedEventFromEventQueue()
16	UIKitCore	___processEventQueue()
17	UIKitCore	___eventFetcherSourceCallback()
18	CoreFoundation	___CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__()
19	CoreFoundation	___CFRunLoopDoSource0()
20	CoreFoundation	___CFRunLoopDoSources0()
21	CoreFoundation	___CFRunLoopRun()
22	CoreFoundation	_CFRunLoopRunSpecific()
23	GraphicsServices	_GSEventRunModal()
24	UIKitCore	-[UIApplication _run]()
25	UIKitCore	_UIApplicationMain()

堆栈分析

崩溃发生在系统函数内部,先分析堆栈理解崩溃的上下文,好在 libobjc 有开源的代码,极大的提高了排查的效率。

_weak_register_no_lock

抛出 fatal errr 最上层的代码,删减部分非关键信息后如下。

id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
    objc_object *referent = (objc_object *)referent_id;
    if (deallocatingOptions == ReturnNilIfDeallocating ||
        deallocatingOptions == CrashIfDeallocating) {
        bool deallocating;
        if (!referent->ISA()->hasCustomRR()) {
            deallocating = referent->rootIsDeallocating();
        }
        else {
            deallocating =
            ! (*allowsWeakReference)(referent, @selector(allowsWeakReference));
        }

        if (deallocating) {
            if (deallocatingOptions == CrashIfDeallocating) {
                _objc_fatal("Cannot form weak reference to instance (%p) of " <=== 崩溃
                            "class %s. It is possible that this object was "
                            "over-released, or is in the process of deallocation.",
                            (void*)referent, object_getClassName((id)referent));
            } else {
                return nil;
            }
        }
    }
}

直接原因是  _UIRemoteInputViewController 实例的 allowsWeakReference 返回了 false。

options == CrashIfDeallocating 就会 crash。否则的话返回 nil。不过 CrashIfDeallocating 写死在了代码段,没有权限修改。整个 storeWeak 的调用链路上都没有可以 hook 的方法。

__UIResponderForwarderWantsForwardingFromResponder

调用 storeWeak 的地方反汇编

    if (r27 != 0x0) {
            r0 = [[&var_60 super] init];
            r27 = r0;
            if (r0 != 0x0) {
                    objc_storeWeak(r27 + 0x10, r25);
                    objc_storeWeak(r27 + 0x8, r26);
            }
    }

xcode debug r27 的值

<_UITouchForwardingRecipient: 0x2825651d0> - recorded phase = began, autocompleted phase = began, to responder: (null), from responder: (null)

otool 查看 _UITouchForwardingRecipient 这个类的成员变量

ivars          0x1cfb460 __OBJC_$_INSTANCE_VARIABLES__UITouchForwardingRecipient
    entsize   32
    count     4
    offset    0x1e445d0 _OBJC_IVAR_$__UITouchForwardingRecipient.fromResponder 8
    name      0x19c7af3 fromResponder
    type      0x1a621c5 @"UIResponder"
    alignment 3
    size      8
    offset    0x1e445d8 _OBJC_IVAR_$__UITouchForwardingRecipient.responder 16
    name      0x181977f responder
    type      0x1a621c5 @"UIResponder"

第一个 storeweak  赋值 offset 0x10 responder: UIResponder 取值 r25。

第二个 storeweak 赋值 offset 0x8 fromResponder: UIResponder 取值 r26。

XCode debug 采集 r25 r26 的值

fromResponder responder
UIView UITransitionView
UITransitionView xxxRootWindow
xxxRootWindow UIWindowScene
UIWindowScene UIApplication

到这里就比较清晰了,_UITouchForwardingRecipient 是在保存响应者链。其中_UITouchForwardingRecipient.responder = _UITouchForwardingRecipient.fromResponder.nextReponder(这里省略了一长串的证明过程,最近卷的厉害,没有时间整理之前的文档了)。崩溃发生在 objc_storeWeak(_UITouchForwardingRecipient.responder), 我们可以从 nextReponder 这个方法入手校验 responder 是否合法。

结论

修复方案

找到 nextresponder_UIRemoteInputViewController 的类,hook 掉它的 nextresponder 方法,在new_nextresponder 方法里面判断,如果 allowsWeakReference == NOreturn nil。 在崩溃的地址断点,可以找到这个类是 _UISizeTrackingView

- (UIResponder *)xxx_new_nextResponder {
    UIResponder *res = [self xxx_new_nextResponder];
    if (res == nil){
        return nil;
    }
    static Class nextResponderClass = nil;
    static bool initialize = false;
    if (initialize == false && nextResponderClass == nil) {
        nextResponderClass = NSClassFromString(@"_UIRemoteInputViewController");
        initialize = true;
    }
    
    if (nextResponderClass != nil && [res isKindOfClass:nextResponderClass]) {
        if ([res respondsToSelector:NSSelectorFromString(@"allowsWeakReference")]) {
            BOOL (*allowsWeakReference)(id, SEL) =
            (__typeof__(allowsWeakReference))class_getMethodImplementation([res class], NSSelectorFromString(@"allowsWeakReference"));
            if (allowsWeakReference && (IMP)allowsWeakReference != _objc_msgForward) {
                if (!allowsWeakReference(res, @selector(allowsWeakReference))) {
                    return nil;
                }
            }
        }
    }
    return res;
 }
    

友情提示

  1. 方案里面涉及到了两个私有类,建议都使用开关下发,避免审核的风险。
  2. 系统 crash 的修复还是老规矩,一定要加好开关,限制住系统版本,在修复方案触发其它问题的时候可以及时回滚,hook 存在一定的风险,这个方案 hook 的点相对较小了。
  3. 我只剪切了核心代码,希望看懂并认可后再采用这个方案。

猜你喜欢

转载自juejin.im/post/7240789855138873403