写点什么

得物 Android Crash 治理实践

作者:得物技术
  • 2025-03-13
    上海
  • 本文字数:10407 字

    阅读完需:约 34 分钟

得物 Android Crash 治理实践

一、前言

通过修复历史遗留的 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 startonMessageReceived_proxy startonMessageReceived_proxy 1__android_log_assert startonMessageReceived_proxy 3onMessageReceived_proxy endmain 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 startonMessageReceived_proxy startonMessageReceived_proxy 1__android_log_assert startonMessageReceived_proxy 3onMessageReceived_proxy endmain 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 referenceat 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 类的崩溃显著减少,希望这些经验对大家有所帮助。


文 / 亚鹏


关注得物技术,每周更新技术干货


要是觉得文章对你有帮助的话,欢迎评论转发点赞~


未经得物技术许可严禁转载,否则依法追究法律责任。

发布于: 刚刚阅读数: 4
用户头像

得物技术

关注

得物APP技术部 2019-11-13 加入

关注微信公众号「得物技术」

评论

发布
暂无评论
得物 Android Crash 治理实践_android_得物技术_InfoQ写作社区