做为技术人员,咱们不该该盲目追求崩溃率这一个数字,应该以用户体验为先,若是强行去掩盖一些问题每每更加拔苗助长。咱们不该该随意使用 try catch 去隐藏真正的问题,要从源头入手,了解崩溃的本质缘由,保证后面的运行流程。在解决崩溃的过程,也要作到由点到面,不能只针对这个崩溃去解决,而应该要考虑这一类崩溃怎么解决和预防。(附github项目demo参考项目)
java
1、Android 的两种崩溃android
咱们都知道,Android 崩溃分为 Java 崩溃和 Native崩溃。git
简单来讲,Java 崩溃就是在 Java 代码中,出现了未捕获异常,致使程序异常退出。那 Native 崩溃通常都是由于Native代码中访问非法地址或者是地址对齐出现问题,再或者是发生程序主动abort,这些都会产生对应的signal信号,致使程序异常退出。因此,“崩溃”就是程序出现异常,而一个产品的崩溃率,跟咱们如何捕获、处理这些异常有比较大的关系。Java 崩溃的捕获比较简单,可是不少同窗对于如何捕获Native 崩溃仍是只知其一;不知其二,下面我就重点介绍 Native 崩溃的捕获流程和难点。github
一、Native崩溃捕获流程(Native 崩溃机制)浏览器
(1)编译端。编译 C/C++ 代码时,须要将带符号信息的文件保留下来。安全
(2)客户端。捕获到崩溃时候,将收集到尽量多的有用信息写入日志文件,而后选择合适的时机上传到服务器。服务器
(3)服务端,读取客户端上报的日志文件,寻找适合的符号文件,生成可读的C/C++调用栈。微信
二、Native崩溃捕获的难点网络
Chromium 的Breakpad是目前 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、阿里的啄木鸟平台、网易云捕、Google 的 Firebase 等等。
2、发现应用中的 ANR 异常(Application Not Responding,程序无响应)问题
一般两种方法去发现应用中ANR
一、使用 FileObserver 监听 /data/anr/traces.txt的变化。很是不幸的是,不少高版本的 ROM,已经没有读取这个文件的权限了。这个时候你可能只能思考其余路径,海外可使用 Google Play 服务,而国内微信利用Hardcoder框架(HC 框架是一套独立于安卓系统实现的通讯框架,它让 App 和厂商 ROM 可以实时“对话”了,目标就是充分调度系统资源来提高 App 的运行速度和画质,切实提升你们的手机使用体验)向厂商获取了更大的权限。
二、 监控消息队列的运行时间。这个方案没法准确地判断是否真正出现了 ANR 异常,也没法获得完整的 ANR 日志。在我看来,更应该放到卡顿的性能范畴。
在讨论什么是异常退出以前,咱们先看看都有哪些应用退出的情形:
主动自杀、
Process.killProcess()或者exit() 等
崩溃。出现了 Java 或 Native 崩溃。
系统重启;系统出现异常、断电、用户主动重启等,咱们能够经过比较应用开机运行时间是否比以前记录的值更小。
被系统杀死。被 low memory killer 杀掉、从系统的任务管理器中划掉等。
ANR。
咱们能够在应用启动的时候设定一个标志,在主动自杀或崩溃后更新标志,这样下次启动时经过检测这个标志就能确认运行期间是否发生过异常退出,对应上面五种情形除了主动自杀和崩溃(崩溃会单独统计)但愿监控剩下三种异常退出,理论上对于异常捕获机制可百分之百覆盖。
3、采集崩溃来源
一、从崩溃的基本信息。
好比进程名或线程名:崩溃的进程是前台进程仍是后台进程,崩溃是否是发生在 UI 线程。
崩溃堆栈和类型:崩溃是属于 Java 崩溃、Native 崩溃,仍是 ANR,对于不一样类型的崩溃咱们关注的点也不太同样,特别须要看崩溃堆栈的栈顶,看具体崩溃在系统的代码,仍是咱们本身的代码里面。
二、系统信息。
Logcat。这里包括应用、系统的运行日志。因为系统权限问题,获取到的 Logcat 可能只包含与当前 App 相关的。其中系统的 event logcat 会记录 App 运行的一些基本状况,记录在文件/system/etc/event-log-tags 中。
机型、系统、厂商、CPU、ABI、Linux 版本等。咱们会采集多达几十个维度,这对后面讲到寻找共性问题会颇有帮助。
设备状态:是否 root、是不是模拟器。一些问题是由 Xposed 或多开软件形成,对这部分问题咱们要区别对待。
三、内存信息:OOM、ANR、虚拟内存耗尽等,不少崩溃都跟内存有直接关系。若是咱们把用户的手机内存分为“2GB 如下”和“2GB 以上“两个桶,会发现“2GB 如下”用户的崩溃率是“2GB 以上”用户的几倍。
系统剩余内存:关于系统内存状态,能够直接读取文件 /proc/meminfo,当系统可用内存很小(低于 MemTotal 的 10%)时,OOM、大量 GC、系统频繁自杀拉起等问题都很是容易出现。
应用使用内存:包括 Java 内存、RSS(Resident Set Size)、PSS(Proportional Set Size),咱们能够得出应用自己内存的占用大小和分布。PSS 和 RSS 经过 /proc/self/smap 计算,能够进一步获得例如 apk、dex、so 等更加详细的分类统计。
虚拟内存:虚拟内存能够经过 /proc/self/status 获得,经过 /proc/self/maps 文件能够获得具体的分布状况。有时候咱们通常不过重视虚拟内存,可是不少相似 OOM、tgkill 等问题都是虚拟内存不足致使的。
四、资源信息:有的时候咱们会发现应用堆内存和设备内存都很是充足,仍是会出现内存分配失败的状况,这跟资源泄漏可能有比较大的关系。
文件句柄fd:文件句柄的限制能够经过 /proc/self/limits得到,通常单个进程容许打开的最大文件句柄个数为 1024。可是若是文件句柄超过 800 个就比较危险,须要将全部的 fd 以及对应的文件名输出到日志中,进一步排查是否出现了有文件或者线程的泄漏。
线程数:当前线程数大小能够经过上面的 status 文件获得,一个线程可能就占 2MB 的虚拟内存,过多的线程会对虚拟内存和文件句柄带来压力。根据个人经验来讲,若是线程数超过 400 个就比较危险。须要将全部的线程 id 以及对应的线程名输出到日志中,进一步排查是否出现了线程相关的问题。
JNI:使用 JNI 时,若是不注意很容易出现引用失效、引用爆表等一些崩溃,咱们能够经过 DumpReferenceTables 统计JNI的引用表,进一步分析是否出现了JNI泄露等问题。
五、应用信息:除了系统,其实咱们的应用更懂本身,能够留下不少相关的信息。
崩溃场景:崩溃发生在哪一个 Activity 或 Fragment,发生在哪一个业务中。
关键操做路径:不一样于开发过程详细的打点日志,咱们能够记录关键的用户操做路径,这对咱们复现崩溃会有比较大的帮助。
其余自定义信息:不一样的应用关心的重点可能不太同样,好比网易云音乐会关注当前播放的音乐,QQ 浏览器会关注当前打开的网址或视频。此外例如运行时间、是否加载了补丁、是不是全新安装或升级等信息也很是重要。
除了上面这些通用的信息外,针对特定的一些崩溃,咱们可能还须要获取相似磁盘空间、电量、网络使用等特定信息。因此说一个好的崩溃获取工具,会根据场景为咱们采集足够多的信息,让咱们有更多的线索去分析和定位问题,固然数据的采集须要注意用户隐私,作到足够强度的加密和脱敏。
4、崩溃分析
第一步:肯定重点。
(1)确认严重程度:解决崩溃也要看性价比,咱们优先解决 Top 崩溃或者对业务有重大影响,例如启动、支付过程的崩溃。
(2)崩溃基本信息:肯定崩溃的类型以及异常描述,对崩溃有大体判断,通常来讲大部分崩溃通过这一步已经能够获得结论。好比JAVA崩溃,类型比较明显,像NullPointerException等。或者Native崩溃,须要观察signal、code、fault addr等内容,以及崩溃时 Java 的堆栈。关于各 signal 含义的介绍,比较常见的是有 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 等各类各样的手段或工具作进一步分析。 回想当时在开发 Tinker 的时候,咱们遇到了各类各样的奇葩问题。好比某个厂商改了底层实现、新的 Android 系统实现有所更改,都须要去 Google、翻源码,有时候还须要去抠厂商的 ROM 或手动刷 ROM。这个痛苦的经历告诉我,不少疑难问题须要咱们耐得住寂寞,反复猜想、反复发灰度、反复验证。
疑难问题:系统崩溃 系统崩溃经常令咱们感到很是无助,它多是某个 Android 版本的 bug,也多是某个厂商修改 ROM 致使。这种状况下的崩溃堆栈可能彻底没有咱们本身的代码,很难直接定位问题。针对这种疑难问题,我来谈谈个人解决思路。
1. 查找可能的缘由。经过上面的共性归类,咱们先看看是某个系统版本的问题,仍是某个厂商特定 ROM 的问题。虽然崩溃日志可能没有咱们本身的代码,但经过操做路径和日志,咱们能够找到一些怀疑的点。
2. 尝试规避。查看可疑的代码调用,是否使用了不恰当的 API,是否能够更换其余的实现方式规避。
3. Hook 解决。这里分为 Java Hook 和 Native Hook。以我最近解决的一个系统崩溃为例,咱们发现线上出现一个 Toast 相关的系统崩溃,它只出如今 Android 7.0 的系统中,看起来是在 Toast 显示的时候窗口的 token 已经无效了。这有可能出如今 Toast 须要显示时,窗口已经销毁了。 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)
为何 Android 8.0 的系统不会有这个问题?在查看 Android 8.0 的源码后咱们发现有如下修改: try { mWM.addView(mView, mParams); trySendAccessibilityEvent(); } catch (WindowManager.BadTokenException e) { /* ignore */ }
考虑再三,咱们决定参考 Android 8.0 的作法,直接 catch 住这个异常。这里的关键在于寻找 Hook 点,这个案例算是相对比较简单的。Toast 里面有一个变量叫 mTN,它的类型为 handler,咱们只须要代理它就能够实现捕获。 若是你作到了我上面说的这些,95% 以上的崩溃都能解决或者规避,大部分的系统崩溃也是如此。固然总有一些疑难问题须要依赖到用户的真实环境,咱们但愿具有相似动态跟踪和调试的能力。专栏后面还会讲到 xlog 日志、远程诊断、动态分析等高级手段,能够帮助咱们进一步调试线上疑难问题,敬请期待。 崩溃攻防是一个长期的过程,咱们但愿尽量地提早预防崩溃的发生,将它消灭在萌芽阶段。这可能涉及咱们应用的整个流程,包括人员的培训、编译检查、静态扫描工做,还有规范的测试、灰度、发布流程等。 而崩溃优化也不是孤立的,它跟咱们后面讲到的内存、卡顿、I/O 等内容都有关。可能等你学完整个课程后,再回头来看会有不一样的理解。