布景

iOS 16 崩了: /post/715360…

iOS 16 又崩了: /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. 我只剪切了中心代码,希望看懂并认可后再选用这个计划。