A
第 40 章JAVA35 分钟

崩溃监控:Firebase Crashlytics

Firebase Crashlytics 崩溃监控:SDK 接入、自动崩溃上报、自定义键值(setCustomKey)、面包屑日志(log)、用户标识、非致命异常、NDK 崩溃符号化、Dashboard 使用与堆栈分析。

学习目标

  • 能够接入 Firebase Crashlytics 并配置依赖
  • 掌握自定义键值、面包屑日志、用户标识的使用
  • 理解非致命异常记录与严重崩溃的区别
  • 了解 NDK 崩溃上报与 native 符号文件上传
  • 能够阅读 Crashlytics Dashboard 并分析崩溃堆栈

学习目标

  • 能够接入 Firebase Crashlytics 并配置依赖;
  • 掌握自定义键值、面包屑日志、用户标识的使用;
  • 理解非致命异常记录与严重崩溃的区别;
  • 了解 NDK 崩溃上报与 native 符号文件上传;
  • 能够阅读 Crashlytics Dashboard 并分析崩溃堆栈。

崩溃监控:Firebase Crashlytics

应用上线后,崩溃率 是衡量质量的核心 KPI(Google Play 内部阈值约 1.09% 为优秀)。但崩溃发生在用户设备上,开发者无法现场调试。Firebase Crashlytics 是 Google 官方的免费崩溃上报服务,自动收集 Java 与 Native 崩溃,归并去重,提供 Dashboard 分析。

一、接入准备

1. 创建 Firebase 项目

  1. 访问 Firebase Console,登录 Google 账号;
  2. 点击 「添加项目」,命名(如 “MyApp Prod”),选 Google Analytics 区域;
  3. 项目创建后,「+ 添加应用」 → 选 Android,填包名(com.example.app,必须与 build.gradle 一致);
  4. 下载 google-services.json,放到 app/ 目录;
  5. 在根 build.gradle 添加 Google Services 插件:
// 项目根目录 build.gradle
plugins {
    id 'com.google.gms.google-services' version '4.4.2' apply false
    id 'com.google.firebase.crashlytics' version '3.0.2' apply false
}

2. 配置 app 模块

// app/build.gradle
plugins {
    id 'com.android.application'
    id 'com.google.gms.google-services'
    id 'com.google.firebase.crashlytics'
}

android {
    compileSdk 34
    // 启用 native symbol 上传(如应用含 NDK)
    ndkVersion "26.1.10909125"

    buildTypes {
        release {
            // release 自动开启 native symbol 上传
            firebaseCrashlytics {
                nativeSymbolUploadEnabled true
                // 可选:指定 native 符号路径
                // strippedNativeLibsDir 'build/intermediates/stripped_native_libs/release/out/lib'
            }
        }
    }
}

dependencies {
    // Firebase BoM 统一版本管理
    implementation platform('com.google.firebase:firebase-bom:33.4.0')

    // Crashlytics(Java + NDK)
    implementation 'com.google.firebase:firebase-crashlytics'
    implementation 'com.google.firebase:firebase-crashlytics-ndk'

    // Analytics(可选,Crashlytics 部分功能依赖它)
    implementation 'com.google.firebase:firebase-analytics'
}

firebase-bom 是 BoM(Bill of Materials),统一管理版本号,下面 firebase-crashlytics 不需要写版本。

3. 初始化

Crashlytics 无需手动初始化——SDK 在 ContentProvider 自动启动并安装 UncaughtExceptionHandler。只要 google-services.json 配置正确,崩溃会自动上报。

如需关闭自动初始化(按用户同意 GDPR 后再启用):

<!-- AndroidManifest.xml -->
<meta-data
    android:name="firebase_crashlytics_collection_enabled"
    android:value="false" />

代码内按需启用:

import com.google.firebase.crashlytics.FirebaseCrashlytics;

public class AppInitializer {
    public static void enableCrashReportingIfConsented() {
        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
        boolean consented = PreferenceManager.getDefaultSharedPreferences(context)
                .getBoolean("analytics_consent", false);
        crashlytics.setCrashlyticsCollectionEnabled(consented);
    }
}

setCrashlyticsCollectionEnabled(true) 后的下一次崩溃起开始上报。

二、自动崩溃上报

Java/Kotlin 层未捕获的异常(如 NullPointerException、IllegalStateException)会被 Crashlytics 自动捕获、序列化、上传到 Firebase 后台。无需写一行代码。

测试上报:在某个按钮点击抛异常:

buttonCrash.setOnClickListener(v -> {
    throw new RuntimeException("Test Crash from Crashlytics demo");
});

崩溃后重启 App(Crashlytics 在下次启动时上报),打开 Firebase Console → Crashlytics 几分钟内可见该崩溃记录。

三、自定义键值(Custom Keys)

崩溃发生时,单纯看堆栈不够——你不知道用户当时在哪一页、什么状态。自定义键值 是附加在崩溃上的键值对(最多 64 对,key ≤ 1024 字符,value ≤ 1MB)。

import com.google.firebase.crashlytics.FirebaseCrashlytics;

public class CrashLogger {
    private final FirebaseCrashlytics crashlytics;

    public CrashLogger() {
        this.crashlytics = FirebaseCrashlytics.getInstance();
    }

    /** 用户登录后,记录用户身份便于排查 */
    public void onLoginSuccess(long userId, String level) {
        crashlytics.setUserId("user_" + userId);   // 不展示给用户,仅内部
        crashlytics.setCustomKey("user_level", level);
        crashlytics.setCustomKey("is_vip", true);
    }

    /** 进入某个页面 */
    public void onScreenView(String screenName) {
        crashlytics.setCustomKey("current_screen", screenName);
    }

    /** 进入支付页时记录订单 ID */
    public void onPaymentStart(String orderId, double amount) {
        crashlytics.setCustomKey("order_id", orderId);
        crashlytics.setCustomKey("payment_amount", amount);
        crashlytics.setCustomKey("payment_method", "alipay");
    }
}

Dashboard 中崩溃详情页会显示这些 key-value,方便定位“该崩溃是不是只发生在 VIP 用户?是否和特定订单相关?”

注意:

  • setCustomKey 是覆盖式(同名 key 后写覆盖前写);
  • setUserId 是 PII 敏感信息,Google 不建议放真实手机号/邮箱,用 hash 或内部 ID;
  • 长字符串(>1MB)会被截断。

四、面包屑日志(Custom Logs)

崩溃前发生了什么?面包屑日志(Breadcrumbs)按时间顺序记录最近 8 条事件,崩溃发生时附在报告里。

public class BreadcrumbLogger {
    private final FirebaseCrashlytics crashlytics;

    public BreadcrumbLogger() {
        this.crashlytics = FirebaseCrashlytics.getInstance();
    }

    /** 进入支付页 */
    public void logEnterPayment(String orderId) {
        crashlytics.log("Enter payment, order=" + orderId);
    }

    /** 网络请求前后 */
    public void logApiCall(String endpoint, int statusCode) {
        crashlytics.log("API " + endpoint + " -> " + statusCode);
    }

    /** 用户点击支付按钮 */
    public void logPayButtonClick(String method) {
        crashlytics.log("User clicked pay, method=" + method);
    }
}

崩溃堆栈旁会显示:

11:23:01  Enter payment, order=ORD_12345
11:23:05  API /api/order/ORD_12345 -> 200
11:23:12  User clicked pay, method=alipay
11:23:13  <-- 崩溃发生

crashlytics.log() 仅在崩溃发生时附带,不上传到服务器单独日志流——它不是日志框架替代品,是“事故现场的黑匣子”。

推荐做法:自定义 Log 拦截器

把 crashlytics.log 和项目日志(Timber)组合:

public class CrashlyticsTree extends Timber.Tree {
    private final FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();

    @Override
    protected void log(int priority, String tag, @NonNull String message, Throwable t) {
        // 只在 WARN 以上做面包屑
        if (priority >= Log.WARN) {
            crashlytics.log("[" + tag + "] " + message);
        }
        if (t != null && priority >= Log.ERROR) {
            // ERROR 异常作为非致命异常上报
            crashlytics.recordException(t);
        }
    }
}

// Application.onCreate
Timber.plant(new CrashlyticsTree());

普通 Timber.d 不进 Crashlytics,Timber.e(t, ...) 上报非致命异常。

五、用户标识

setUserId 是最常用的标识(最多 1024 字符):

crashlytics.setUserId("user_12345");

注意:

  • Crashlytics 不展示用户手机号/邮箱给开发者,但 setUserId 内容会上传到 Firebase。合规要求下用 hash 或内部 ID;
  • setUserId 一旦设置,会持久化到所有后续崩溃报告;
  • 退出登录时调用 crashlytics.setUserId(null) 清空,否则下个用户会继承前一个用户标识。

六、非致命异常(recordException)

不是所有异常都会让 App 崩溃。被 catch 的异常仍然可能代表问题——比如网络请求失败被 catch 后用户看到错误提示。Crashlytics 提供 recordException 上报这类“非致命异常”:

public class SafeRunner {
    private final FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();

    public void runRisky(Task task) {
        try {
            task.run();
        } catch (Exception e) {
            // 上报非致命异常,附带键值
            crashlytics.setCustomKey("task_name", task.getName());
            crashlytics.setCustomKey("attempt_count", task.getAttemptCount());
            crashlytics.recordException(e);

            // 用户层提示
            showErrorToast("操作失败,请重试");
        }
    }
}

Dashboard 默认区分“崩溃”与“非致命异常”两类,可分别过滤。

不要把所有 catch 都上报——会刷屏。仅上报“用户感知到但未崩溃”的关键失败(支付失败、登录失败、关键 API 错误)。

七、NDK 崩溃与符号化

应用含 C/C++ 代码(游戏/音视频)时,native 层崩溃(SIGSEGV、SIGABRT)也需要上报。Crashlytics NDK 自动捕获这些信号,但堆栈是内存地址——需要 native 符号文件 才能反解出函数名。

1. 启用 NDK 上报

build.gradle 已配置:

android {
    buildTypes {
        release {
            firebaseCrashlytics {
                nativeSymbolUploadEnabled true
            }
        }
    }
}

依赖(已加):

implementation 'com.google.firebase:firebase-crashlytics-ndk'

2. 上传 native 符号

构建 release 时插件会自动:

  1. 编译 release 时生成 lib/**/*.so + 对应的 *.sym.so(带符号的 native 库);
  2. 把 stripped so 打包进 APK/AAB;
  3. 把 *.sym.so 上传到 Crashlytics(需要网络)。

手动上传(CI 用):

./gradlew uploadCrashlyticsSymbolFileRelease

3. 堆栈反解

未上传符号时崩溃堆栈长这样:

backtrace:
  #00 pc 0x0000000000012345  /lib/arm64-via/libnative.so
  #01 pc 0x0000000000006789  /lib/arm64-via/libnative.so

上传后 Dashboard 显示:

backtrace:
  #00 pc 0x0000000000012345  libnative.so  (Java_com_example_NativePlayer_decode+45)
  #01 pc 0x0000000000006789  libnative.so  (audio_callback+128)

能直接看到 C 函数名和源代码行号。

没符号文件的 native 崩溃等于盲查。每次 release 必须执行 uploadCrashlyticsSymbolFileRelease,CI 里加为发布流水线必经步骤。

八、Crashlytics Dashboard

1. 主要视图

打开 Firebase Console → Crashlytics:

  • 概览:崩溃率曲线、无崩溃用户百分比(NCU = No Crash Users,Google 内部质量指标,目标 ≥ 99.7%);
  • 问题列表:每条崩溃按堆栈指纹(signature)聚合,显示发生次数、影响用户数、首次/最后出现时间;
  • 崩溃详情:堆栈、设备分布、Android 版本分布、自定义键值、面包屑、用户标识。

2. 关键指标

  • 崩溃率(Crash Rate):崩溃会话数 / 总会话数。Google 行业基准 0.1%(优秀)/ 1.09%(中位数)/ 2.79%(P90);
  • 影响用户数:受该崩溃影响的 unique 用户,更关键——同一崩溃可能影响几千个用户但只算 1 个“问题”;
  • 首次出现时间:判断“是新版本引入还是历史遗留”——对照 git 提交时间快速定位 commit。

3. 堆栈分析

点击某条崩溃,展开“堆栈”面板:

Fatal Exception: java.lang.NullPointerException
  at com.example.app.feature.payment.PaymentActivity.onCreate(PaymentActivity.java:84)
  at android.app.Activity.performCreate(Activity.java:8200)
  ...

如果堆栈是 a.b.c() 形式——是混淆过的名字,必须用 mapping.txt 反混淆:

# Android SDK 工具
./sdk/tools/bin/retrace.sh mapping.txt obfuscated_stack.txt > deobfuscated.txt

# 或用 Crashlytics 上传 mapping 让其自动反解:
./gradlew uploadMappingFileRelease

Crashlytics 支持上传 mapping.txt 后自动反混淆,无需手动 retrace。

4. 过滤与排序

Dashboard 顶部过滤器:

  • 版本:选某个 versionCode/versionName 看其崩溃;
  • 设备型号:判断“只发生在某机型”(如某 ROM 的 bug);
  • Android 版本:判断“只发生在 Android 14”;
  • 时间范围:30/7/1 天,看趋势;
  • 事件类型:崩溃 / 非致命 / ANR。

九、ANR 上报

Android 11+ 起,系统会自动捕获 ANR(Application Not Responding)并通过 Crashlytics 上报。ANR 在主线程被阻塞 5 秒以上触发,常见原因:

  • 主线程做 IO / 网络;
  • 主线程持锁等待子线程;
  • BroadcastReceiver onReceiver 内执行慢任务;
  • ContentProvider 初始化做重活。

Dashboard 中 ANR 单独分类,关键排查思路:

  1. 看 ANR 主线程堆栈,定位卡在哪个方法;
  2. 看自定义键值(current_screen 等),了解上下文;
  3. 看 native 线程状态(主线程被 native 锁阻塞时 Java 堆栈可能正常)。

十、Velocity Alert 实时告警

Crashlytics 的 Velocity Alert 在崩溃率突然飙升时实时邮件/Slack 通知:

  • 触发条件:某崩溃在 1 小时内影响超过 1% 用户(默认);
  • 邮件包含崩溃详情链接;
  • 可在 Settings 调整阈值或 webhook。

集成 Slack:

  1. Firebase Console → 集成 → Slack;
  2. 授权工作空间;
  3. 选择通知频道,Crashlytics 告警自动转发。

常见坑与最佳实践

  1. 未上传 mapping.txt:崩溃堆栈是 a.b.c(),无法定位。每次发布提交后必须 ./gradlew uploadMappingFileRelease。
  2. setUserId 用真实手机号:违反 PII 合规。用 hash 或内部 ID,并在隐私政策中声明。
  3. log 滥用导致 SDK 流量超限:crashlytics.log 仅当崩溃发生时上报,但每次崩溃会带最多 8 条,请勿塞大量日志。
  4. GDPR 未做用户同意:默认开启上报在欧盟可能违规。用 firebase_crashlytics_collection_enabled=false + setCrashlyticsCollectionEnabled(true) 按需启用。
  5. recordException 滥用:catch 全部异常都上报导致非致命列表爆炸。仅上报关键失败路径。
  6. Native 符号未上传:NDK 崩溃堆栈全是地址,等于没上报。CI 流水线必须包含 uploadCrashlyticsSymbolFileRelease。
  7. 测试崩溃污染生产数据:开发期 throw new RuntimeException("Test") 上报到生产 project。建议 debug build 用 firebaseCrashlytics { mappingFileAggregationEnabled false } 或用测试 Firebase project。
  8. Crashlytics 上线后崩了:版本未到 Firebase 项目注册(包名不一致),崩溃丢失。google-services.json 重新下载核对。
  9. 不上线 Slack 告警:上线后崩溃率飙升无人知,等用户投诉才反应。Velocity Alert + Slack webhook 必配。
  10. 只看崩溃率不看分布:总体 0.5% 但 Android 14 占 3%,说明新版本适配问题。务必按 Android 版本/设备过滤看细分。

章节小结

本章覆盖了 Crashlytics 的核心用法:

  • 接入:Firebase 项目 + google-services.json + BoM + Crashlytics 插件,无需手动初始化即自动上报 Java 崩溃;
  • 自定义键值:setCustomKey + setUserId 记录崩溃现场,最多 64 对;
  • 面包屑:crashlytics.log() 按时间顺序记录最近 8 条,配合 Timber Tree 自动记录 WARN 以上日志;
  • 非致命异常:recordException 上报被 catch 但用户感知的关键失败;
  • NDK 崩溃:nativeSymbolUploadEnabled true + uploadCrashlyticsSymbolFileRelease,没有符号文件等于盲查;
  • Dashboard:按版本/设备/Android 版本过滤,看崩溃率(目标 < 0.1%)和影响用户数,mapping.txt 上传后自动反混淆堆栈;
  • 告警:Velocity Alert + Slack webhook 在崩溃率飙升时实时通知。

下一章预告

第 41 章讨论热修复与插件化:基于 ClassLoader/Dex 元素的热修复原理(Tinker)、VirtualApk 插件化方案,以及 Google Play 政策下热修复的合规风险与替代方案(应用内更新 / 远程配置)。