fundo
iOS 16 está quebrado: juejin.cn/post/715360…
iOS 16 está quebrado novamente: juejin.cn/post/722551…
As falhas analisadas neste artigo também são acionadas apenas no sistema iOS16, e nosso APP relata mais de 2 mil falhas todos os dias.
Motivo do acidente:
Não é possível formar uma referência fraca à instância (0x1107c6200) da classe _UIRemoteInputViewController. É possível que este objeto tenha sido liberado em excesso ou esteja em processo de desalocação.
Um objeto do tipo _UIRemoteInputViewController não pode ser referenciado fracamente. Pode ser que o objeto tenha sido liberado em excesso ou esteja em processo de liberação. Referências fracas a objetos que foram lançados ou estão sendo lançados travarão. Esse tipo de travamento é frequentemente visto no uso de __weak para modificar self em dealloc.
_UIRemoteInputViewController está obviamente relacionado ao teclado. Depois de ler o log do usuário, ele também trava após o teclado aparecer.
Pilha de falha:
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()
análise de pilha
A falha ocorreu dentro da função do sistema. Primeiro analise a pilha para entender o contexto da falha. Felizmente, libobjc tem código-fonte aberto, o que melhora muito a eficiência da solução de problemas.
_weak_register_no_lock
Throw fatal errr O código de nível superior, após excluir algumas informações não-chave, é o seguinte.
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;
}
}
}
}
O motivo direto é que o allowWeakReference da instância _UIRemoteInputViewController retorna 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
是否合法。
结论
修复方案
Encontre a classe nextresponder
cujo _UIRemoteInputViewController
é , conecte seu nextresponder
método new_nextresponder
e julgue o método, se allowsWeakReference == NO
então return nil
. Essa classe pode ser encontrada no ponto de interrupção no endereço da falha _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;
}
lembrete amigável
- O esquema envolve duas aulas particulares e é recomendável usar o switch para entregá-las para evitar o risco de auditoria.
- O reparo da falha do sistema ainda é uma regra antiga. Você deve adicionar uma opção para limitar a versão do sistema. Quando o plano de reparo acionar outros problemas, você pode reverter no tempo. Existem certos riscos no gancho e no ponto do gancho deste plano é relativamente pequeno.
- Eu apenas cortei o código principal e espero entendê-lo e aprová-lo antes de adotar esta solução.