背景
近3个月,抖音 Android 版面临一个多次触发线上报警的崩溃问题,全量版本和灰度版本的异常数据激增,该问题不仅容易触发报警,更成为了 Java Top 1 崩溃问题,带来巨大困扰,急需攻坚解决。
本文展现了具体的分析过程、优化思路和解决方案,同时提供了已集成该方案的实用工具。
初步分析
多维特征
我们以某发版期间数据为例进行分析:
- 机型方面:比较分散,有聚集部分
samsung sm-s9180 占比 top1
。 - 渠道方面:比较分散,华为、小米、三星占比 top3。
- ROM方面:比较分散。
- OS 版本方面:排除古老的 5.1.1 系统,
只有 10 以上版本,12/13/10 占比 top3
。 - App小版本方面,排除古老的版本,基本跟日活排序一致。
通过对多维特征分析,我们得出结论是:存在 OS 高版本和三星机型聚集特征。
崩溃堆栈
从下图我们看到,崩溃类型 TransactionTooLargeException,崩溃现场在 BinderProxy.transactNative 方法附近,崩溃提示消息 data parcel size xxx bytes
,崩溃时 Activity 在向 AMS 传递 stop 信息。初步分析这些信息,我们得出结论:
- 这是由 Binder 传输数据量超过阈值触发的崩溃。
- 排除其中无关的动态代理 ProxyInvocationHandler 的业务堆栈,其它都在系统堆栈中,无法直接定位到具体业务方解决。
崩溃日志
除了崩溃堆栈,我们也要分析崩溃日志,这样往往能够从中获得一些有价值的线索。以下,我们分别从聚合 logcat 日志和单次 logcat 日志进行分析。通过 应用性能监控全链路版(APMPlus)App端监控 的 logcat 词云,看到聚合日志 top1 是 JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = **
。这个是系统 Binder 传输数据失败打印的日志。
我们从源码里面找到打印日志的地方,发现是在 Native 层 Binder.cpp 里面,接收到 Binder 驱动的错误码 FAILED_TRANSACTION 之后,先打印错误日志,然后通过 jniThrowException 函数从 JNI 层进入 Java 层抛出异常。
通过对聚合 logcat 日志分析,我们得到结论:只有系统 Binder 传输失败日志,无业务相关日志,不确定是哪个业务不合理导致的崩溃。根据前面聚合 locat 日志关键字,我们翻看了几十个上报日志,发现在 JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = **
的后面紧接着出现 Tag 为 ActivityStopInfo
的 Bundle stats:
日志,其中一个如图所示。因此这时我们知道 Binder transaction 传输的 parcel 内容很大可能跟 Bundle 有关。
从打印的 Bundle stats:
树形结构看,只打印了系统的 view 和 fragment 占用 size,没有 App 业务相关占用 size,无法细分是哪里不合理。另外在日志 JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = **
的前面我们终于发现了跟业务相关的 Activity 是 com.ss.android.ugc.aweme.detail.ui.DetailActivity
。翻看了几十个上报日志,并不是每个日志都是这个 com.ss.android.ugc.aweme.detail.ui.DetailActivity
,还有 com.ss.android.ugc.aweme.shortvideo.ui.VideoPublishActivity
等。
经过对单次 logcat 日志分析,能够从单次 logcat 日志中知道是哪个 Activity stop 时出错,但是目前分析不出这些处于 stop 状态的 Activity 占比情况。
综合聚合和单次 logcat 日志看,无明显 App 业务逻辑聚合,只有系统日志,无法简单定位到某个业务解决。
初步结论
经过前面的分析,我们得出初步结论:
- 问题是 Activity stop 时给 AMS 传输 Bundle 超过 Binder 驱动限制的大小,触发了崩溃。
- 调用栈和日志无明显业务聚合,无法简单确定是哪个业务引入的问题。
因此,需要进行深入分析,一方面要从监控归因着手排查其中不合理的因素,另一方面也要从底层通用解决方案尝试突破限制彻底解决问题。
深入分析
下面将主要从崩溃堆栈进行深入分析,翻了几十个崩溃堆栈,我们发现并不是只有 Activity stop 调用栈,还有 Activity start 调用栈,主要包含两类,一个是 activityStopped 关键字的崩溃,占比约 97.25%;另一个是 handleStartActivity 关键字的崩溃,占比约 2.75%,这些崩溃主要与三星兼容性问题有关。
因此,我们判断在有限的时间和精力下,先解决 top 的 activityStopped 关键字的崩溃,以下主要是对 activityStopped 分析。如图所示,我们接下来从底层向上层对核心逻辑崩溃堆栈进行逐步分析。
Caused by: android.os.TransactionTooLargeException: data parcel size 541868 bytes
根据关键字 data parcel size
和相关源码分析,我们得出 data parcel size
附近的数据处理流程:
- JNI 层 BinderProxy_transact 函数先调用 IBinder->transact 函数进行 Binder 传输数据。
- IBinder->transact 函数的返回值 err 用于识别 Binder 传输