第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 中主线程卡在
• 若卡在
• 若卡在
• 若 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 的根因分类,以及每个阶段对应的典型阻塞源。图中深浅节点分别表示不同阶段,红色标注高风险操作。
故障树解读方法:
- 从根节点“启动 ANR”出发,向下判断主线程卡在哪个阶段(P1–P4)。
- 每个阶段下方的红色矩形列出该阶段最常见的阻塞源。若 trace 堆栈匹配其中任意一项,则直接定位为该原因。
- 若 trace 堆栈未命中红色区域,则需进一步查看该阶段内的其他同步操作(如反射、序列化)。
3.5 修复动作 – 分阶段解决方案
| 阶段 | 典型阻塞 | 修复动作 | 代码示例(示意) |
|---|---|---|---|
| P1 | 第三方 SDK 同步初始化 | 使用 StartupProvider 延迟初始化或异步 init |
|
| P2 | XML 布局解析耗时 | 迁移到 Compose 布局,或使用 AsyncLayoutInflater |
|
| P3 | 首帧中发起 HTTP 请求 | 将网络请求移出 Compose 函数,使用 LaunchedEffect 或 ViewModel 异步加载 |
|
| P4 | Recompose 次数过多 | 使用 remember、derivedStateOf 减少重组范围 |
|
最佳实践:若无法立即消除某个阻塞(如必须同步读取加密文件),可将其放入
StrictMode 白名单并记录到后台,但必须在 reportFullyDrawn() 之前完成。同时将此阶段纳入后续监控。
3.6 复测验证 – 确认阶段耗时回归
修复后使用同一设备与网络环境,再次采集各阶段耗时:
- 运行
adb shell am start -W packagename/.MainActivity获取TotalTime和WaitTime。 - 结合 Perfetto 抓取
Trace.beginSection标记,对比修复前后的四个阶段耗时。 - 若某阶段耗时降低 50% 以上且总启动时间 < 1.5s,视为修复有效。
# 复测命令示例
adb shell am start -W -n com.example.app/.MainActivity
输出中 ThisTime 即为本次 Activity 启动总耗时。若该值 < 2000ms 且无 ANR 弹窗,即可通过。
3.7 预防方案 – 上线前自动化检查清单
- CI 流水线集成: 在单元测试/UI 测试中,使用
TraceAPI 断言每个阶段耗时不超过阈值(如 P1 < 30ms, P2 < 100ms, P3+P4 < 400ms)。 - 代码审查规则: 禁止在
Application.onCreate中调用Thread.sleep()、BitmapFactory.decodeStream、Class.forName等阻塞 API。 - 运行时监控: 使用
FrameMetrics或Choreographer检测首帧渲染耗时,超过 16ms 自动上报。 - 灰度验证: 每次大版本在灰度期间开启
StrictMode磁盘/网络检查,捕获启动阶段的违规操作。