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 集成步骤

  1. build.gradle中添加依赖
  2. 在Application中初始化
  3. 配置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小时内只上报一次。

五、知识体系总览

下面这张图,是我对崩溃收集与异常处理整个知识体系的梳理。你可以把它当作一个检查清单,看看自己还有哪些没覆盖到。

崩溃收集与异常处理 Java层异常 UncaughtExceptionHandler 保存堆栈到本地文件 自定义处理(重启/退出) Native层异常 signal()信号处理器 第三方SDK(Bugly等) 符号表解析 上报策略 下次启动上报 去重与限流 Gzip压缩

六、总结

崩溃收集这件事,说白了就是“防患于未然”。你永远不知道用户会在什么奇葩环境下运行你的App。我见过在冰箱里用手机的,也见过在电梯里信号断断续续的。没有崩溃收集,这些问题你永远发现不了。

我的建议是:小项目用Bugly,大项目自研+Bugly双通道。自研保证数据安全,Bugly作为兜底。嗯,这样最稳妥。

最后提醒一句:崩溃收集只是手段,不是目的。真正重要的是分析崩溃原因,修复Bug,让App越来越稳定。别光顾着收集,忘了修啊。


公众号:蓝海资料掘金营,微信deep3321