Administrator
发布于 2026-08-26 / 30 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 86 篇:Crash 监控与稳定性工程:从 Bugly 到自建 APM

前言

ANR 之后的最大杀手是 Crash。本篇聊完整的崩溃监控与稳定性工程:从 Crash 发生、上报、聚合、定位、修复、回归。一个 100 万 DAU 的 App,每天能看到 5K-50K 次 Crash,治理不好就是日复一日的负面评价。

一、Crash 的分类

类型

出现频率

修复难度

典型原因

RuntimeException

高

中

NPE、ClassCast

OutOfMemoryError

中

高

Bitmap 大、内存泄漏

NullPointerException

高

低

漏判 null

IllegalStateException

中

中

状态机错误

SecurityException

低

低

权限被撤销

StackOverflowError

低

中

递归深

SIGSEGV (Native)

中

高

JNI 崩溃

ANR

中

中

主线程阻塞

二、Bugly vs Firebase Crashlytics vs 自建

维度

Bugly

Firebase Crashlytics

自建

服务可用性

国内好

国内访问受限

自己维护

上报实时性

< 1 分钟

< 5 分钟

实时

聚合能力

强

强

自定义

用户数限制

无

无

自定义

自定义字段

支持

支持

完全可控

调试支持

Symbol 上传

完整

自定义

价格

免费

免费

成本自担

中小团队推荐 Bugly + 自建(前后端配合);海外选 Crashlytics;重视隐私/可控的选自建。

三、Bugly 上手与最佳实践

3.1 集成

// build.gradle.kts
implementation("com.tencent.bugly:crashreport:5.0.1")
implementation("com.tencent.bugly:nativecrashreport:4.0.3")
// App.onCreate
CrashReport.initCrashReport(this, "bugly-app-id", BuildConfig.DEBUG)
CrashReport.setUserId("user_$userId")
CrashReport.setDeviceId(AndroidId)
CrashReport.setDeviceModel(Build.MODEL)

3.2 自定义 key-value

CrashReport.putUserData(this, "page", currentPage)
CrashReport.putUserData(this, "channel", "huawei")

崩溃发生时,这些值会跟着上报,定位时能筛"特定渠道/页面"的问题。

3.3 上传 symbol 文件

// 设置 Bugly symbol 上传任务
bugly {
    appId = "xxxx"
    uploadSymbol = true
    symbolDir = file("build/native_libs").absolutePath
}

Native crash 必须有 symbol 才能看到正确栈。

四、自建 Crash 监控系统的架构

┌──────────────────────────────────────┐
│              App 端                  │
├──────────────────────────────────────┤
│  Crash Handler                       │
│   - Thread.setDefaultUncaughtHandler │
│   - SIGSEGV / SIGBUS / SIGABRT      │
│   - ANR Detector                     │
│   - Local Buffer (offline tolerant)  │
└────────┬─────────────────────────────┘
         │  HTTPS (带重试)
         ▼
┌──────────────┐    ┌────────────────┐
│ Collector    │───▶│  Kafka/Queue   │
└──────────────┘    └────────┬───────┘
                            ▼
                     ┌────────────────┐
                     │  Storage       │
                     │ (ClickHouse/ES)│
                     └────────┬───────┘
                              ▼
              ┌────────────────────────────────────┐
              │ Aggregation / Analysis            │
              │  - Stack hashing dedup            │
              │  - Trend / first / repeat crash   │
              │  - Affected devices / versions    │
              └─────────┬──────────────────────────┘
                        ▼
                  ┌───────────────┐
                  │  Dashboard     │
                  │ / Alarm system │
                  └───────────────┘

4.1 上报字段

每条 crash 至少带:

{
  "crash_id": "uuid",
  "timestamp": 1234567890,
  "app_version": "1.0.0",
  "app_build": 12345,
  "device": "Xiaomi 14",
  "os_version": "Android 14",
  "stacktrace": "...",
  "thread_state": { "name": "main", "priority": 5 },
  "user_id": "...",
  "extra": { "channel": "huawei", "page": "feed" }
}

4.2 Crash 哈希去重

把 stacktrace 头部 5 行做 SHA1,得到聚合 key:

fun hashStack(stack: String): String {
    val lines = stack.lines().take(5)
    val digest = MessageDigest.getInstance("SHA-1")
    lines.forEach { digest.update(it.toByteArray()) }
    return digest.digest().joinToString("") { "%02x".format(it) }
}

这样同一个根因的 crash 会聚合到一起。

4.3 离线/网络异常处理

Crash 上报要走 fallback:

class CrashReporter(private val api: CrashApi) {
    private val cacheFile = File(context.cacheDir, "crash_queue.jsonl")

    fun init() {
        Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
            saveCrashToCache(thread, throwable)
            flushCache()
        }
        // 同时启动定时 flush
        scheduler.scheduleAtFixedRate(::flushCache, 0, 30, TimeUnit.SECONDS)
    }

    private fun flushCache() {
        if (!isNetworkAvailable()) return
        cacheFile.useLines { lines ->
            lines.forEach { line ->
                runCatching { api.upload(line) }
            }
        }
        cacheFile.writeText("")
    }
}

五、Bugly 控制台实战

5.1 必看的几个视图

视图

用法

崩溃列表

看 top crash by crash count / 设备数

版本对比

看新版本崩溃率变化

影响用户

唯一用户数(影响用户/DAU 比例)

崩溃轨迹

用户操作回放

5.2 关键指标

指标

健康值

Crash count / 1 万 DAU

< 10

Crash count / 1 万启动

< 5

影响用户比例(影响用户/DAU)

< 1%

重复 crash 比例

< 20%

一个 100 万 DAU App,每天 Crash 应控制在 1000 次以内。

5.3 Issue 创建流程

  1. Crash 阈值报警触发

  2. 从控制台拉 stacktrace、版本、设备分布

  3. 查 git blame,找到具体代码行

  4. 创建工单,owner 分配

  5. 修复 + 加测试用例

  6. 灰度发布,验证 Crash 量下降

六、稳定性专项治理

6.1 Crash Free Sessions

Crashlytics 等 APM 平台的指标:

CrashFreeSessions = 1 - (CrashSessions / TotalSessions)

CrashFree

评级

>= 99.95%

A+

99.5% - 99.95%

A

95% - 99.5%

B

< 95%

C

6.2 Top Crash 治理流程

每周末做一次 top crash 复盘:

步骤

动作

1

拉 top 10 crash(去重后)

2

计算 crash count、影响设备、影响版本

3

决策:当天修 / 下个版本修 / 不修

4

修复后跟踪"修复率"(下一周 crash 数 < 上周)

6.3 黄金信号与 SLO

可用性 SLO:
- Crash Free Sessions: 99.95%
- ANR Free Sessions: 99.9%
- 启动成功率: 99.5%

告警阈值:
- 任意 crash count +50%(vs 上周均值) → 触发报警
- Crash Free 跌到 99.5% 以下 → 触发报警

七、Crash 防御工程

7.1 全局 try-catch

class SafeExecutor {
    companion object {
        fun <T> safely(block: () -> T): Result<T> = runCatching(block)
            .onFailure { crashReporter.report(it, level = NON_FATAL) }
    }
}

这样非关键路径的崩溃会被捕获,转为非致命错误上报。

7.2 StrictMode 检测

if (BuildConfig.DEBUG) {
    StrictMode.setThreadPolicy(
        StrictMode.ThreadPolicy.Builder()
            .detectAll()
            .penaltyLog()
            .build()
    )
}

StrictMode 会在 debug 包里捕获主线程 IO、网络、磁盘操作,提前预警。

7.3 Crash 测试

写一个"Crash 测试用例":

@Test
fun testCrashReported() {
    // 模拟崩溃场景,确保被捕获
    assertThrows(RuntimeException::class.java) {
        throw RuntimeException("test")
    }
}

CI 里跑这用例,确保 crash handler 链路有效。

八、Native Crash 定位

JNI 崩溃比 Java 崩溃难定位。流程:

# 1. 用 addr2line 反向解析
addr2line -e libnative.so -f <address>

# 2. 用 ndk-stack 工具
ndk-stack --sym ./app/src/main/obj/local/arm64-v8a \
          --dump crash.dump

# 3. 用 AS Memory Profiler 看 native heap

如果用了第三方 so,记得开 Bugly native 上报:

implementation("com.tencent.bugly:nativecrashreport:4.0.3")

九、稳定性工程的"长期指标"建设

指标

数据源

频率

Crash Free Sessions

Bugly / 自建

实时

ANR Free Sessions

Bugly / 自建

实时

用户差评率

应用市场评论

每周

用户投诉稳定性

客服工单

每周

重启率

自定义 telemetry

实时

OTA 成功率

应用市场后台

每次发版

每季度对稳定性指标做一次复盘。

十、从"事后修"到"事前防"

阶段

行动

立项

设定 Crash Free 阈值

设计

关键流程走风险评估

编码

Lint + Code Review + StrictMode

测试

Crash 测试 + Monkey

灰度

监控关键指标

发布

收集真实数据

复盘

Crash 专题分析

SRE 思路引入移动端稳定性工程,多能把这套做成日常运转。

小结

稳定性工程不止是"出了 crash 修一下"。它包括事前防御(Lint/StrictMode)、事中捕获(Bughly/APM)、事后分析(聚合/告警)、长期建设(SLO/复盘)。一个团队至少要有 1 位 engineer 把 20% 的工作时间投入到稳定性工程里,否则 Crash 会越来越难收场。

下一篇聊 Android 启动框架——App 冷启动链路里能优化的所有点,以及"业务 1 秒启动"是怎么做到的。

参考阅读

  • Tencent Bugly 官方文档

  • Firebase Crashlytics 集成指南

  • SRE 系列:Google SRE Workbook


评论