1.Android中的崩溃有两种,Java崩溃和Native崩溃。Java崩溃就是在Java代码中,出现了未捕获异常,导致程序异常退出;Native崩溃一般都是因为在Native代码中访问非法地址,也可能是地址对齐出现问题,或者发生了程序主动abort,这些都会产生相应的signal信号,导致程序异常退出。
2.崩溃就是程序出现异常,而一个产品的崩溃率,跟我们如何捕获、处理这些异常有比较大的关系。Java崩溃的捕获比较简单,但Native的捕获有一定难度。
3.Native crash一直是crash里的大头,具有上下文不全、出错信息模糊、难以捕捉等特点,比java crash更难修复。一个合格的异常捕获组件要能达到以下目的:
4.现有方案:
5.捕获Native crash:
6.一个完整的Native崩溃从捕获到解析要经历的流程:
7.程序在崩溃时,处于一个不安全的状态,如果处理不当,非常容易发生二次崩溃,怎样才能保证客户端在各种极端情况下依然可以生成崩溃日志。
8.生成崩溃日志的时候会有哪些比较棘手的情况呢?
9.想要彻底清楚Native崩溃捕获,需要我们对虚拟机运行、汇编这些内功有一定的造诣。做一个高可用的崩溃SDK并不容易,它需要经过多年技术积累,要考虑的细节也比较多,每一个失败路径或者二次崩溃场景都要有应对措施或备用方案。
10.启动崩溃对用户带来的伤害最大,应用无法启动往往通过热修复也无法拯救。闪屏广告、运营活动、各种资源下发、配置下发,过程复杂,所以极容易出现问题,所以这种偏运营的应用都有使用一种叫做安全模式的技术来保障客户端的启动流程,在监控到客户端启动失败后,会给用户自救的机会。
11.会导致应用异常退出的情形有:
12.我们可以在应用启动的时候设定一个标志,在主动自杀或崩溃后更新标志,这样下次启动时通过检测这个标志就能确认运行期间是否发生过异常退出。
13.崩溃现场应该采集哪些信息?
1.崩溃信息 : 从崩溃的基本信息,我们可以对崩溃有初步的判断。
2.系统信息: 系统信息有时候会带一些关键线索,对我们解决问题帮助很大。
3.内存信息:OOM、ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系。
4.资源信息 :有时候我们发现应用堆内存和设备内存都非常充足,还是会出现内存分配失败的情况,这跟资源泄漏可能有比较大的关系。
5.应用信息:除了系统,我们的应用更懂自己,可以留下许多相关的信息。
6.其他信息:除了通用的信息外,针对特定的一些崩溃,我们可能还需要获取类似磁盘空间、电量、网络等特定信息。所以说一个好的崩溃捕获工具,会根据场景为我们采集足够多的信息,让我们有足够多的线索去分析和定位问题。当然数据的采集需要注意用户隐私,做到足够强度的加密和脱敏。
14.崩溃分析三部曲:
第一步:确定重点:关键是在于日志中找到重要信息,对问题有一个大致判断。一般来说,建议关注以下几点:
无论是资源文件还是Logcat,内存与线程相关的信息都需要特别注意,很多崩溃都是由于它们使用不当造成的。
第二步:查找共性:如果使用了上面的方法还是不能有效定位,我们可以尝试查找这类崩溃有没有共性,找到了共性,也就可以进一步找到差异,离解决问题也就更近一步。机型、系统、ROM、厂商、ABI,这些采集到的系统信息都可以作为维度聚合,共性问题例如是不是因为安装了Xposed,是不是只出现在x86的手机,是不是只有三星这款机型,是不是只在5.0系统上。应用信息也可以作为维度来聚合,比如正在打开的链接、正在播放的视频、国家、地区等。找到了共性,可以对下一步复现问题有更明确的指引。
第三步:尝试复现:如果我们大概知道了崩溃原因,为了进一步确认更多信息,就需要尝试复现崩溃;如果我们对崩溃完全没有头绪,也希望通过用户操作路径来尝试复现,然后再去分析崩溃原因。在稳定的复现路径上面,我们可以 采用增加日志或者使用Debugger、GDB等各种各样的手段或工具做进一步分析。
奇葩问题,比如某个厂商改了底层实现、新的Android系统实现有所更改,都需要去Google、翻源码,有时候还需要去抠厂商的ROM或手动刷ROM。这些痛苦经历告诉我们,很多疑难问题需要我们耐得住寂寞,反复猜测、反复发灰度、反复验证。
15.疑难问题:系统崩溃。系统崩溃常常令我们感到非常无助,它可能是某个Android版本的bug,也可能是某个厂商修改ROM导致,这种情况下的崩溃堆栈可能完全没有我们自己的代码,很难直接定位问题。针对这种问题,可以尝试以下思路:
android.view.WindowManager$BadTokenException:
at android.view.ViewRootImpl.setView(ViewRootImpl.java)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java4)
at android.widget.Toast$TN.handleShow(Toast.java)
为什么8.0系统不会有这个问题呢?在查看了Android 8.0的源码后我们发下有以下修改:
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
考虑再三,我们决定参考Android 8.0的做法,直接catch住这个异常。这里的关键点在于寻找Hook点,这个案例算是相对简单的,Toast里面有一个变量叫mTN,它的类型为handler,我们只需要代理它就可以实现捕获。
16.如果做到了上面说的这些,95%以上的崩溃都能解决或者规避,大部分的系统崩溃也是如此。当然我们也希望具备类似动态跟踪、远程诊断等手段,帮助我们进一步调试线上疑难问题。
17.补充一下获得logcat和Jave堆栈的方法:
一. 获取logcat
logcat日志流程是这样的,应用层 --> liblog.so --> logd,底层使用ring buffer来存储数据。
获取的方式有以下三种:
1. 通过logcat命令获取。
优点:非常简单,兼容性好。
缺点:整个链路比较长,可控性差,失败率高,特别是堆破坏或者堆内存不足时,基本会失败。
2. hook liblog.so实现。通过hook liblog.so 中__android_log_buf_write 方法,将内容重定向到自己的buffer中。
优点:简单,兼容性相对还好。
缺点:要一直打开。
3. 自定义获取代码。通过移植底层获取logcat的实现,通过socket直接跟logd交互。
优点:比较灵活,预先分配好资源,成功率也比较高。
缺点:实现非常复杂
二. 获取Java 堆栈
native崩溃时,通过unwind只能拿到Native堆栈。我们希望可以拿到当时各个线程的Java堆栈
1. Thread.getAllStackTraces()。
优点:简单,兼容性好。
缺点:
a. 成功率不高,依靠系统接口在极端情况也会失败。
b. 7.0之后这个接口是没有主线程堆栈。
c. 使用Java层的接口需要暂停线程
2. hook libart.so。通过hook ThreadList和Thread的函数,获得跟ANR一样的堆栈。为了稳定性,我们会在fork子进程执行。
优点:信息很全,基本跟ANR的日志一样,有native线程状态,锁信息等等。
缺点:黑科技的兼容性问题,失败时可以用Thread.getAllStackTraces()兜底
获取Java堆栈的方法还可以用在卡顿时,因为使用fork进程,所以可以做到完全不卡主进程。