注册

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 的值

72986dc8c59eb0ed2998d813132d5cc7.png

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

结论

修复方案

找到 nextresponder_UIRemoteInputViewController 的类,hook 掉它的 nextresponder 方法,在new_nextresponder 方法里面判断,如果 allowsWeakReference == NO 则 return 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. 我只剪切了核心代码,希望看懂并认可后再采用这个方案。

作者:yuec
链接:https://juejin.cn/post/7240789855138873403
来源:稀土掘金

0 个评论

要回复文章请先登录注册