Crash优化:Crash捕获机制、堆栈解析、符号化、Crash聚合分析
说到Crash优化,我其实挺有感触的。刚入行那会儿,线上出了个闪退,我愣是盯着一个无符号的堆栈看了三天,最后发现是空指针。嗯,从那以后我就把Crash捕获和解析当成了基本功来练。今天咱们就聊聊这个话题,从捕获到分析,一条龙讲清楚。
Crash捕获机制:你得知道它怎么来的
Crash说白了就是程序跑崩了。在Android里,常见的Crash分两种:Java异常和Native信号。Java异常好抓,try-catch就能搞定。但Native层的信号,比如SIGSEGV(段错误)、SIGABRT(主动中止),你得用信号处理器来捕获。
我个人习惯在Application的attachBaseContext里就注册好全局异常处理器。这样不管哪个线程崩了,都能第一时间抓到。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 捕获Java层未处理异常
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
// 保存Crash信息到本地
saveCrashLog(t, e);
// 这里不要做耗时操作,否则会被系统杀掉
android.os.Process.killProcess(android.os.Process.myPid());
}
});
}
}
对于Native层Crash,我推荐用Google的Breakpad库。它能把信号处理器的上下文dump下来,生成minidump文件。你想想看,没有这个库,你连崩在哪都不知道。
堆栈解析:从乱码到可读
抓到Crash后,你得到的是什么?一堆内存地址和寄存器值。说白了就是天书。这时候就需要堆栈解析了。
Java层的堆栈相对友好,Throwable的printStackTrace就能打出调用链。但Native层就麻烦了,你得用addr2line或者llvm-symbolizer来把地址翻译成函数名和行号。
我记得有一次线上Crash,堆栈里全是问号。后来发现是编译时strip掉了符号表。嗯,这是个坑,大家一定要记住:发布release包时,保留符号文件(.sym或.map),但不要打进APK里。
符号化:把地址变成代码
符号化,说白了就是把内存地址映射回源代码。Android里有个工具叫ndk-stack,专门干这个的。用法很简单:
adb logcat | ndk-stack -sym /path/to/obj/local/armeabi-v7a/
但线上环境你没法用adb。所以我的做法是:在Crash捕获时,把堆栈地址和寄存器值一起存下来。然后上传到服务器,用脚本批量符号化。
这里有个关键点:符号文件必须和APK完全匹配。你想想看,如果代码改了一行,地址就全变了。所以我每次发版都会把符号文件归档到对象存储里,按版本号索引。
| 工具 | 适用场景 | 输入 | 输出 |
|---|---|---|---|
| addr2line | 单个地址解析 | 地址 + 符号文件 | 文件名:行号 |
| ndk-stack | logcat堆栈解析 | logcat输出 | 完整堆栈 |
| llvm-symbolizer | LLVM编译产物 | 地址 + 符号文件 | 函数名 + 行号 |
| Breakpad dump_syms | minidump解析 | minidump文件 | 符号化堆栈 |
Crash聚合分析:从个体到全局
单个Crash没什么好看的,关键是看趋势。我见过很多团队,每天盯着几十个Crash日志,一个一个看,累死也看不出规律。正确的做法是聚合分析。
聚合的核心思路是:把相似的Crash归为一类。怎么判断相似?看堆栈的相似度。我常用的方法是取堆栈的前5层(去掉系统库),计算哈希值。相同哈希的归为一类。
举个例子:
// Crash A: 空指针
java.lang.NullPointerException
at com.example.MainActivity.onClick(MainActivity.java:45)
at android.view.View.performClick(View.java:7125)
...
// Crash B: 也是空指针,但调用路径不同
java.lang.NullPointerException
at com.example.MainActivity.onClick(MainActivity.java:45)
at com.example.Utils.handleClick(Utils.java:20)
...
这两个Crash虽然都是空指针,但调用路径不同,应该归为不同类别。因为修复方式可能不一样。
聚合分析还有个好处:能发现隐藏的关联。比如某个Crash只在Android 12上出现,那八成是系统行为变更导致的。再比如某个Crash只在WiFi环境下出现,那可能是网络库的问题。
知识体系总览
下面这张图是我自己整理的Crash优化知识体系,从捕获到分析,再到闭环修复,每一步都有对应的工具和方法。
你看,从捕获到闭环,每一步都环环相扣。少了任何一环,Crash优化就成了瞎子摸象。我个人建议,先把捕获和符号化做好,这是基础。然后再上聚合分析,这样才能从海量Crash中找到真正需要修复的。
好了,Crash优化这块就聊到这儿。记住,工具再好,不如思路清晰。先把捕获机制搭好,再一步步把解析和聚合做起来,你的App稳定性一定会越来越好。