68、崩溃收集与异常处理:UncaughtExceptionHandler、Bugly集成、自定义上报
崩溃收集,说白了就是给App装个“黑匣子”。
App上线后,用户手机上的崩溃你是看不到的。没有崩溃收集,你就像闭着眼睛开车。我个人习惯,项目启动的第一天就把崩溃收集加上,哪怕只是个简单的本地日志。
一、Java层的“最后一道防线”:UncaughtExceptionHandler
当你的App发生未捕获异常时,系统会调用线程的UncaughtExceptionHandler。默认情况下,系统会直接弹个“很抱歉,XX已停止运行”的对话框,然后杀掉进程。
我们可以自己实现一个,在App死亡前做点事情——比如保存堆栈到本地文件。
核心思路:在Application初始化时,替换掉默认的异常处理器。
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private static final String CRASH_DIR = "crash_logs";
private Context mContext;
private CrashHandler() {}
private static class Holder {
private static final CrashHandler INSTANCE = new CrashHandler();
}
public static CrashHandler getInstance() {
return Holder.INSTANCE;
}
public void init(Context context) {
mContext = context.getApplicationContext();
// 把默认的处理器保存起来
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
// 1. 保存崩溃日志到本地
saveCrashToFile(e);
// 2. 可以在这里做自定义处理,比如重启App
// 3. 调用系统默认处理器,或者直接退出
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(1);
}
private void saveCrashToFile(Throwable throwable) {
File dir = new File(mContext.getExternalFilesDir(null), CRASH_DIR);
if (!dir.exists()) {
dir.mkdirs();
}
String fileName = "crash_" + System.currentTimeMillis() + ".log";
File file = new File(dir, fileName);
try (FileOutputStream fos = new FileOutputStream(file);
PrintWriter pw = new PrintWriter(fos)) {
pw.println("Time: " + new Date().toString());
pw.println("Thread: " + Thread.currentThread().getName());
throwable.printStackTrace(pw);
pw.flush();
} catch (IOException ioException) {
Log.e(TAG, "save crash log failed", ioException);
}
}
}
我的经验:我曾经在某个项目中只保存了崩溃日志,但忘了处理ANR。结果用户反馈App卡死,日志里却什么都没有。后来我加上了ANR的监控,才把问题定位到——是一个死循环导致的。
嗯,这里要注意:uncaughtException方法里不能做耗时操作。系统给你几秒钟时间,超时就直接杀进程了。所以保存日志要快,别在这里上传网络。
二、Native层的崩溃怎么抓?
Java层的UncaughtExceptionHandler只能捕获Java异常。如果你的App用了JNI或者C/C++代码,发生段错误(SIGSEGV)时,Java层是收不到的。
这时候需要接入第三方SDK,比如Bugly、腾讯的Matrix、或者自己用signal()函数注册信号处理器。说实话,自己写Native崩溃收集成本很高,我建议直接用现成的。
三、Bugly集成:最省心的方案
Bugly是腾讯出品的免费崩溃监控平台。集成简单,上报稳定,还自带符号表解析。我个人在好几个项目里都用它,基本没出过问题。
3.1 集成步骤
- 在
build.gradle中添加依赖 - 在Application中初始化
- 配置AppId和AppKey
// app/build.gradle
dependencies {
implementation 'com.tencent.bugly:crashreport:4.1.9'
implementation 'com.tencent.bugly:nativecrashreport:3.9.2' // 如果需要捕获Native崩溃
}
// Application
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
// 第三个参数是调试模式开关,上线前记得关掉
Bugly.init(getApplicationContext(), "你的AppId", false);
}
}
避坑指南:我曾经在混淆配置上栽过跟头。Bugly需要保留一些类不被混淆,否则上报的堆栈全是混淆后的名字,根本看不懂。记得在proguard-rules.pro里加上Bugly的keep规则。
3.2 Bugly的优势
| 功能 | 说明 |
|---|---|
| Java崩溃 | 自动捕获,无需额外代码 |
| Native崩溃 | 需要添加nativecrashreport依赖 |
| ANR监控 | 默认开启,可配置阈值 |
| 符号表解析 | 上传mapping文件后自动反混淆 |
| 自定义日志 | 支持在崩溃前添加自定义上下文 |
四、自定义上报:当Bugly不够用时
有些场景下,你可能不想用第三方SDK。比如公司有自研的监控平台,或者App涉及敏感数据,不允许外传。这时候就需要自己实现上报逻辑。
4.1 上报策略
- 时机:App下次启动时上报,不要在崩溃时立即上报(网络可能不可用)
- 去重:相同的崩溃只上报一次,避免刷爆服务器
- 压缩:日志文件用Gzip压缩后再上传,节省流量
public class CrashReporter {
public static void uploadCrashLogs(Context context) {
File dir = new File(context.getExternalFilesDir(null), "crash_logs");
File[] files = dir.listFiles();
if (files == null || files.length == 0) return;
for (File file : files) {
// 读取文件内容
String content = readFile(file);
// 上传到服务器(伪代码)
// uploadToServer(content);
// 上传成功后删除本地文件
file.delete();
}
}
private static String readFile(File file) {
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
}
我的建议:自定义上报时,一定要加个“上传频率限制”。我曾经遇到过测试同学反复触发崩溃,结果一天上传了几万条重复日志,直接把测试服务器打挂了。后来我加了个本地缓存,相同的堆栈MD5值24小时内只上报一次。
五、知识体系总览
下面这张图,是我对崩溃收集与异常处理整个知识体系的梳理。你可以把它当作一个检查清单,看看自己还有哪些没覆盖到。
六、总结
崩溃收集这件事,说白了就是“防患于未然”。你永远不知道用户会在什么奇葩环境下运行你的App。我见过在冰箱里用手机的,也见过在电梯里信号断断续续的。没有崩溃收集,这些问题你永远发现不了。
我的建议是:小项目用Bugly,大项目自研+Bugly双通道。自研保证数据安全,Bugly作为兜底。嗯,这样最稳妥。
最后提醒一句:崩溃收集只是手段,不是目的。真正重要的是分析崩溃原因,修复Bug,让App越来越稳定。别光顾着收集,忘了修啊。
公众号:蓝海资料掘金营,微信deep3321