一、前言
通过修复历史遗留的 Crash 漏报问题(包括端侧 SDK 采集的兼容性优化及 Crash 平台的数据消费机制完善),得物 Android 端的 Crash 监控体系得到显著增强,使得历史 Crash 数据的完整捕获能力得到系统性改善,相应 Crash 指标也有所上升,经过架构以及各团队的共同努力下,崩溃率已从最高的万 2 降至目前的万 1.1 到万 1.5,其中疑难问题占比约 90%、因系统 bug 导致的 Crash 占比约 40%,在本文中将简要介绍一些较典型的系统 Crash 的治理过程。
二、DNS 解析崩溃
背景
Android11 及以下版本在 DNS 解析过程中的有几率产生野指针问题导致的 Native Crash,其中 Android9 占比最高。
堆栈与上报趋势
at libcore.io.Linux.android_getaddrinfo(Linux.java)
at libcore.io.BlockGuardOs.android_getaddrinfo(BlockGuardOs.java:172)
at java.net.InetAddress.parseNumericAddressNoThrow(InetAddress.java:1631)
at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:96)
at java.net.InetAddress.getAllByName(InetAddress.java:1154)
#00 pc 000000000003b938 /system/lib64/libc.so (android_detectaddrtype+1164)
#01 pc 000000000003b454 /system/lib64/libc.so (android_getaddrinfofornet+72)
#02 pc 000000000002b5f4 /system/lib64/libjavacore.so (_ZL25Linux_android_getaddrinfoP7_JNIEnvP8_jobjectP8_jstringS2_i+336)
复制代码
问题分析
崩溃入口方法 InetAddress.getAllByName 用于根据指定的主机名返回与之关联的所有 IP 地址,它会根据系统配置的名称服务进行解析,沿着调用链查看源码发现在 parseNumericAddressNoThrow 方法内部调用 Libcore.os.android_getaddrinfo 时中有 try catch 的容错逻辑,继续查看后续调用的 c++的源码,在调用 android_getaddrinfofornet 函数返回值不为 0 时抛出 GaiException 异常。
https://cs.android.com/android/platform/superproject/+/android-9.0.0_r49:libcore/ojluni/src/main/java/java/net/InetAddress.java
static InetAddress parseNumericAddressNoThrow(String address) {
// Accept IPv6 addresses (only) in square brackets for compatibility.
if (address.startsWith("[") && address.endsWith("]") && address.indexOf(':') != -1) {
address = address.substring(1, address.length() - 1);
}
StructAddrinfo hints = new StructAddrinfo();
hints.ai_flags = AI_NUMERICHOST;
InetAddress[] addresses = null;
try {
addresses = Libcore.os.android_getaddrinfo(address, hints, NETID_UNSET);
} catch (GaiException ignored) {
}
return (addresses != null) ? addresses[0] : null;
}
复制代码
https://cs.android.com/android/platform/superproject/+/master:libcore/luni/src/main/native/libcore_io_Linux.cpp?q=Linux_android_getaddrinfo&ss=android%2Fplatform%2Fsuperproject
static jobjectArray Linux_android_getaddrinfo(JNIEnv* env, jobject, jstring javaNode,
jobject javaHints, jint netId) {
......
int rc = android_getaddrinfofornet(node.c_str(), NULL, &hints, netId, 0, &addressList);
std::unique_ptr<addrinfo, addrinfo_deleter> addressListDeleter(addressList);
if (rc != 0) {
throwGaiException(env, "android_getaddrinfo", rc);
return NULL;
}
......
return result;
}
复制代码
解决过程
解决思路是代理 android_getaddrinfofornet 函数,捕捉调用原函数过程中出现的段错误信号,接着吃掉这个信号并返回-1,使之转换为 JAVA 异常进而走进 parseNumericAddressNoThrow 方法的容错逻辑,和负责网络的同学提前做了沟通,确定此流程对业务没有影响后开始解决。
首先使用 inline-hook 代理了 android_getaddrinfofornet 函数,接着使用字节封装好的 native try catch 工具做吃掉段错误信号并返回-1 的,字节工具内部原理是在 try 块的开始使用 sigsetjmp 打个锚点并快照当前寄存器的值,然后设置信号量处理器并关联当前线程,在 catch 块中解绑线程与信号的关联并执行业务兜底代码,在捕捉到信号时通过 siglongjmp 函数长跳转到 catch 块中,感兴趣的同学可以用下面精简后的 demo 试试,以下代码保存为 mem_err.c,执行 gcc ./mem_err.c;./a.out
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
struct sigaction old;
static sigjmp_buf buf;
void SIGSEGV_handler(int sig, siginfo_t *info, void *ucontext) {
printf("信号处理 sig: %d, code: %d\n", sig, info->si_code);
siglongjmp(buf, -1);
}
int main() {
if (!sigsetjmp(buf, 0)) {
struct sigaction sa;
sa.sa_sigaction = SIGSEGV_handler;
sigaction(SIGSEGV, &sa, &old);
printf("try exec\n");
//产生段错误
int *ptr = NULL;
*ptr = 1;
printf("try-block end\n");//走不到
} else {
printf("catch exec\n");
sigaction(SIGSEGV, &old, NULL);
}
printf("main func end\n");
return 0;
}
//输出以下日志
//try exec
//信号处理 sig: 11, code: 2
//catch exec
//main func end
复制代码
inline-hook 库: https://github.com/bytedance/android-inline-hook
字节 native try catch 工具: https://github.com/bytedance/android-inline-hook/blob/main/shadowhook/src/main/cpp/common/bytesig.c
三、MediaCodec 状态异常崩溃
背景
在 Android 11 系统库的音视频播放过程中,偶尔会出现因状态异常导致的 SIGABRT 崩溃。音视频团队反馈指出,这是 Android 11 的一个系统 bug。随后,我们协助音视频团队通过 hook 解决了这一问题。
堆栈与上报趋势
#00 pc 0000000000089b1c /apex/com.android.runtime/lib64/bionic/libc.so (abort+164)
#01 pc 000000000055ed78 /apex/com.android.art/lib64/libart.so (_ZN3art7Runtime5AbortEPKc+2308)
#02 pc 0000000000013978 /system/lib64/libbase.so (_ZZN7android4base10SetAborterEONSt3__18functionIFvPKcEEEEN3$_38__invokeES4_+76)
#03 pc 0000000000006e30 /system/lib64/liblog.so (__android_log_assert+336)
#04 pc 0000000000122074 /system/lib64/libstagefright.so (_ZN7android10MediaCodec37postPendingRepliesAndDeferredMessagesENSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEERKNS_2spINS_8AMessageEEE+720)
#05 pc 00000000001215cc /system/lib64/libstagefright.so (_ZN7android10MediaCodec37postPendingRepliesAndDeferredMessagesENSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEi+244)
#06 pc 000000000011c308 /system/lib64/libstagefright.so (_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE+8752)
#07 pc 0000000000017814 /system/lib64/libstagefright_foundation.so (_ZN7android8AHandler14deliverMessageERKNS_2spINS_8AMessageEEE+84)
#08 pc 000000000001d9cc /system/lib64/libstagefright_foundation.so (_ZN7android8AMessage7deliverEv+188)
#09 pc 0000000000018b48 /system/lib64/libstagefright_foundation.so (_ZN7android7ALooper4loopEv+572)
#10 pc 0000000000015598 /system/lib64/libutils.so (_ZN7android6Thread11_threadLoopEPv+460)
#11 pc 00000000000a1d6c /system/lib64/libandroid_runtime.so (_ZN7android14AndroidRuntime15javaThreadShellEPv+144)
#12 pc 0000000000014d94 /system/lib64/libutils.so (_ZN13thread_data_t10trampolineEPKS_+412)
#13 pc 00000000000eba94 /apex/com.android.runtime/lib64/bionic/libc.so (_ZL15__pthread_startPv+64)
#14 pc 000000000008bd80 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
复制代码
问题分析
根据堆栈内容分析 Android11 的源码以及结合 SIGABRT 信号采集到的信息(postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING),找到崩溃发生在 onMessageReceived 函数处理 kWhatRelease 类型消息的过程中,onMessageReceived 函数连续收到两条消息,第一条是 kWhatError:STOPPING,第二条是 kWhatRelease:STOPPING 此时因 mReplyID 已经被置为空,因此走到判空抛异常的逻辑。
https://cs.android.com/android/_/android/platform/frameworks/av/+/refs/tags/android-11.0.0_r48:media/libstagefright/MediaCodec.cpp;l=2280;drc=789055bbcb4560b42faf19103b1cda5534e8f9cb;bpv=0;bpt=0
对比 Android12 的源码,在处理 kWhatRelease 事件且状态为 STOPPING 抛异常前,增加了对 mReplyID 不为空的判断来规避这个问题。
https://cs.android.com/android/_/android/platform/frameworks/av/+/ca0c3286a4790a4de2d90cb275ae89a9601b805b:media/libstagefright/MediaCodec.cpp;dlc=7327aab894f6c456ea16c95b64134841da8d5737
解决过程
Android12 的修复方式意味着上述三个条件结合下吃掉异常是符合预期的,接下来就是想办法通过 hook Android11 使逻辑对齐 Android12。
【初探】最先想到的办法是代理相关函数通过判断走到这个场景时提前 return 出去来规避,音视频的同学尝试后发现不可行,原因如下:
void MediaCodec::postPendingRepliesAndDeferredMessages(std::string origin, status_t err): 匹配 origin 是否为特征字符串(postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING);很多设备找不到这个符号不可行;
void MediaCodec::onMessageReceived(const sp&msg): 已知 MediaCodec 实例的内存首地址,需要通过 hardcode 偏移量来获取 mReplay、mState 两个字段,这里又缺少可供校验正确性的特征,风险略大担心有不同机型的兼容性问题(不同机型新增、删除字段导致偏移量不准)。
【踩坑】接着尝试使用与修复 DNS 崩溃类似思路的保护方案,使用 inline-hook 代理 onMessageReceived 函数调用原函数时使用 setjmp 打锚点,然后使用 plt hook 代理_android_log_assert 函数并在内部检测错误信息为特征字符串时通过 longjmp 跳转到 onMessageReceived 函数的锚点并作 return 操作,精简后的 demo 如下:
Plt-hook 库: https://github.com/iqiyi/xHook
#include <iostream>
#include <setjmp.h>
#include <csignal>
static thread_local jmp_buf _buf;
void *origin_onMessageReceived = nullptr;
void *origin__android_log_assert = nullptr;
void _android_log_assert_proxy(const char* cond, const char *tag, const char* fmt, ...) {
//模拟liblog.so的__android_log_assert函数
std::cout << "__android_log_assert start" << std::endl;
if (!strncmp(fmt, "postPendingRepliesAndDeferredMessages: mReplyID == null", 55)) {
longjmp(_buf, -1);
}
//模拟调用origin__android_log_assert,产生崩溃
raise(SIGABRT);
}
void onMessageReceived_proxy(void *thiz, void *msg) {
std::cout << "onMessageReceived_proxy start" << std::endl;
if (!setjmp(_buf)) {
//模拟调用onMessageReceived原函数(origin_onMessageReceived)进入崩溃流程
std::cout << "onMessageReceived_proxy 1" << std::endl;
_android_log_assert_proxy(nullptr, nullptr, "postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING");
std::cout << "onMessageReceived_proxy 2" << std::endl;//走不到
} else {
//保护后从此处返回
std::cout << "onMessageReceived_proxy 3" << std::endl;
}
std::cout << "onMessageReceived_proxy end" << std::endl;
}
int main() {
std::cout << "main func start" << std::endl;
/**
inline-hook: shadowhook_hook_sym_name("libstagefright.so","_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE",(void *) onMessageReceived_proxy, (void **) &origin_onMessageReceived);
plhook: xh_core_register("libstagefright.so", "__android_log_assert", (void *) (_android_log_assert_proxy), (void **) (&origin__android_log_assert));
*/
//模拟调用libstagefright.so的_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE函数
onMessageReceived_proxy(nullptr, nullptr);
std::cout << "main func end" << std::endl;
return 0;
}
/**
日志输出
main func start
onMessageReceived_proxy start
onMessageReceived_proxy 1
__android_log_assert start
onMessageReceived_proxy 3
onMessageReceived_proxy end
main func end
*/
复制代码
线下一阵操作猛如虎经测试保护逻辑符合预期,但是在灰度期间踩到栈溢出保护导致错误转移的坑,堆栈如下:
#00 pc 000000000004e40c /apex/com.android.runtime/lib64/bionic/libc.so (abort+164)
#01 pc 0000000000062730 /apex/com.android.runtime/lib64/bionic/libc.so (__stack_chk_fail+20)
#02 pc 000000000000a768 /data/app/~~JaQm4SU8wxP7T2GaSWxYkQ==/com.shizhuang.duapp-N5RFIB8WurdccMgAVsBang==/lib/arm64/libduhook.so (_ZN25CrashMediaCodecProtection5proxyEPvS0_)
#03 pc 0000000001091c0c [anon:scudo:primary]
复制代码
*关于栈溢出保护机制感兴趣的同学可以参考这篇文章https://bbs.kanxue.com/thread-221762-1.htm
(CSPP 第 3 版 “3.10.3 内存越界引用和缓冲区溢出”章节讲的更详细)*
longjmp 函数只是恢复寄存器的值后从锚点处再次返回,过程中也唯一可能会操作栈祯只有 inline-hook,当时怀疑是与 setjmp/longjmp 机制不兼容,由于 inline-hook 内部逻辑大量使用汇编来实现排查起来比较困难,因此这个问题困扰比较久,网上的资料提到可以使用代理出错函数(__stack_chk_fail)或者编译 so 时增加参数不让编译器生成保护代码来绕过,这两种方式影响面都比较大所以未采用。有了前面的怀疑点想到使用 c++的 try catch 机制来做跨函数域的跳转,大致的思路同上只是把 setjmp 替换为 c++的 try catch,把 longjmp 替换为 throw exception,精简后的 demo 如下:
c++异常机制介绍: https://baiy.cn/doc/cpp/inside_exception.htm
#include <iostream>
#include <csignal>
void *origin_onMessageReceived = nullptr;
void *origin__android_log_assert = nullptr;
class MyCustomException : public std::exception {
public:
explicit MyCustomException(const std::string& message)
: msg_(message) {}
virtual const char* what() const noexcept override {
return msg_.c_str();
}
private:
std::string msg_;
};
void _android_log_assert_proxy(const char* cond, const char *tag, const char* fmt, ...) {
//模拟liblog.so的__android_log_assert函数
std::cout << "__android_log_assert start" << std::endl;
if (!strncmp(fmt, "postPendingRepliesAndDeferredMessages: mReplyID == null", 55)) {
throw MyCustomException("postPendingRepliesAndDeferredMessages: mReplyID == null");
}
//模拟调用origin__android_log_assert,产生崩溃
raise(SIGABRT);
}
void onMessageReceived_proxy(void *thiz, void *msg) {
std::cout << "onMessageReceived_proxy start" << std::endl;
try {
//模拟调用onMessageReceived原函数(origin_onMessageReceived)进入崩溃流程
std::cout << "onMessageReceived_proxy 1" << std::endl;
_android_log_assert_proxy(nullptr, nullptr, "postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING");
std::cout << "onMessageReceived_proxy 2" << std::endl;//走不到
} catch (const MyCustomException& e) {
//保护后从此处返回
std::cout << "onMessageReceived_proxy 3" << std::endl;
}
std::cout << "onMessageReceived_proxy end" << std::endl;
}
int main() {
std::cout << "main func start" << std::endl;
/**
inline-hook: shadowhook_hook_sym_name("libstagefright.so","_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE",(void *) onMessageReceived_proxy, (void **) &origin_onMessageReceived);
plhook: xh_core_register("libstagefright.so", "__android_log_assert", (void *) (_android_log_assert_proxy), (void **) (&origin__android_log_assert));
*/
//模拟调用libstagefright.so的_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE函数
onMessageReceived_proxy(nullptr, nullptr);
std::cout << "main func end" << std::endl;
return 0;
}
/**
日志输出
main func start
onMessageReceived_proxy start
onMessageReceived_proxy 1
__android_log_assert start
onMessageReceived_proxy 3
onMessageReceived_proxy end
main func end
*/
复制代码
灰度上线后发现有设备走到了_android_log_assert 代理函数中的 throw 逻辑,但是未按预期走到 catch 块而是把错误又转移为" terminating with uncaught exception of type" ,有点搞心态啊。
【柳暗花明】C++的异常处理机制在 throw 执行时,会开始在调用栈中向上查找匹配的 catch 块,检查每一个函数直到找到一个具有合适类型的 catch 块,上述的错误信息代表未找到匹配的 catch 块。从转移的堆栈中注意到没有 onMessageReceived 代理函数的堆栈,此时基于 inline-hook 的原理(修改原函数前面的汇编代码跳转到代理函数)又怀疑到它身上,再次排查代码时发现代理函数开头漏写了一个宏,在 inline-hook 中 SHADOWHOOK_STACK_SCOPE 就是来管理栈祯的,因此出现找不到 catch 块以及前面 longjmp 的问题就不奇怪了。加上这个宏以后柳暗花明,重新放量后保护逻辑按预期执行并且保护生效后视频播放正常。和音视频的小伙伴一努力下,经历了几个版本终于解决了这个系统 bug,目前仅剩老版本 App 有零星的上报。
四、bio 多线程环境崩溃
背景
Android 11 Socket close 过程中在多线程场景下有几率产生野指针问题导致 Native Crash,现象是多个线程同时 close 连接时,一个线程已销毁了 bio 的上下文,另外一个线程仍执行 close 并在此过程中尝试获取这个 bio 有多少未写出去的字节数时出现野指针导致的段错误。此问题从 21 年首次上报以来在得物的 Crash 列表中一直处于较前的位置。
堆栈与上报趋势
at com.android.org.conscrypt.NativeCrypto.SSL_pending_written_bytes_in_BIO(Native method)
at com.android.org.conscrypt.NativeSsl$BioWrapper.getPendingWrittenBytes(NativeSsl.java:660)
at com.android.org.conscrypt.ConscryptEngine.pendingOutboundEncryptedBytes(ConscryptEngine.java:566)
at com.android.org.conscrypt.ConscryptEngineSocket.drainOutgoingQueue(ConscryptEngineSocket.java:584)
at com.android.org.conscrypt.ConscryptEngineSocket.close(ConscryptEngineSocket.java:480)
at okhttp3.internal.Util.closeQuietly_aroundBody0(Util.java:1)
at okhttp3.internal.Util$AjcClosure1.run(Util.java:1)
at org.aspectj.runtime.reflect.JoinPointImpl.proceed(JoinPointImpl.java:3)
at com.shizhuang.duapp.common.aspect.ThirdSdkAspect.t(ThirdSdkAspect.java:1)
at okhttp3.internal.Util.closeQuietly(Util.java:3)
at okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.java:42)
at okhttp3.internal.connection.ExchangeFinder.findHealthyConnection(ExchangeFinder.java:1)
at okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.java:6)
at okhttp3.internal.connection.Transmitter.newExchange(Transmitter.java:5)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:5)
#00 pc 0000000000064060 /system/lib64/libcrypto.so (bio_ctrl+144)
#01 pc 00000000000615d8 /system/lib64/libcrypto.so (BIO_ctrl_pending+40)
#02 pc 00000000000387dc /apex/com.android.conscrypt/lib64/libjavacrypto.so (_ZL45NativeCrypto_SSL_pending_written_bytes_in_BIOP7_JNIEnvP7_jclassl+20)
复制代码
问题分析
从设备分布上看,出问题都全是 Android 11 且各个国内厂商的设备都有,怀疑是 Android 11 引入的 bug,对比了 Android 11 和 Android 12 的源码,发现在 Android12 崩溃堆栈中的相关类 com.android.org.conscrypt.NativeSsl$BioWrapper 有四个方法增加了读写锁,此时怀疑是多线程问题,通过搜索 Android 源码的相关 issue 以及差异代码的 MR 描述信息,进一步确认此结论。通过源码进一步分析发现 NativeSsl 的所有加锁的方法,会分发到 NativeCrypto.java 中的 native 方法,最终调用到 native_crypto.cc 中的 JNI 函数,如果能 hook 到相关的 native 函数并在 Native 层实现与 Android12 相同的读写锁逻辑,这个问题就可以解决了。
https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:external/conscrypt/repackaged/common/src/main/java/com/android/org/conscrypt/NativeSsl.javahttps://cs.android.com/android/platform/superproject/+/android-11.0.0_r48:external/conscrypt/repackaged/common/src/main/java/com/android/org/conscrypt/NativeCrypto.javahttps://cs.android.com/android/platform/superproject/+/android-11.0.0_r48:external/conscrypt/common/src/jni/main/cpp/conscrypt/native_crypto.cc
解决过程
通过 JNI hook 代理 Android12 中增加锁的相关函数,当走到代理函数中时,先分发到 JAVA 层通过反射获取 ReadWriteLock 实例并上锁再通过跳板函数调用原来的 JNI 函数,此时就完成了对 Android12 增量锁逻辑的复刻。经历了两个版本的灰度 hook 方案已稳定在线上运行,期间无因 hook 导致的网络不可用和其它崩溃问题,目前开关放全量的版本崩溃设备数已降为 0。
JNI hook 原理,以及详细修复过程: https://blog.dewu-inc.com/article/MTMwNDU?fromType=personal_blog
五、小米 Android15 焦点处理空指针崩溃
背景
随着 Android15 开放公测,焦点处理过程中发生的空指针问题逐步增多,并在 1 月份上升到 Top。
堆栈与上报趋势
java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.ViewGroup$LayoutParams android.view.View.getLayoutParams()' on a null object reference
at android.view.ViewRootImpl.handleWindowFocusChanged(ViewRootImpl.java:5307)
at android.view.ViewRootImpl.-$$Nest$mhandleWindowFocusChanged(Unknown Source:0)
at android.view.ViewRootImpl$ViewRootHandler.handleMessageImpl(ViewRootImpl.java:7715)
at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:7611)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loopOnce(Looper.java:249)
at android.os.Looper.loop(Looper.java:337)
at android.app.ActivityThread.main(ActivityThread.java:9568)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)
复制代码
问题分析
通过分析 ASOP 的源码,崩溃的触发点是 mView 字段为空。
https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/ViewRootImpl.java;drc=98e96368cc73432efbacd6fbcf61fe789dcec0ee;l=7243?q=ViewRootImpl
源码中 mView 为空的情况有两种:
结合前置判断了 mAdded 为 true 才会走到崩溃点,在源码中寻找到只有先正常调用 setView 以后在调用 dispatchDetachedFromWindow 时才满足 mAdded=true、mView=null 的条件,从采集的 logcat 日志中可以证明这一点,此时基本可以定位根因是窗口销毁与焦点事件处理的时序问题。
解决过程
在问题初期,尝试通过 Hook 拦截 handleWindowFocusChanged 方法增加防御:当检测到 mView 为空时直接中断后续逻辑执行。本地验证阶段,通过在 Android 15 设备上高频触发商详页 Dialog 弹窗的焦点获取与关闭操作,未复现线上崩溃问题。考虑到 Hook 方案的侵入性风险 ,且无法本地测试,最终放弃此方案上线。
通过崩溃日志分析发现,问题设备 100% 集中在小米/红米机型,而该品牌在 Android 15 DAU 中仅占 36% ,因此怀疑是 MIUI 对 Android15 某些定制功能有 bug。经与小米技术团队数周的沟通与联合排查,最终小米在 v2.0.28 版本修复了此问题,需要用户升级 ROM 解决,目前>=2.0.28 的 MIUI 设备无此问题的上报。
六、总结
通过上述问题的治理,系统 bug 类的崩溃显著减少,希望这些经验对大家有所帮助。
文 / 亚鹏
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
评论