崩溃是Android应用最严重的质量问题,一次闪退可能直接导致用户流失和差评。要有效治理崩溃,第一步就是建立完善的异常捕获与上报机制。Android系统在发生未捕获异常时会直接终止进程,如果在终止前没有把异常信息记录下来,这次崩溃就彻底消失了。本文将从Java层和Native层两个维度,讲解崩溃捕获的完整实现方案。

一、Java层崩溃捕获:UncaughtExceptionHandler原理与使用
Java层的崩溃本质上是一个未被try-catch捕获的异常,沿着调用栈一路抛到了线程的顶层。Android系统中,每个线程都可以设置一个UncaughtExceptionHandler,当异常没有被任何层捕获时,这个处理器会被回调,随后进程被杀掉。系统的默认处理器(在应用进程中是RuntimeInit里的KillApplicationHandler)做的事情很简单:打印堆栈日志,然后杀死进程。
既然默认处理器会直接杀进程,我们的思路就是用自己的处理器替换它,在进程死亡前的最后几百毫秒里把崩溃信息保存下来。核心代码如下:
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private final Thread.UncaughtExceptionHandler mDefaultHandler;
private final Context mContext;
private CrashHandler(Context context) {
mContext = context.getApplicationContext();
// 保存系统默认的处理器,处理完自己的逻辑后可以继续交给它
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
}
public static void init(Context context) {
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context));
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
// 1. 拼装崩溃信息
String crashInfo = buildCrashInfo(thread, ex);
// 2. 写入本地文件,等待下次启动上报
saveToFile(crashInfo);
// 3. 也可以在这里尝试同步上报
// uploadCrash(crashInfo);
// 4. 交给系统默认处理器,让系统走正常流程
if (mDefaultHandler != null) {
mDefaultHandler.uncaughtException(thread, ex);
}
}
private String buildCrashInfo(Thread thread, Throwable ex) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw); // 打印完整堆栈
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(pw);
cause = cause.getCause();
}
pw.flush();
return "Thread: " + thread.getName() + "\n" + sw.toString();
}
}
有几个细节需要注意。首先,uncaughtException回调运行在崩溃线程上,如果崩溃发生在主线程,此时不能做任何依赖主线程的UI操作;其次,这个方法里要尽量用同步方式写文件,因为进程随时可能被系统回收,异步任务很可能来不及执行;最后,一定要保存系统默认处理器的引用并在最后调用它,否则系统的崩溃对话框和应用重启逻辑都不会触发。
除了setDefaultUncaughtExceptionHandler设置主处理器,还可以对单个线程调用thread.setUncaughtExceptionHandler()设置线程级别的处理器,线程级处理器优先级更高。实际项目中,后台线程的崩溃同样会导致整个进程被杀,所以统一在默认处理器里处理就够了。
二、崩溃信息收集与上报策略设计
光有堆栈还不够,定位线上问题需要更多上下文。一份完整的崩溃报告通常包含以下内容:
- 崩溃堆栈及异常类型、异常消息(cause链要完整展开)
- 设备信息:机型、系统版本、CPU架构、屏幕分辨率
- 应用信息:版本号、versionCode、渠道号、打包时间
- 运行环境:内存占用、磁盘剩余空间、是否在前台、电量状态
- 用户行为路径:崩溃前最近若干次页面切换或关键操作日志
其中用户行为路径的价值非常高,很多崩溃只能靠复现步骤才能定位,记录最近的操作日志(通常用环形缓冲区保存最近100条)能极大提升排查效率。收集完信息后,上报策略是个关键设计点。主流做法有两种:
第一种是本地持久化加下次启动补报。崩溃发生时把信息写入私有目录下的文件,应用下次启动时检查并统一上传。这种方式最可靠,不受网络状态影响,缺点是时效性差,开发者要等用户再次打开应用才能看到数据。第二种是崩溃时实时上报,在uncaughtException里同步发起网络请求。时效性好,但风险也高:进程随时可能被杀,网络请求很可能发送一半就中断,而且如果崩溃发生频繁,反复重试会消耗流量。成熟方案如Bugly的做法是两者结合——先落盘保证数据不丢,再尽力尝试实时上报,失败则等待补报。
上报时还要考虑数据聚合。同一个崩溃点可能产生几万条记录,客户端可以给每条崩溃计算一个指纹(通常是堆栈首行加上异常类名的哈希),服务端按指纹聚合统计,就能算出每个崩溃的影响人数和崩溃率,而不是淹没在重复日志里。
三、Native崩溃捕获与成熟方案选型
Java层的方案只能覆盖Java异常,NDK开发、so库内部的信号错误(如空指针访问、内存越界)属于Native崩溃,走的是Linux信号机制,UncaughtExceptionHandler完全捕获不到。Native崩溃的表现是进程收到SIGSEGV、SIGABRT等致命信号,默认行为是直接终止。
捕获Native崩溃的思路是注册信号处理函数,在进程死亡前把当时的寄存器状态、调用栈、内存映射等信息写下来:
#include <signal.h>
#include <execinfo.h>
#include <unistd.h>
void crash_signal_handler(int sig, siginfo_t *info, void *context) {
// 注意:信号处理函数中只能使用异步信号安全的函数
void *array[50];
int size = backtrace(array, 50);
// 先写文件保存堆栈,再走默认处理让进程终止
int fd = open("/data/data/com.example.app/crash.log",
O_WRONLY | O_CREAT | O_APPEND, 0600);
if (fd >= 0) {
backtrace_symbols_fd(array, size, fd);
close(fd);
}
signal(sig, SIG_DFL);
raise(sig);
}
void install_crash_handler() {
struct sigaction sa;
sa.sa_sigaction = crash_signal_handler;
sa.sa_flags = SA_SIGINFO | SA_RESETHAND;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGABRT, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
}
写一个生产级的Native崩溃捕获器并不容易:信号处理函数里不能使用malloc、pthread锁等非信号安全的接口,堆栈解析需要处理C++符号名demangle,64位设备上还要注意栈溢出时连处理函数自身的栈都可能不可用。因此大多数团队直接选用成熟方案。Firebase Crashlytics免费且集成简单,后台体验好,适合出海应用;腾讯Bugly对国内环境友好,同时支持Java和Native崩溃,还带ANR监控;如果需要完全自主可控,可以基于开源的xCrash库二次开发,它由爱奇艺团队开源,覆盖了Java崩溃、Native崩溃和ANR三大场景,稳定性经过了大规模验证。
选型时的一个建议:无论使用哪个第三方方案,最好保留一份自建的最简崩溃落盘逻辑作为兜底。第三方SDK自身也可能初始化失败或者在某些机型上失效,多一层保险总没有坏处。
四、常见踩坑点与优化建议
第一,处理器的设置时机要尽早,建议放在Application.onCreate的最前面,保证应用启动早期发生的崩溃也能被捕获。第二,注意处理器的覆盖问题,多个SDK可能都注册了自己的UncaughtExceptionHandler,后注册的会覆盖先注册的,所以自定义处理器初始化时要先取出并保存旧的默认处理器,形成处理器链,避免破坏其他SDK的逻辑。第三,uncaughtException里不要做耗时操作,写文件控制在几百毫秒内,避免触发系统的ANR弹窗干扰正常流程。
另外要区分崩溃和ANR。ANR是主线程长时间无响应触发的,走的是系统的/data/anr/traces.txt机制,与UncaughtExceptionHandler无关。监控ANR通常用FileObserver监听trace文件变化,或者用看门狗线程定期向主线程发送消息检测响应状态。两者监控体系要分开建设,但上报通道可以复用。
最后,崩溃监控不是搭建完就结束了,要建立持续运营的闭环:每日查看崩溃率变化、对新增崩溃及时分配负责人、验证修复后线上数据是否收敛。工具只是手段,把崩溃率降下来才是最终目的。
Android CrashUncaughtExceptionHandler崩溃上报修改时间:2026-09-08 10:41:10