You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

158 lines
15 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 01 | 崩溃优化(上):关于“崩溃”那些事儿
在各种场合遇到其他产品的开发人员时,大家总忍不住想在技术上切磋两招。第一句问的通常都是“你们产品的崩溃率是多少?”
程序员A自豪地说 “百分之一。”
旁边的程序员B鄙视地看了一眼然后喊到 “千分之一!”
“万分之一” 程序员C说完之后全场变得安静起来。
崩溃率是衡量一个应用质量高低的基本指标,这一点是你我都比较认可的。不过你说的“万分之一”就一定要比我说的“百分之一” 更好吗?我觉得,这个问题其实并不仅仅是比较两个数值这么简单。
今天我们就来聊一聊有关“崩溃”的那些事我会从Android的两种崩溃类型谈起再和你进一步讨论到底该怎样客观地衡量崩溃这个指标以及又该如何看待和崩溃相关的稳定性。
## Android 的两种崩溃
我们都知道Android崩溃分为Java崩溃和Native崩溃。
简单来说,**Java崩溃就是在Java代码中出现了未捕获异常导致程序异常退出**。那Native崩溃又是怎么产生的呢**一般都是因为在Native代码中访问非法地址也可能是地址对齐出现了问题或者发生了程序主动abort这些都会产生相应的signal信号导致程序异常退出**。
所以“崩溃”就是程序出现异常而一个产品的崩溃率跟我们如何捕获、处理这些异常有比较大的关系。Java崩溃的捕获比较简单但是很多同学对于如何捕获Native崩溃还是一知半解下面我就重点介绍Native崩溃的捕获流程和难点。
1.Native崩溃的捕获流程
如果你对Native崩溃机制的一些基本知识还不是很熟悉建议你阅读一下[《Android平台Native代码的崩溃捕获机制及实现》](http://mp.weixin.qq.com/s/g-WzYF3wWAljok1XjPoo7w)。这里我着重给你讲讲一个完整的Native崩溃从捕获到解析要经历哪些流程。
* 编译端。编译C/C++代码时,需要将带符号信息的文件保留下来。
* 客户端。捕获到崩溃时候,将收集到尽可能多的有用信息写入日志文件,然后选择合适的时机上传到服务器。
* 服务端。读取客户端上报的日志文件寻找适合的符号文件生成可读的C/C++调用栈。
![](https://static001.geekbang.org/resource/image/95/11/95d9733860e3a52c6c3b5976ca25b711.jpg)
2.Native崩溃捕获的难点
Chromium的[Breakpad](http://chromium.googlesource.com/breakpad/breakpad/+/master)是目前Native崩溃捕获中最成熟的方案但很多人都觉得Breakpad过于复杂。其实我认为Native崩溃捕获这个事情本来就不容易跟当初设计Tinker的时候一样如果只想在90%的情况可靠那大部分的代码的确可以砍掉但如果想达到99%,在各种恶劣条件下依然可靠,后面付出的努力会远远高于前期。
所以在上面的三个流程中,**最核心的是怎么样保证客户端在各种极端情况下依然可以生成崩溃日志**。因为在崩溃时,程序会处于一个不安全的状态,如果处理不当,非常容易发生二次崩溃。
那么,生成崩溃日志时会有哪些比较棘手的情况呢?
**情况一:文件句柄泄漏,导致创建日志文件失败,怎么办?**
应对方式我们需要提前申请文件句柄fd预留防止出现这种情况。
**情况二:因为栈溢出了,导致日志生成失败,怎么办?**
应对方式为了防止栈溢出导致进程没有空间创建调用栈执行处理函数我们通常会使用常见的signalstack。在一些特殊情况我们可能还需要直接替换当前栈所以这里也需要在堆中预留部分空间。
**情况三:整个堆的内存都耗尽了,导致日志生成失败,怎么办?**
应对方式这个时候我们无法安全地分配内存也不敢使用stl或者libc的函数因为它们内部实现会分配堆内存。这个时候如果继续分配内存会导致出现堆破坏或者二次崩溃的情况。Breakpad做的比较彻底重新封装了[Linux Syscall Support](https://chromium.googlesource.com/linux-syscall-support/)来避免直接调用libc。
**情况四:堆破坏或二次崩溃导致日志生成失败,怎么办?**
应对方式Breakpad会从原进程fork出子进程去收集崩溃现场此外涉及与Java相关的一般也会用子进程去操作。这样即使出现二次崩溃只是这部分的信息丢失我们的父进程后面还可以继续获取其他的信息。在一些特殊的情况我们还可能需要从子进程fork出孙进程。
当然Breakpad也存在着一些问题例如生成的minidump文件是二进制格式的包含了太多不重要的信息导致文件很容易达到几MB。但是minidump也不是毫无用处它有一些比较高级的特性比如[使用gdb调试](https://www.chromium.org/chromium-os/packages/crash-reporting/debugging-a-minidump)、可以看到传入参数等。Chromium未来计划使用Crashpad全面替代Breakpad但目前来说还是 “too early to mobile”。
我们有时候想遵循Android的文本格式并且添加更多我们认为重要的信息这个时候就要去改造Breakpad的实现。**比较常见的例如增加Logcat信息、Java调用栈信息以及崩溃时的其他一些有用信息在下一节我们会有更加详细的介绍。**
如果想彻底弄清楚Native崩溃捕获需要我们对虚拟机运行、汇编这些内功有一定造诣。做一个高可用的崩溃收集SDK真的不是那么容易它需要经过多年的技术积累要考虑的细节也非常多每一个失败路径或者二次崩溃场景都要有应对措施或备用方案。
3.选择合适的崩溃服务
对于很多中小型公司来说,我并不建议自己去实现一套如此复杂的系统,可以选择一些第三方的服务。目前各种平台也是百花齐放,包括腾讯的[Bugly](https://bugly.qq.com/v2/)、阿里的[啄木鸟平台](http://wpk.uc.cn/)、网易云捕、Google的Firebase等等。
当然在平台的选择方面我认为从产品化跟社区维护来说Bugly在国内做的最好从技术深度跟捕获能力来说阿里UC浏览器内核团队打造的啄木鸟平台最佳。
## 如何客观地衡量崩溃
对崩溃有了更多了解以后,我们怎样才能客观地衡量崩溃呢?
要衡量一个指标,首先要统一计算口径。如果想评估崩溃造成的用户影响范围,我们会先去看**UV崩溃率**。
```
UV 崩溃率 = 发生崩溃的 UV / 登录 UV
```
只要用户出现过一次崩溃就会被计算到所以UV崩溃率的高低会跟应用的使用时长有比较大的关系这也是微信UV崩溃率在业界不算低的原因强行甩锅。当然这个时候我们还可以去看应用PV崩溃率、启动崩溃率、重复崩溃率这些指标计算方法都大同小异。
这里为什么要单独统计启动崩溃率呢?因为启动崩溃对用户带来的伤害最大,应用无法启动往往通过热修复也无法拯救。闪屏广告、运营活动,很多应用启动过程异常复杂,又涉及各种资源、配置下发,极其容易出现问题。微信读书、蘑菇街、淘宝、天猫这些“重运营”的应用都有使用一种叫作[“安全模式”](https://mp.weixin.qq.com/s?__biz=MzUxMzcxMzE5Ng==&mid=2247488429&idx=1&sn=448b414a0424d06855359b3eb2ba8569&source=41#wechat_redirect)的技术来保障客户端的启动流程,在监控到客户端启动失败后,给用户自救的机会。
现在回到文章开头程序员“华山论剑”的小故事,我来揭秘他们解决崩溃率的“独家秘笈”。
程序员B对所有线程、任务都封装了一层try catch“消化”掉了所有Java崩溃。至于程序是否会出现其他异常表现这是上帝要管的事情反正我是实现了“千分之一”的目标。
程序员C认为Native崩溃太难解决所以他想了一个“好方法”就是不采集所有的Native崩溃美滋滋地跟老板汇报“万分之一”的工作成果。
了解了美好数字产生的“秘笈”后不知道你有何感想其实程序员B和C都是真实的案例而且他们的用户体量都还不算小。技术指标过于KPI化是国内比较明显的一个现象。崩溃率只是一个数字我们的出发点应该是让用户有更好的体验。
## 如何客观地衡量稳定性
到此我们讨论了崩溃是怎么回事儿以及怎么客观地衡量崩溃。那崩溃率是不是就能完全等价于应用的稳定性呢答案是肯定不行。处理了崩溃我们还会经常遇到ANRApplication Not Responding程序没有响应这个问题。
出现ANR的时候系统还会弹出对话框打断用户的操作这是用户非常不能忍受的。这又带来另外一个问题我们怎么去发现应用中的ANR异常呢总结一下通常有两种做法。
**1\. 使用FileObserver监听/data/anr/traces.txt 的变化**。非常不幸的是很多高版本的ROM已经没有读取这个文件的权限了。这个时候你可能只能思考其他路径海外可以使用Google Play服务而国内微信利用[Hardcoder](https://mp.weixin.qq.com/s/9Z8j3Dv_5jgf7LDQHKA0NQ?)框架HC框架是一套独立于安卓系统实现的通信框架它让App和厂商ROM能够实时“对话”了目标就是充分调度系统资源来提升App的运行速度和画质切实提高大家的手机使用体验向厂商获取了更大的权限。
**2\. 监控消息队列的运行时间**。这个方案无法准确地判断是否真正出现了ANR异常也无法得到完整的ANR日志。在我看来更应该放到卡顿的性能范畴。
回想我当时在设计Tinker的时候为了保证热修复不会影响应用的启动Tinker在补丁的加载流程也设计了简单的“安全模式”在启动时会检查上次应用的退出类型如果检查连续三次异常退出将会自动清除补丁。所以除了常见的崩溃还有一些会导致应用异常退出的情况。
在讨论什么是异常退出之前,我们先看看都有哪些应用退出的情形。
* 主动自杀。`Process.killProcess()`、`exit()` 等。
* 崩溃。出现了Java或Native崩溃。
* 系统重启;系统出现异常、断电、用户主动重启等,我们可以通过比较应用开机运行时间是否比之前记录的值更小。
* 被系统杀死。被low memory killer杀掉、从系统的任务管理器中划掉等。
* ANR。
我们可以在应用启动的时候设定一个标志,在主动自杀或崩溃后更新标志,这样下次启动时通过检测这个标志就能确认运行期间是否发生过异常退出。对应上面的五种退出场景,我们排除掉主动自杀和崩溃**(崩溃会单独的统计)**这两种场景希望可以监控到剩下三种的异常退出理论上这个异常捕获机制是可以达到100%覆盖的。
通过这个异常退出的检测可以反映如ANR、low memory killer、系统强杀、死机、断电等其他无法正常捕获到的问题。当然异常率会存在一些误报比如用户从系统的任务管理器中划掉应用。对于线上的大数据来说还是可以帮助我们发现代码中的一些隐藏问题。
所以就得到了一个新的指标来衡量应用的稳定性,即**异常率**。
```
UV 异常率 = 发生异常退出或崩溃的 UV / 登录 UV
```
前不久我们的一个应用灰度版本发现异常退出的比例增长不少最后排查发现由于视频播放存在一个巨大bug会导致可能有用户手机卡死甚至重启这是传统崩溃收集很难发现的问题。
根据应用的前后台状态,我们可以把异常退出分为前台异常退出和后台异常退出。“被系统杀死”是后台异常退出的主要原因,当然我们会**更关注前台的异常退出**的情况这会跟ANR、OOM等异常情况有更大的关联。
通过异常率我们可以比较全面的评估应用的稳定性对于线上监控还需要完善崩溃的报警机制。在微信我们可以做到5分钟级别的崩溃预警确保能在第一时间发现线上重大问题尽快决定是通过发版还是动态热修复解决问题。
## 总结
今天我讲了Android的两种崩溃重点介绍了Native崩溃的捕获流程和一些难点。做一个高可用的崩溃收集SDK并不容易它背后涉及Linux信号处理以及内存分配、汇编等知识当你内功修炼得越深厚学习这些底层知识就越得心应手。
接着我们讨论了崩溃率应该如何去计算崩溃率的高低跟应用时长、复杂度、收集SDK有关。不仅仅是崩溃率我们还学习了目前ANR采集的方式以及遇到的问题最后提出了异常率这一个新的稳定性监控指标。
作为技术人员我们不应该盲目追求崩溃率这一个数字应该以用户体验为先如果强行去掩盖一些问题往往更加适得其反。我们不应该随意使用try catch去隐藏真正的问题要从源头入手了解崩溃的本质原因保证后面的运行流程。在解决崩溃的过程也要做到由点到面不能只针对这个崩溃去解决而应该要考虑这一类崩溃怎么解决和预防。
崩溃的治理是一个长期的过程,在专栏下一期我会重点讲一些分析应用崩溃的方法论。另外,你如果细心的话,可以发现,在这篇文章里,我放了很多的超链接,后面的文章里也会有类似的情况。所以,这就要求你在读完文章之后,或者读的过程中,如果对相关的背景信息或者概念不理解,就需要花些时间阅读周边文章。当然,如果看完还是没有明白,你也可以在留言区给我留言。
## 课后作业
[Breakpad](https://chromium.googlesource.com/breakpad/breakpad/+/master)是一个跨平台的开源项目今天的课后作业是使用Breakpad来捕获一个Native崩溃并在留言区写下你今天学习和练习后的总结与思考。
当然我在专栏GitHub的[Group](https://github.com/AndroidAdvanceWithGeektime)里也为你提供了一个[Sample](https://github.com/AndroidAdvanceWithGeektime/Chapter01)方便你练习如果你没使用过Breakpad的话只需要直接编译即可。希望你可以通过一个简单的Native崩溃捕获过程完成minidump文件的生成和解析在实践中加深对Breakpad工作机制的认识。
我要再次敲黑板划重点了,请你一定要坚持参与我们的课后练习,从最开始就养成学完后立马动手操作的好习惯,这样才能让学习效率最大化,一步步接近“成为高手”的目标。当然了,认真提交作业的同学还有机会获得学习加油礼包。接下来,就看你的了!
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“[学习加油礼包](http://time.geekbang.org/column/article/70250)”,期待与你一起切磋进步哦。
![](https://static001.geekbang.org/resource/image/24/c0/24c190870d71c3daa203a939d67358c0.jpg)![](https://static001.geekbang.org/resource/image/30/aa/306ef8892cc985a19fdd36534e7c5daa.png)