稳定性优化的维度
很多人都会认为稳定性优化就是降低Crash
率,但如果你的APP
没有崩溃,但是关键功能却不可用,这又怎么算是稳定的呢?
因此应用的稳定性可以分为三个纬度,如下所示:
- 1、
Crash
纬度:最重要的指标就是应用的Crash
率。 - 2、性能纬度:包括启动速度、内存、绘制等等优化方向,相对于
Crash
来说是次要的,但也是应用稳定性的一部分。 - 3、业务高可用纬度:它是非常关键的一步,我们需要采用多种手段来保证我们
App
的主流程以及核心路径的稳定性。
Crash
处理的一般步骤
下面我们来看下应该如何处理Crash
,即如果应用崩溃了,你应该如何去分析?
主要从崩溃现场和崩溃分析两个角度来分析
崩溃现场
崩溃现场是我们的“第一案发现场”,它保留着很多有价值的线索。在这里我们挖掘到的信息越多,下一步分析的方向就越清晰,而不是去靠盲目猜测。
接下来我们具体来看看在崩溃现场应该采集哪些信息。
崩溃信息
从崩溃的基本信息,我们可以对崩溃有初步的判断。
- 进程名、线程名。崩溃的进程是前台进程还是后台进程,崩溃是不是发生在 UI 线程。
- 崩溃堆栈和类型。崩溃是属于
Java
崩溃、Native
崩溃,还是ANR
,对于不同类型的崩溃我们关注的点也不太一样。特别需要看崩溃堆栈的栈顶,看具体崩溃在系统的代码,还是我们自己的代码里面。
系统信息
除了崩溃的信息之外,系统的信息有时候会带有一些关键的线索,对我们解决问题有非常大的帮助。
Logcat
输出。这里包括应用、系统的运行日志。有时从堆栈中看不出什么信息,反而可以从Logcat
中获得意外收获- 机型、系统、厂商、
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
),我们可以得出应用本身内存的占用大小和分布。 - 虚拟内存。虚拟内存可以通过
/proc/self/status
得到,通过/proc/self/maps
文件可以得到具体的分布情况。有时候我们一般不太重视虚拟内存,但是很多类似OOM
、tgkill
等问题都是虚拟内存不足导致的。
Name: com.sample.name // 进程名
FDSize: 800 // 当前进程申请的文件句柄个数
VmPeak: 3004628 kB // 当前进程的虚拟内存峰值大小
VmSize: 2997032 kB // 当前进程的虚拟内存大小
Threads: 600 // 当前进程包含的线程个数
一般来说,对于 32 位进程,如果是 32 位的 CPU
,虚拟内存达到 3GB 就可能会引起内存申请失败的问题。如果是 64 位的 CPU
,虚拟内存一般在 3~4GB 之间。当然如果我们支持 64 位进程,虚拟内存就不会成为问题。因此我们的应用应该尽量适配64位
资源信息
有的时候我们会发现应用堆内存和设备内存都非常充足,还是会出现内存分配失败的情况,这跟资源泄漏可能有比较大的关系。
- 文件句柄
fd
。一般单个进程允许打开的最大文件句柄个数为1024
。但是如果文件句柄超过800
个就比较危险,需要将所有的fd
以及对应的文件名输出到日志中,进一步排查是否出现了有文件或者线程的泄漏 - 线程数。一个线程可能就占
2MB
的虚拟内存,过多的线程会对虚拟内存和文件句柄带来压力。根据我的经验来说,如果线程数超过 400 个就比较危险。需要将所有的线程id
以及对应的线程名输出到日志中,进一步排查是否出现了线程相关的问题。
应用信息
除了系统,其实我们的应用更懂自己,可以留下很多相关的信息。
- 崩溃场景。崩溃发生在哪个
Activity
或Fragment
,发生在哪个业务中。 - 关键操作路径。不同于开发过程详细的打点日志,我们可以记录关键的用户操作路径,这对我们复现崩溃会有比较大的帮助。
- 其他自定义信息。不同的应用关心的重点可能不太一样,比如网易云音乐会关注当前播放的音乐,QQ 浏览器会关注当前打开的网址或视频。此外例如运行时间、是否加载了补丁、是否是全新安装或升级等信息也非常重要。
上面介绍了在崩溃现场应该采集的信息,当然开发一个这样的采集平台还是很复杂的,大多数情况我们只需要接入一些第三方的平台比如bugly
和Sentry
即可。但是通过上述介绍,我们可以知道在分析崩溃的时候应该重点关注哪些信息,同时如果平台能力有缺失,我们也可以添加自定义的上报
崩溃分析
在崩溃现场上报了足够的信息之后,我们就可以开始分析崩溃了,下面我们介绍崩溃分析“三部曲”
第一步:确定重点
确认和分析重点,关键在于在日志中找到重要的信息,对问题有一个大致判断。一般来说,我建议在确定重点这一步可以关注以下几点。
- 确认严重程度与优先级。解决崩溃也要看性价比,我们优先解决
Top
崩溃或者对业务有重大影响, - 崩溃基本信息。确定崩溃的类型以及异常描述,对崩溃有大致的判断。一般来说,大部分的简单崩溃经过这一步已经可以得到结论。
Java
崩溃。Java
崩溃类型比较明显,比如NullPointerException
是空指针,OutOfMemoryError
是资源不足,这个时候需要去进一步查看日志中的 “内存信息”和“资源信息”。Native
崩溃。需要观察signal
、code
、fault addr
等内容,以及崩溃时Java
的堆栈。关于各signal
含义的介绍,你可以查看 崩溃信号介绍 。比较常见的是有SIGSEGV
和SIGABRT
,前者一般是由于空指针、非法指针造成,后者主要因为ANR
和调用abort()
退出所导致。ANR
。我的经验是,先看看主线程的堆栈,是否是因为锁等待导致。接着看看ANR
日志中iowait
、CPU
、GC
、system server
等信息,进一步确定是I/O
问题,或是CPU
竞争问题,还是由于大量GC
导致卡死
Logcat
。Logcat
一般会存在一些有价值的线索,日志级别是Warning
、Error
的需要特别注意。从Logcat