崩溃优化(下)应用崩溃了,你应该如何去分析?
崩溃现场
1. 崩溃信息
从崩溃的基本信息,可以对崩溃有初步的判断。
- 进程名、线程名。崩溃的进程是前台进程还是后台进程,崩溃是不是发生在 UI 线程。
- 崩溃堆栈和类型。崩溃是属于 Java 崩溃、Native 崩溃,还是 ANR。特别需要看崩溃堆栈的栈顶,看具体崩溃在系统代码,还是我们自己的代码里面。
AndroidRuntime: FATAL EXCEPTION: MyThread
Process: com.simple.carsh, PID: 6825
java.lang.ArithmeticException: divide by zero
at com.simple.crash.TouchActivity$1.run(TouchActivity.java:33)
2. 系统信息
-
Logcat。包括应用、系统运行日志。其中系统的 event logcat 会记录 App 运行的一些基本情况,记录在文件 /system/etc/event-log-tags 中
-
机型、系统、厂商、CPU、ABI、Linux 版本等;
-
设备状态:是否 root、是否模拟器;
3. 内存信息
OOM 、 ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系。
- 系统剩余内存。关于系统内存状态,可以直接读取文件 /proc/meminfo 当系统可用内存很小(低于 MemTotal 的 10%)时,OOM、大量 GC、系统频繁自杀拉起等问题都非常容易出现。
// /proc/meminfo
MemTotal: 1550948 kB
MemFree: 465832 kB
Buffers: 25008 kB
Cached: 764404 kB
SwapCached: 0 kB
Active: 480312 kB
Inactive: 550552 kB
Active(anon): 241468 kB
Inactive(anon): 12292 kB
Active(file): 238844 kB
Inactive(file): 538260 kB
Unevictable: 0 kB
Mlocked: 0 kB
HighTotal: 663432 kB
HighFree: 13024 kB
LowTotal: 887516 kB
LowFree: 452808 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 241436 kB
Mapped: 188572 kB
Shmem: 12324 kB
Slab: 21936 kB
SReclaimable: 10864 kB
SUnreclaim: 11072 kB
KernelStack: 4056 kB
PageTables: 6132 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 775472 kB
Committed_AS: 18331812 kB
VmallocTotal: 122880 kB
VmallocUsed: 26120 kB
VmallocChunk: 31652 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 4096 kB
DirectMap4k: 16376 kB
DirectMap4M: 892928 kB
- 应用使用内存。包括 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 等问题都是虚拟内存不足导致的。
// /proc/self/status
Name: com.sample.name // 进程名
FDSize: 800 // 当前进程申请的文件句柄个数
VmPeak: 3004628 kB // 当前进程的虚拟内存峰值大小
VmSize: 2997032 kB // 当前进程的虚拟内存大小
Threads: 600 // 当前进程包含的线程个数
4. 资源信息
-
文件句柄 fd 。文件句柄的限制可以通过 /proc/self/limits 获得,一般单个进程允许打开的最大文件句柄个数为 1024。但是如果文件句柄超过 800 个就比较危险,需要将所有的 fd 以及对应的文件名输出到日志中,进一步排查是否出现了有文件或者线程的泄漏。
-
线程数。当前线程数大小可以通过上面 status 文件得到,一个线程可能占 2MB 的虚拟内存,过多的线程会对虚拟内存和文件句柄带来压力。
-
JNI。使用 JNI 时,如果不注意很容易出现引用失效,引用爆表等一些崩溃。
5. 应用信息
- 崩溃场景。崩溃发生在哪个 Activity 或 Fragment,发生在哪个业务中。
- 关键操作路径。
崩溃分析
第一步:确认重点
- 确认严重程度。优先解决 Top 崩溃或者对业务有重大影响的崩溃。
- 崩溃的基本信息。确定崩溃的类型已经异常描述,对崩溃有大致的判断。
-
Java 崩溃。Java 崩溃比较明显,比如 NullPointerException 是空指针;
-
Native 崩溃。需要观察 signal、code、fault addr 等内容,已经崩溃时 Java 的堆栈。崩溃信号介绍
-
ANR。先看看主线程的堆栈,是否是因为锁等待导致。接着看看 ANR 日志中 iowait、CUP、GC、system server 等信息,进一步确认是 I/O 问题,或者是 CPU 竞争问题,还是由于大量 GC 导致卡死。
-
LogCat。从 LogCat 中我们可以看到当时系统的一些行为跟手机状态,例如出现 ANR 时,会有 “am_arn”; App 被杀时,会有“am_kill” 。
-
各个资源情况。结合崩溃信息,看看是不是跟“内存信息”有关,是不是跟“资源信息”有关,比如物理内存不足,虚拟内存不足,还是文件句柄 fb 泄漏了。
内存与线程相关的信息都需要特别注意,很多崩溃都是它们使用不当造成的。
第二步:查找共性
共性问题例如是不是因为安装了 Xposed,是不是只出现在 x86 的手机等
第三步:尝试复现
-
查找功能的原因。通过上面的共性归类,先看看是某个系统版本的问题,还是某个厂商特定 ROM 的问题。
-
尝试规避。查看可疑代码调用,是否使用了不恰当的 API,是否可以切换其他的实现方式规避。
-
Hook 解决。