Android应用崩溃优化

942 阅读7分钟

本文目的主要是在学习张绍文老师的Android开发高手课过程中有很多吃力的地方,所以讲其中不太懂得东西,写下来供大家在学习的时候参考.

张绍文老师的Android开发高手课地址

文中将崩溃分为了Java崩溃和Native崩溃,Java崩溃的话我们可以通过一些第三方平台的监控,定时的清理,和测试同学的测试,相对来说没有那么复杂,但是Native崩溃使我们平常小应用很少见到也很少去解决的一个部分.

文中老师推荐了一篇Native代码崩溃的入门基础知识 Android 平台 Native 代码的崩溃捕获机制及实现

后面介绍说Chromium 的Breakpad是目前 Native 崩溃捕获中最成熟的方案,Breakpad过于负责是为了应对各种极端情况下依然能够正确的采集到崩溃日志.

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

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

  • 文件句柄:句柄是在Windows中引入的一个概念,它是和对象一一对应的无符号整数值. 在 Linux 环境中,任何事物都是用文件来表示,设备是文件,目录是文件,socket 也是文件。用来表示所处理对象的接口和唯一接口就是文件。应用程序在读 / 写一个文件时,首先需要打开这个文件,打开的过程其实质就是在进程与文件之间建立起连接,句柄的作用就是唯一标识此连接。此后对文件的读 / 写时,目标文件就由这个句柄作为代表。最后关闭文件其实就是释放这个句柄的过程,使得进程与文件之间的连接断开。 个人理解句柄就是一种对指针的封装,因为直接操作指针可操作性太强,所有用了句柄封装了指针
  • 句柄文件泄漏: Android使用的Linux的内核,单个进程的默认文件句柄数量是1024,文件句柄泄漏进程的某些模块大量的持有文件句柄没有回收(例如视频播放),导致没有办法创建日志文件.
  • 文中给出的解决方法是提前申请文件句柄预留的方式.我自己脑子中想到这个问题的第一想法是开启一个独立的进程去做这个事情,默认的文件句柄数量正常情况下是满足使用的,没必要为了这种极端情况去单独开启进程.

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

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

  1. 栈溢出应该是指线程的虚拟机栈溢出了,也就是说到了信号的处理,进程返回到用户态时,需要执行相应的信号处理函数时候,虚拟机栈已经无法执行.

  2. signalstack:我们一定需要在这种极端的情况下处理信号,那么还是有办法的,也就是使用 sigaltstack() 函数来实现,可用下面的步骤:

    • 1 分配一块内存区,当然是从堆中分配,这块内存区就称为“可替换信号栈”(alternate signal stack),顾名思义,我们就是希望将信号处理函数的栈挪到堆中,而不和进程共用一块栈区。

    • 2 使用 sigaltstack() 系统调用通知内核“可替换信号栈”已经建立。

    • 3 接着建立信号处理函数,此时需要对 sigaction() 函数的 sa_flags 成员设立 SA_ONSTACK 标志,该标志告诉内核信号处理函数的栈帧就在“可替换信号栈”上建立。

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

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

  1. stllibc: stl是C++的一部分,是一些容器的集合,libc是Linux下的ANSI C的函数库,包括字符类型,错误码等.

  2. Linux Syscall Support:应该是一个可以嵌入Linux系统调用的标准C库,通过这个库绕开上面的的那些函数,不在堆中创建? 这个有很多疑问,需要深入了解.

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

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

  • fork函数: 它的作用是克隆进程,也就是将原先的一个进程再克隆出一个来,克隆出的这个进程就是原进程的子进程,这个子进程和其他的进程没有什么区别,同样拥有自己的独立的地址空间。不同的是子进程是在fork返回之后才开始执行的,就像一把叉子一样,执行fork之后,父子进程就分道扬镳了. 子进程只执行fork指令之后的指令 所以不用担心套娃的事情.

选择合适的崩溃服务

对于很多中小型公司来说,我并不建议自己去实现一套如此复杂的系统,可以选择一些第三方的服务。目前各种平台也是百花齐放,包括腾讯的Bugly、阿里的啄木鸟平台、网易云捕、Google 的 Firebase 等等。 个人在工作中使用的是腾讯的bugly

安全模式

安全模式 这个是我个人看过之后产生了一个不寒而栗的想法,如果未来人工智能已经成熟到了一定的地步,应用自动采集到了崩溃问题,那么人工智能经过大量的练习是可以去解决一部分场景的问题的,然后可以直接生成字节码补丁,然后推送到手机上,那么一大部分的码农又要面临被机器而取代. 让人看了之后不得不去努力学习. 特别是大的巨头公司在技术成熟之后直接做成平台.

如何客观地衡量稳定性

在这个段落中列出了一下应用异常退出场景

  • 主动自杀。Process.killProcess()、exit() 等。
  • 崩溃。出现了 Java 或 Native 崩溃。
  • 系统重启;系统出现异常、断电、用户主动重启等,我们可以通过比较应用开机运行时间是否比之前记录的值更小。
  • 被系统杀死。被 low memory killer 杀掉、从系统的任务管理器中划掉等。
  • ANR。

文章中提出在启动的时候设置一个标致,在下次启动的时候检测该标志来判断应用是否异常退出.

总结:

  • 1 小公司的话就直接采用合适的崩溃采集平台,但是要了解Native崩溃的捕获流程,和设计一个Native崩溃捕获框架要考虑的一些极端情况,大家要了解这些的原理,
  • 2 稳定性不能仅仅参考稳定性,要考虑 上文提到的5种情况,如果采集到某个机型或者某个模块异常退出比例较高,可以采取针对性测试,然后解决问题.
  • 3 在自己的应用中增加合适的安全模式.

祝大家五一劳动节快乐

在关于崩溃优化的第二章中,张绍文老师主要讲了如何去分析崩溃日志,这是一个商业文章,没有什么难点就不写了,再次向大家强烈推荐这个课程.