布景
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 == 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;
}
友谊提示
- 计划里面触及到了两个私有类,建议都运用开关下发,避免审核的危险。
- 体系 crash 的修正还是老规矩,必定要加好开关,限制住体系版别,在修正计划触发其它问题的时分能够及时回滚,hook 存在必定的危险,这个计划 hook 的点相对较小了。
- 我只剪切了中心代码,希望看懂并认可后再选用这个计划。