这系列相关博客,转载 Android开发高手课
在各种场合遇到其他产品的开发人员时,大家总忍不住想在技术上切磋两招。第一句问的通常都是“你们产品的崩溃率是多少?”
程序员A自豪地说:“百分之一。”
旁边的程序员B鄙视地看了一眼,然后喊到:“千分之一! ”
“万分之一”,程序员C说完之后全场变得安静起来。
崩溃率是衡量一个应用质量高低的基本指标,这一点是你我都比较认可的。不过你说的“万分之一”就一定要比我说的“百分之一”更好吗?我觉得,这个问题其实并不仅仅是比较两个数值这么简单。
今天我们就来聊一聊有关“崩溃”的那些事,我会从Android的两种崩溃类型谈起,再和你进一步讨论到底该怎样客观地衡量崩溃这个指标,以及又该如何看待和崩溃相关的稳定性。
我们都知道,Android崩溃分为Java崩溃和Native崩溃。
简单来说,Java崩溃就是在Java代码中,出现了未捕获异常,导致程序异常退出。那Native崩溃又是怎么产生的呢? 一般都是因为在Native代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动abort,这些都会产生相应的 signal信号,导致程序异常退出。
所以,“崩溃”就是程序出现异常,而一个产品的崩溃率,跟我们如何捕获、处理这些异常有比较大的关系。Java崩溃的捕获比较简单,但是很多同学对于如何捕获Native崩溃还是一知半解,下面我就重点介绍Native崩溃的捕获流程和难点。
所以在上面的三个流程中,最核心的是怎么样保证客户端在各种极端情况下依然可以生成崩溃日志。因为在崩溃时,程序会处于一个不安全的状态,如果处理不当,非常容易发生二次崩溃。
那么,生成崩溃日志时会有哪些比较棘手的情况呢?
情况一:文件句柄泄漏,导致创建曰志文件失败,怎么办?
应对方式:我们需要提前申请文件句柄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真的不是那么容易,它需要经过多年的技术积累,要考虑的细节也非常多,每一个失败路径或者二次崩溃场景都要有应对措施或备用方案。
当然,在平台的选择方面,我认为,从产品化跟社区维护来说,Bugly在国内做的最好;从技术深度跟捕获能力来说,阿里UC 浏览器内核团队打造的啄木鸟平台最佳。
对崩溃有了更多了解以后,我们怎样才能客观地衡量崩溃呢?
要衡量一个指标,首先要统一计算口径。如果想评估崩溃造成的用户影响范围,我们会先去看UV崩溃率。
UV崩溃率=发生崩溃的UV /登录UV
只要用户出现过一次崩溃就会被计算到,所以UV崩溃率的高低会跟应用的使用时长有比较大的关系,这也是微信UV崩溃率在业界不算低的原因(强行甩锅)。当然这个时候,我们还可以去看应用PV崩溃率、启动崩溃率、重复崩溃率这些指标,计算方法都大同小异。
**这里为什么要单独统计启动崩溃率呢?因为启动崩溃对用户带来的伤害最大,应用无法启动往往通过热修复也无法拯救。**闪屏广告、运营活动,很多应用启动过程异常复杂,又涉及各种资源、配置下发,极其容易出现问题。微信读书、蘑菇街、淘宝、 天猫这些“重运营”的应用都有使用一种叫作“安全模式”的技术来保障客户端的启动流程,在监控到客户端启动失败后,给用户自救的机会。
现在回到文章开头程序员“华山论剑”的小故事,我来揭秘他们解决崩溃率的“独家秘笈”。
程序员B对所有线程、任务都封装了一层try catch, “消化”掉了所有Java崩溃。至于程序是否会出现其他异常表现,这是上帝要管的事情,反正我是实现了“千分之一”的目标。
程序员C认为Native崩溃太难解决,所以他想了一个“好方法”,就是不采集所有的Native崩溃,美滋滋地跟老板汇报“万分之―”的工作成果。
了解了美好数字产生的“秘笈”后,不知道你有何感想?其实程序员B和C都是真实的案例,而且他们的用户体量都还不算小。技术指标过于KPI化,是国内比较明显的一个现象。崩溃率只是一个数字,我们的出发点应该是让用户有更好的体验。
到此,我们讨论了崩溃是怎么回事儿,以及怎么客观地衡量崩溃。那崩溃率是不是就能完全等价于应用的稳定性呢?答案是肯定不行。处理了崩溃,我们还会经常遇到ANR (Application Not Responding,程序没有响应)这个问题。
出现ANR的时候,系统还会弹出对话框打断用户的操作,这是用户非常不能忍受的。这又带来另外一个问题,我们怎么去发现应用中的ANR异常呢?总结一下,通常有两种做法。
回想我当时在设计Tinker的时候,为了保证热修复不会影响应用的启动,Tinker在补丁的加载流程也设计了简单的“安全模式”,在启动时会检查上次应用的退出类型,如果检查连续三次异常退出,将会自动清除补丁。所以除了常见的崩溃,还有一些会导致应用异常退出的情况。
在讨论什么是异常退出之前,我们先看看都有哪些应用退出的情形。
我们可以在应用启动的时候设定一个标志,在主动自杀或崩溃后更新标志,这样下次启动时通过检测这个标志就能确认运行期间是否发生过异常退出。对应上面的五种退出场景,我们排除掉主动自杀和崩溃(崩溃会单独的统计)这两种场景,希望可以监控到剩下三种的异常退出,理论上这个异常捕获机制是可以达到100%覆盖的。
通过这个异常退出的检测,可以反映如ANR、low memory killer、系统强杀、死机、断电等其他无法正常捕获到的问题。当然异常率会存在一些误报,比如用户从系统的任务管理器中划掉应用。对于线上的大数据来说,还是可以帮助我们发现代码中的—些隐藏问题。
所以就得到了一个新的指标来衡量应用的稳定性,即异常率。
UV异常率=发生异常退出或崩溃的UV /登录UV
前不久我们的一个应用灰度版本发现异常退出的比例增长不少,最后排查发现由于视频播放存在一个巨大bug,会导致可能有用户手机卡死甚至重启,这是传统崩溃收集很难发现的问题。
根据应用的前后台状态,我们可以把异常退出分为前台异常退出和后台异常退出。“被系统杀死”是后台异常退出的主要原因, 当然我们会更关注前台的异常退出的情况,这会跟ANR、OOM等异常情况有更大的关联。
通过异常率我们可以比较全面的评估应用的稳定性,对于线上监控还需要完善崩溃的报警机制。在微信我们可以做到5分钟级别的崩溃预警,确保能在第一时间发现线上重大问题,尽快决定是通过发版还是动态热修复解决问题。
今天,讲了Android的两种崩溃,重点介绍了Native崩溃的捕获流程和一些难点。做一个高可用的崩溃收集SDK并不容易, 它背后涉及Linux信号处理以及内存分配、汇编等知识,当你内功修炼得越深厚,学习这些底层知识就越得心应手。
接着,我们讨论了崩溃率应该如何去计算,崩溃率的高低跟应用时长、复杂度、收集SDK有关。不仅仅是崩溃率,我们还学习了目前ANR采集的方式以及遇到的问题,最后提出了异常率这一个新的稳定性监控指标。
作为技术人员,我们不应该盲目追求崩溃率这一个数字,应该以用户体验为先,如果强行去掩盖一些问题往往更加适得其反。 我们不应该随意使用try catch去隐藏真正的问题,要从源头入手,了解崩溃的本质原因,保证后面的运行流程。在解决崩溃的过程,也要做到由点到面,不能只针对这个崩溃去解决,而应该要考虑这一类崩溃怎么解决和预防。
崩溃的治理是一个长期的过程,在专栏下一期我会重点讲一些分析应用崩溃的方法论。另外,你如果细心的话,可以发现,在这篇文章里,我放了很多的超链接,后面的文章里也会有类似的情况。所以,这就要求你在读完文章之后,或者读的过程中, 如果对相关的背景信息或者概念不理解,就需要花些时间阅读周边文章。当然,如果看完还是没有明白,你也可以在留言区给我留言。
Breakpad是一个跨平台的开源项目,今天的课后作业是使用Breakpad来捕获一个Native崩溃,并在留言区写下你今天学习和练习后的总结与思考。
当然我在专栏GitHub的Group里也为你提供了一个Sample方便你练习,如果你没使用过Breakpad的话,只需要直接编译即可。希望你可以通过一个简单的Native崩溃捕获过程,完成minidump文件的生成和解析,在实践中加深对Breakpad工作机制的认识。
我要再次敲黑板划重点了,请你一定要坚持参与我们的课后练习,从最开始就养成学完后立马动手操作的好习惯,这样才能让学习效率最大化,一步步接近“成为高手”的目标。
1.⾸次看到这篇⽂章后就进⾏了⼿动操作,⽆奈遇到了模拟器⽆法⽣产dmp⽂件,不过还好⽤真机可以⽣成成功,但是使⽤sample中的tools⼯具不能⽣成txt⽂件,后⾯倒是⽣成了,但⽣成的txt却是个空⽂件,低迷了⼀段时间,却⼜⽆可奈何,后来换⽤了windows,结果没想到难度系数⽆限增⼤。
最近看了疑点解答的⽂章后,⼜重新产⽣了希望,开始动⼿实践,终于搞定了模拟器⽆法⽣产dmp⽂件的问题,继续使⽤Mac操作,总算成功了。
第⼀步:
运⾏sample,得到dmp⽂件,将⽂件导⼊到电脑中
第⼆步:
git下来Google的breakpad项⽬,使⽤./configure && make进⾏项⽬编译
第三步:
使⽤/breakpad/src/processor/microdump_stackwalk,对dmp⽂件进⾏处理,得到txt⽂件
命令:microdump_stackwalk xxx.dmp >xxx.txt
成果如下:
Operating system: Android
0.0.0 Linux 4.4.23+ #1 SMP PREEMPT Mon Sep 17 22:10:21 CST 2018 aarch64
CPU: arm64
8 CPUs
GPU: UNKNOWN
Crash reason: SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available
Thread 0 (crashed)
0 libcrash-lib.so + 0x5dc // 利⽤这个地址
x0 = 0x0000000000000000 x1 = 0x0000000000000001
x2 = 0x0000000000000000 x3 = 0x0000007a494a3a00
第四步:
根据编译得到的信息:CPU: arm64,0x5dc
根据这两个信息,使⽤NDK对应⽬录下的处理⼯具,我这⾥的信息使⽤的⼯具是:
/android-ndk-r16b/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
命令:
/xxx/xxx/xxx-addr2line -f -C -e /xxx/Chapter01/sample/build/intermediates/transforms/mergeJniLibs/debug/0/lib/arm64-
v8a/libcrash-lib.so
解析得到的信息如下:
Crash()
/Users/qixuefeng/AndroidDemo/Chapter1/sample/src/main/cpp/crash.cpp:10
2.看到评论有⼈说在⼿机上没有⽣成dmp⽂件,是因为作者在simple的gradle⽂件中
ndk {
abiFilters "armeabi-v7a"//, "arm64-v8a", "x86"
}
注释掉了v8a的部分,只要把注释取消就好了。
通过本章学习,了解到了NDK的异常捕获⽅式及使⽤⽅法,以前获取想⽅设法复现异常,通过logcat直接获取到到堆栈的地址信息,并且通过addr2line、objdump和ndk-stack等⼯具进⾏定位,复现及定位⼗分麻烦,现在使⽤breakpad可以⽅便的像Java异常捕获,并上传到后台,以⽅便定位分析问题。
3.换了aarch64-linux-android-4.9下⾯的addr2line就可以了
$NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -f -C -e sampl
e/build/intermediates/transforms/mergeJniLibs/release/0/lib/arm64-v8a/libcrash-lib.so 0x5e4
作者回复
对的,不同平台的需要使⽤对应的toolchains解析,⼀会我补充到readme⾥
4.第⼀步:
git clone代码,下载安装NDK和CMAKE,在此期间碰到⼀个问题,就是NDK下载版本太⾼,导致项⽬编译不过去,进⼀步分析,发现ndk-bundle\toolchains⽂件夹中少了mips64el-linux-android-4.9和mipsel-linux-android-4.9⽂件,解决⽅法是下载⼀个旧的NDK版本,把这两个⽂件夹复制过去即可。
第⼆步:
接着就run,在⼿机上跑起来了,点击CRASH,程序崩溃,我进去sdcard中看到了crashDump⽬录,⽣成崩溃⽂件。
第三步:
接着运⾏: ./tools/mac/minidump_stackwalk crashDump/***.dmp >crashLog.txt
,
结果⼜报错了,由于我的系统是Windows,不是Mac,故需要重新编译minidump_stackwalk ⼯具,解决⽅法是下载个VMWare虚拟机,装个Unbuntu 系统,然后重新编译,在编译过程中,由于git clone了breakpad源码,会缺少⼀个thrid_party⾥的⼀个less⽂件夹,名称为linux_syscall_support.h头⽂件,需要补上即可。
第四步:
利⽤./configure && make
对源码完成了编译,minidump_stackwalk这个⽂件在src/processor/⽬录中,然后运⾏:
./minidump_stackwalk crashDump/***.dmp >crashLog.txt
⼤功告成:
Operating system: Android
0.0.0 Linux 3.18.22+ #1 SMP PREEMPT Mon Jun 11 17:42:41 CST 2018 armv8l
CPU: arm
ARMv1 ARM part(0x4100d0b0) features: half,thumb,fastmult,vfpv2,edsp,neon,vfpv3,tls,vfpv4,idiva,idivt
10 CPUs
GPU: UNKNOWN
Crash reason: SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available
Thread 0 (crashed)
0 libcrash-lib.so + 0x77e
r0 = 0x00000000 r1 = 0x00000001 r2 = 0xffdb9bdc r3 = 0xf4a57fc0
r4 = 0x702af968 r5 = 0x6fb3bc10 r6 = 0x12e21970 r7 = 0xffdb9bc8
r8 = 0x12de5330 r9 = 0xf4a76a00 r10 = 0x12dd28b0 r12 = 0xf36a6fd8
fp = 0x70536430 sp = 0xffdb9bb4 lr = 0xf36a379b pc = 0xf36a377e
Found by: given as instruction pointer in context
1 dalvik-main space (deleted) + 0x5fffe
sp = 0xffdb9bcc pc = 0x12c60000
Found by: stack scanning
2 base.odex + 0x44121f
sp = 0xffdb9bd0 pc = 0xdf254221
Found by: stack scanning
3 dalvik-LinearAlloc (deleted) + 0xf016
sp = 0xffdb9bd4 pc = 0xeea9a018
Found by: stack scanning
4 dalvik-main space (deleted) + 0xd539e
sp = 0xffdb9be0 pc = 0x12cd53a0
Found by: stack scanning
5.⾸先,按照github⼯程的ReadMe流程⾛⼀遍,碰到了2个提示,让我安装NDK和CMAKE,我根据提示安装后编译直接就成功了;
接着就run,在⼿机上跑起来了,点击CRASH,程序崩溃,我进去sdcard中看到了crashDump⽬录,但没有⽣成崩溃⽂件,看到评论说的,将sample的build.gradle中注释给去掉:
abiFilters "armeabi-v7a", // "arm64-v8a", "x86" --> abiFilters "armeabi-v7a", "arm64-v8a", "x86"
我测试了下,成功⽣成了crash⽂件(不知道是不是张⽼师故意制造的障碍~捂⼿偷笑);
接着运⾏: ./tools/mac/minidump_stackwalk crashDump/***.dmp >crashLog.txt
,结果⼜报错了:
dyld: Symbol not found: __ZTTNSt7__cxx1118basic_stringstreamIcSt11char_traitsIcESaIcEEE
Referenced from: /Users/james/Documents/projec/breakpad/Chapter01/./tools/mac/minidump_stackwalk
Expected in: /usr/lib/libstdc++.6.dylib
in /Users/james/Documents/projec/breakpad/Chapter01/./tools/mac/minidump_stackwalk
Abort trap: 6
评论中⽼师说到缺少必要的动态链接库导致的,建议我们去编译breakpad源码;
然后,我去git clone了breakpad源码,利⽤
./configure
make
对源码完成了编译,minidump_stackwalk这个⽂件在src/processor/⽬录中,然后运⾏:
./minidump_stackwalk crashDump/***.dmp >crashLog.txt
⼤功告成,分享下我得成果:
Operating system: Android
0.0.0 Linux 3.18.31-perf-g4fd2040 #1 SMP PREEMPT Tue Dec 4 03:15:19 WIB 2018 aarch64
CPU: arm64
8 CPUs
GPU: UNKNOWN
Crash reason: SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available
Thread 0 (crashed)
0 libcrash-lib.so + 0x600
x0 = 0x0000007f7583e300 x1 = 0x0000007ff1b88ad4
x2 = 0x0000007f792e3000 x3 = 0x0000000001e5a140
下面是直接复制的 sample 中的 readme.md 内容。
例子里集成了Breakpad来获取发生 native crash 时候的系统信息和线程堆栈信息。
注意:由于例子里提供的 minidump_stackwalker 可能由于环境不同,无法启动,建议同学自行编译来获取工具,具体教程可见https://github.com/google/breakpad。
Android Studio 3.2 CMAKE NDK(使用ndk 16-19版本)
例子采用 CMAKE来构建 breakpad 库, 项目可直接导入 AndroidStudio 运行
例子支持armeabi-v7a,arm64-v8a,x86 三种平台。
tools/mac/minidump_stackwalker
工具来根据 minidump 文件生成堆栈跟踪 log ./tools/mac/minidump_stackwalk crashDump/***.dmp >crashLog.txt
Operating system: Android
0.0.0 Linux 4.4.78-perf-gdd4cbe9-00529-g1a92c1c #1 SMP PREEMPT Thu Nov 22 03:44:52 CST 2018 armv8l
CPU: arm
ARMv1 Qualcomm part(0x51008010) features: half,thumb,fastmult,vfpv2,edsp,neon,vfpv3,tls,vfpv4,idiva,idivt
8 CPUs
GPU: UNKNOWN
Crash reason: SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available
Thread 0 (crashed)//crash 发生时候的线程
0 libcrash-lib.so + 0x77e//发生 crash 的位置和寄存器信息
r0 = 0x00000000 r1 = 0x00000001 r2 = 0xff80f2bc r3 = 0xebe31230
r4 = 0xec2850d4 r5 = 0x00000001 r6 = 0x00000000 r7 = 0xff80f2a8
r8 = 0x00000056 r9 = 0xebe6f000 r10 = 0xff80f3a8 r12 = 0xcf1d8fd8
fp = 0xff80f334 sp = 0xff80f294 lr = 0xcf1d579b pc = 0xcf1d577e
Found by: given as instruction pointer in context
Stack contents:
ff80f294 00 00 00 00 30 12 e3 eb bc f2 80 ff bc f2 80 ff ....0...........
ff80f2a4 30 12 e3 eb 98 f5 80 ff 65 40 32 cf 0.......e@2.
Possible instruction pointers:
1 base.odex + 0x9063
sp = 0xff80f2b0 pc = 0xcf324065
Found by: stack scanning
Stack contents:
ff80f2b0 d4 50 28 ec .P(.
Possible instruction pointers:
2 dalvik-LinearAlloc (deleted) + 0xd2
sp = 0xff80f2b4 pc = 0xec2850d4
Found by: stack scanning
Stack contents:
ff80f2b4 74 05 81 ff 01 00 00 00 68 0c 84 13 t.......h...
Possible instruction pointers:
3 dalvik-main space (region space) (deleted) + 0xc40c66
sp = 0xff80f2c0 pc = 0x13840c68
Found by: stack scanning
Stack contents:
ff80f2c0 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
ff80f2d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
ff80f2e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
ff80f2f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
ff80f300 00 00 00 00 01 00 00 00 00 00 00 00 98 f5 80 ff ................
ff80f310 56 00 00 00 a8 f3 80 ff 34 f3 80 ff e3 7f b5 eb V.......4.......
Possible instruction pointers:
4 libart.so + 0x3e1fe1
sp = 0xff80f320 pc = 0xebb57fe3
Found by: stack scanning
Stack contents:
ff80f320 00 00 00 00 68 0c 84 13 ....h...
Possible instruction pointers:
addr2line
来根据地址进行一个符号反解的过程,该工具在 $NDK_HOME/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line
arm-linux-androideabi-addr2line -f -C -e sample/build/intermediates/transforms/mergeJniLibs/debug/0/lib/armeabi-v7a/libcrash-lib.so 0x77e
//输出结果如下
Crash()
关于在 x86模拟器下无法生成 crash 日志问题的解决方法
在 x86 模拟器下无法生成日志的解决方法如下:
externalNativeBuild {
cmake {
cppFlags "-std=c++11"
arguments "-DANDROID_TOOLCHAIN=gcc"
}
}
https://github.com/google/breakpad 例子里只提供了 Mac 的工具,如果需要其他平台的工具,可以去编译源码获得,可以参照 breakpad 项目的说明文档来编译获取。