第3章 启动阶段拆分:Application.onCreate、Activity.onCreate、Compose首帧、第一帧渲染

Compose 启动 ANR 并非瞬间发生,而是累积在四个关键阶段中。只有将启动流程精确拆解,才能定位 ANR 到底卡在哪个环节。本章建立阶段拆分模型,并给出每阶段的常见 ANR 诱因、数据采集方法和排查规则。

3.1 阶段定义与临界时间

阶段编号 阶段名称 时间窗口参考(高端设备) ANR 风险特征
P1 Application.onCreate 0 – 30ms 同步初始化第三方 SDK、阻塞式文件读写
P2 Activity.onCreate 30 – 120ms setContent 前的重量级布局解析、数据库查询
P3 Compose 首帧 120 – 300ms 首帧 Composable 函数中执行网络请求、复杂计算
P4 第一帧渲染 300 – 500ms Recompose 次数过多、Layout 阶段测量嵌套过深
临界阈值说明:若任一步骤超过其窗口上限 2 倍(如 P1 超过 60ms),即表明该阶段存在明显同步阻塞。但 ANR 往往是多阶段累积导致总启动时间 > 5s,需合并分析。

3.2 现象识别 – 从日志与 Trace 中定位超阶段

当用户反馈“点击图标后长时间白屏”或“系统弹窗 XX 无响应”,首选采集 ANR trace 与 系统启动日志。

  • ANR trace 关键字: 查找 main 线程堆栈中 Application.onCreate、ActivityThread.handleLaunchActivity、ComposeContent、AndroidComposeView.onMeasure 等调用点。
  • 日志过滤规则: 搜索 Displayed 与 WaitForBackgroundThread 时间戳差,若 > 4s 则判定为启动 ANR。
快速判断规则:
• 若 ANR trace 中主线程卡在 ContextImpl.bindService 或 FileInputStream.read → 问题在 P1 Application.onCreate。
• 若卡在 LayoutInflater.inflate 或 SQLiteDatabase.query → 问题在 P2 Activity.onCreate。
• 若卡在 ComposeNode.measure 或 Composer.recompose → 问题在 P3 / P4。

3.3 数据采集 – 各阶段精确耗时测量

使用 Trace.begin/endSection 或 SimpleProfiler 手动埋点。推荐方案:

// Application.onCreate 埋点示例
class MyApp : Application() {
    override fun onCreate() {
        Trace.beginSection("P1_ApplicationOnCreate")
        super.onCreate()
        // 初始化操作...
        Trace.endSection()
    }
}

// Activity.onCreate 中 Compose 首帧标记
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        Trace.beginSection("P2_ActivityOnCreate")
        super.onCreate(savedInstanceState)
        setContent {
            Trace.beginSection("P3_ComposeFirstFrame")
            // 首帧 Composable
            Trace.endSection() // P3
        }
        Trace.endSection() // P2
    }
}
采集方式 精度 是否侵入代码 推荐场景
Android Studio Profiler / Perfetto 微秒级 无需埋点(系统级) 线上复现、线下分析
手动 Trace API 微秒级 需修改代码 精确区分 P1/P2/P3/P4 边界
Looper 日志 + 自定义 Printer 毫秒级 轻量注入 用户侧采集堆栈耗时

3.4 原因定位 – 故障树与排查表

下图以故障树形式展示启动 ANR 的根因分类,以及每个阶段对应的典型阻塞源。图中深浅节点分别表示不同阶段,红色标注高风险操作。

Compose启动ANR阶段拆分故障树 启动 ANR P1 Application.onCreate P2 Activity.onCreate P3 Compose首帧 P4 第一帧渲染 SDK 初始化 文件读/写 布局 XML 解析 数据库查询 首帧网络请求 图片解码 Recompose 过多 嵌套测量 定位原则:根据 ANR trace 中主线程当前堆栈落在上述哪个红色区域,即锁定对应阶段 若同时存在 P3 和 P4 的堆栈,优先解决 P3(首帧逻辑阻塞)

故障树解读方法:

  • 从根节点“启动 ANR”出发,向下判断主线程卡在哪个阶段(P1–P4)。
  • 每个阶段下方的红色矩形列出该阶段最常见的阻塞源。若 trace 堆栈匹配其中任意一项,则直接定位为该原因。
  • 若 trace 堆栈未命中红色区域,则需进一步查看该阶段内的其他同步操作(如反射、序列化)。

3.5 修复动作 – 分阶段解决方案

阶段 典型阻塞 修复动作 代码示例(示意)
P1 第三方 SDK 同步初始化 使用 StartupProvider 延迟初始化或异步 init
// 改为晚点到首次使用时 init
class SdkLazyInit {
    private var initialized = false
    fun ensureInit() {
        if (!initialized) {
            initialized = true
            // 真正初始化
        }
    }
}
P2 XML 布局解析耗时 迁移到 Compose 布局,或使用 AsyncLayoutInflater
// Compose 布局无需 XML 解析
setContent {
    // 直接使用 Composable
}
P3 首帧中发起 HTTP 请求 将网络请求移出 Compose 函数,使用 LaunchedEffect 或 ViewModel 异步加载
@Composable
fun MyScreen(viewModel: MyViewModel) {
    LaunchedEffect(Unit) {
        viewModel.loadData() // 异步
    }
}
P4 Recompose 次数过多 使用 remember、derivedStateOf 减少重组范围
val filteredList = remember(list) {
    list.filter { ... }
}
最佳实践:若无法立即消除某个阻塞(如必须同步读取加密文件),可将其放入 StrictMode 白名单并记录到后台,但必须在 reportFullyDrawn() 之前完成。同时将此阶段纳入后续监控。

3.6 复测验证 – 确认阶段耗时回归

修复后使用同一设备与网络环境,再次采集各阶段耗时:

  1. 运行 adb shell am start -W packagename/.MainActivity 获取 TotalTime 和 WaitTime。
  2. 结合 Perfetto 抓取 Trace.beginSection 标记,对比修复前后的四个阶段耗时。
  3. 若某阶段耗时降低 50% 以上且总启动时间 < 1.5s,视为修复有效。
# 复测命令示例
adb shell am start -W -n com.example.app/.MainActivity

输出中 ThisTime 即为本次 Activity 启动总耗时。若该值 < 2000ms 且无 ANR 弹窗,即可通过。

3.7 预防方案 – 上线前自动化检查清单

  • CI 流水线集成: 在单元测试/UI 测试中,使用 Trace API 断言每个阶段耗时不超过阈值(如 P1 < 30ms, P2 < 100ms, P3+P4 < 400ms)。
  • 代码审查规则: 禁止在 Application.onCreate 中调用 Thread.sleep()、BitmapFactory.decodeStream、Class.forName 等阻塞 API。
  • 运行时监控: 使用 FrameMetrics 或 Choreographer 检测首帧渲染耗时,超过 16ms 自动上报。
  • 灰度验证: 每次大版本在灰度期间开启 StrictMode 磁盘/网络检查,捕获启动阶段的违规操作。