目录
一、前言
二、DNS解析崩溃
1. 背景
2. 问题分析
3. 解决过程
三、MediaCodec 状态异常崩溃
1. 背景
2. 问题分析
3. 解决过程
四、bio多线程环境崩溃
1. 背景
2. 问题分析
3. 解决过程
五、小米Android15 焦点处理空指针崩溃
1. 背景
2. 问题分析
3. 解决过程
六、总结
一、前言
通过修复历史遗留的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出去来规避,音视频的同学尝试后发现不可行,原因如下:
a.void MediaCodec::postPendingRepliesAndDeferredMessages(std::string origin, status_t err): 匹配origin是否为特征字符串(postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING);很多设备找不到这个符号不可行;
b.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.java
https://cs.android.com/android/platform/superproject/+/android-11.0.0_r48:external/conscrypt/repackaged/common/src/main/java/com/android/org/conscrypt/NativeCrypto.java
https://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为空的情况有两种:
- 未调用setView方法前触发窗口焦点变化事件(只有setView方法才会给mView赋不为空的值)。
- 先正常调用setView使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类的崩溃显著减少,希望这些经验对大家有所帮助。