Android 应用崩溃

一、崩溃简介

Android崩溃分为Java崩溃和Native崩溃。

简单来说,Java崩溃就是在Java代码中,出现了未捕获异常,导致程序异常退出。那Native崩溃又是怎么产生的呢?一般都是因为在Native代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动abort,这些都会产生相应的signal信号,导致程序异常退出。

一些第三方的服务。腾讯的Bugly、阿里的啄木鸟平台、网易云捕、Google的Firebase等等。Bugly在国内做的最好;从技术深度跟捕获能力来说,阿里UC浏览器内核团队打造的啄木鸟平台最佳。

二、解决崩溃思路

第一步:确定重点

确认和分析重点,关键在于在日志中找到重要的信息,对问题有一个大致判断。一般来说,可以关注以下几点。

  1. 确认严重程度。解决崩溃需要看性价比,优先解决TOP崩溃或者对业务有重大影响的,例如订单功能

  2. 崩溃的基本信息。确定崩溃的类型以及异常描述。

    • java崩溃。Java崩溃比较明显,比如 NullPointerException ,但是像 OutOfMemoryError 这类信息需要进一步查看日志中的内存信息等。
    • Native崩溃。需要观察signal、code、fault addr等内容,以及崩溃时Java的堆栈。比较常见的有 SIGSEGV 和 SIGABRT ,前者一般是由于空指针、非法指针造成,后者主要因为ANR和调用abort()退出导致。
    • ANR 先看看主线程的堆栈,是否因为锁等待导致。接着看看 ANR 日志中的 iowait 、CPU 、GC 、system server等信息、进一步确定是 I/O 问题还是CPU竞争问题,还是由于大量GC导致卡死。
  3. Logcat。 Logcat一般会存在一些有价值的线索,日志级别是Warning、Error的需要特别注意。从Logcat中我们可以看到当时系统的一些行为跟手机的状态,例如出现ANR时,会有“am_anr”;App被杀时,会有“am_kill”。不同的系统、厂商输出的日志有所差别,当从一条崩溃日志中无法看出问题的原因,或者得不到有用信息时,不要放弃,建议查看相同崩溃点下的更多崩溃日志。

4.各个资源情况。结合崩溃的基本信息,我们接着看看是不是跟 “内存信息” 有关,是不是跟“资源信息”有关。比如是物理内存不足、虚拟内存不足,还是文件句柄fd泄漏了。

第二步:查找共性

如果使用了上面的方法还是不能有效定位问题,我们可以尝试查找这类崩溃有没有什么共性。找到了共性,也就可以进一步找到差异,离解决问题也就更进一步。

机型、系统、ROM、厂商、ABI,这些采集到的系统信息都可以作为维度聚合,共性问题例如是不是因为安装了Xposed,是不是只出现在x86的手机,是不是只有三星这款机型,是不是只在Android 5.0的系统上。应用信息也可以作为维度来聚合,比如正在打开的链接、正在播放的视频、国家、地区等。

找到了共性,可以对你下一步复现问题有更明确的指引。

第三步:尝试复现

如果我们已经大概知道了崩溃的原因,为了进一步确认更多信息,就需要尝试复现崩溃。如果我们对崩溃完全没有头绪,也希望通过用户操作路径来尝试重现,然后再去分析崩溃原因。

“只要能本地复现,我就能解”,相信这是很多开发跟测试说过的话。有这样的底气主要是因为在稳定的复现路径上面,我们可以采用增加日志或使用Debugger、GDB等各种各样的手段或工具做进一步分析。

三、崩溃的捕获

java 崩溃捕获

Native 崩溃捕获

mp.weixin.qq.com/s/g-WzYF3wW…

四、崩溃的难点

Native崩溃捕获的难点

Chromium的Breakpad是目前Native崩溃捕获中最成熟的方案,但很多人都觉得Breakpad过于复杂。其实我认为Native崩溃捕获这个事情本来就不容易,跟当初设计Tinker的时候一样,如果只想在90%的情况可靠,那大部分的代码的确可以砍掉;但如果想达到99%,在各种恶劣条件下依然可靠,后面付出的努力会远远高于前期。

所以在上面的三个流程中,最核心的是怎么样保证客户端在各种极端情况下依然可以生成崩溃日志。因为在崩溃时,程序会处于一个不安全的状态,如果处理不当,非常容易发生二次崩溃。

那么,生成崩溃日志时会有哪些比较棘手的情况呢?

情况一:文件句柄泄漏,导致创建日志文件失败,怎么办?

应对方式:
我们需要提前申请文件句柄fd预留,防止出现这种情况。

情况二:因为栈溢出了,导致日志生成失败,怎么办?

应对方式:
为了防止栈溢出导致进程没有空间创建调用栈执行处理函数,我们通常会使用常见的signalstack。在一些特殊情况,我们可能还需要直接替换当前栈,所以这里也需要在堆中预留部分空间。

情况三:整个堆的内存都耗尽了,导致日志生成失败,怎么办?

应对方式:
这个时候我们无法安全地分配内存,也不敢使用stl或者libc的函数,因为它们内部实现会分配堆内存。这个时候如果继续分配内存,会导致出现堆破坏或者二次崩溃的情况。Breakpad做的比较彻底,重新封装了Linux Syscall Support,来避免直接调用libc。

情况四:堆破坏或二次崩溃导致日志生成失败,怎么办?

应对方式:
Breakpad会从原进程fork出子进程去收集崩溃现场,此外涉及与Java相关的,一般也会用子进程去操作。这样即使出现二次崩溃,只是这部分的信息丢失,我们的父进程后面还可以继续获取其他的信息。在一些特殊的情况,我们还可能需要从子进程fork出孙进程。

当然Breakpad也存在着一些问题,例如生成的minidump文件是二进制格式的,包含了太多不重要的信息,导致文件很容易达到几MB。但是minidump也不是毫无用处,它有一些比较高级的特性,比如使用gdb调试、可以看到传入参数等。Chromium未来计划使用Crashpad全面替代Breakpad,但目前来说还是 “too early to mobile”。

我们有时候想遵循Android的文本格式,并且添加更多我们认为重要的信息,这个时候就要去改造Breakpad的实现。比较常见的例如增加Logcat信息、Java调用栈信息以及崩溃时的其他一些有用信息,在下一节我们会有更加详细的介绍。

如果想彻底弄清楚Native崩溃捕获,需要我们对虚拟机运行、汇编这些内功有一定造诣。做一个高可用的崩溃收集SDK真的不是那么容易,它需要经过多年的技术积累,要考虑的细节也非常多,每一个失败路径或者二次崩溃场景都要有应对措施或备用方案。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享