应用启动横跨 Launcher、system_server、Zygote、App 主线程、RenderThread 和 SurfaceFlinger。代码里的 startActivity 只负责发起请求,第一帧出现在屏幕前,还要经历进程准备、Application 初始化、Activity 创建、窗口建立、View 绘制、Buffer 提交和系统合成。
Perfetto 把这些动作放在同一条时间轴上。启动分析因此有一条明确的证据链:先确认启动请求的边界和类型,再跟进进程与主线程,最后沿首帧提交进入 SurfaceFlinger。每一步都能由 Slice、线程状态或跨进程事件验证。

图中的案例有意保留了一个真实系统变量:桌面在用户启动应用前调用了 preStartProcess。应用进程提前创建,正式启动被 Perfetto 判为 warm start。仅凭附近出现了 PostFork 就认定冷启动,会得到错误结论。
先确定启动边界
Perfetto 顶部的 Android App Startups 是 Trace Processor 根据系统事件生成的派生轨道。案例中的启动 Slice 从 1645.183 ms 开始,持续 1338.654 ms,startup_type 为 warm。旁边还有厂商轨道 launching: com.example...,起点是 1657.844 ms。两个起点不同,说明它们采用了不同的事件边界,分析时不能混用。
冷、温、热启动的区分也不能只看某个生命周期方法:
| 类型 | 启动请求到达时的状态 | Trace 中常见证据 |
|---|---|---|
| Cold | 目标进程不存在 | 启动范围内出现进程创建、PostFork、ZygoteInit、ActivityThreadMain 和 bindApplication |
| Warm | 进程存在,Activity 需要重新创建 | 没有新的进程创建;主线程执行 activityStart、performCreate、performStart、activityResume |
| Hot | 进程和 Activity 都保留 | 主要是 resume、窗口可见性、焦点和首帧调度,通常不再执行 performCreate |
案例看起来同时具备 cold 与 warm 的特征。把时间点排好后,矛盾便消失了:
| 时间 | 进程 / 线程 | 事件 |
|---|---|---|
| 1530.804 ms | Launcher | Launcher preStartProcess |
| 1533.681 ms | system_server | Start proc: com.example... |
| 1542.940 ms | App main | PostFork |
| 1543.226 ms | App main | ZygoteInit |
| 1544.083 ms | App main | ActivityThreadMain |
| 1547.408 ms | App main | bindApplication 开始 |
| 1645.183 ms | system_server | Android App Startups 开始,类型为 warm |
| 1656.082 ms | App Binder | scheduleTransaction 到达 |
桌面提前 114.379 ms 请求创建进程;等到系统记录正式启动时,进程已经存在,因此分类为 warm。PostFork 证明进程在这段 Trace 中刚创建,无法单独证明进程创建属于这次 warm-start 指标的计时范围。

这类预启动还有一个性能含义。进程提前创建不等于初始化已经完成。启动事务在 1656.082 ms 到达 App Binder 线程时,主线程仍位于 Application.onCreate;直到 2124.303 ms 才开始 activityStart,中间相隔约 468.221 ms。预启动把部分工作移到了指标起点之前,但没有消除主线程上的工作。
从进程创建进入 bindApplication
PostFork 表示子进程完成 fork 后的运行时整理,随后是 ZygoteInit 和 ActivityThreadMain。主线程进入 ActivityThreadMain 后建立 Looper、ActivityThread 等运行环境,system_server 再经 IApplicationThread.bindApplication 把应用信息送入新进程。Binder 请求先落到 App 的 Binder 线程,实际的 bindApplication 工作由主线程执行。

案例中的 bindApplication 持续 575.869 ms。展开后可以看到几个彼此有先后关系的阶段:
| 时间 | Slice | 耗时 | 对应工作 |
|---|---|---|---|
| 1548.898 ms | base.apk |
1.007 ms | 建立应用代码与资源路径 |
| 1548.921 ms | OpenDexFilesFromOat |
0.724 ms | 打开 odex / vdex / dex 与 App Image |
| 1550.130 ms | ResourcesManager#getResources |
0.430 ms | 创建应用资源对象 |
| 1551.507 ms | makeApplication |
0.253 ms | 实例化 Application |
| 1552.256 ms | Startup |
1.401 ms | AndroidX Startup Provider 初始化 |
| 1554.167 ms | app.onCreate |
568.800 ms | Application 回调及应用初始化 |
顺序里有一个经常被忽略的点:ContentProvider 早于 Application.onCreate。AndroidX Startup 依靠 InitializationProvider 启动,案例中 ProcessLifecycleInitializer、ProfileInstallerInitializer、EmojiCompatInitializer 都位于 app.onCreate 之前。Application 回调很短时,启动成本仍可能藏在 Provider 和三方库自动初始化里。
app.onCreate 占 bindApplication 的 98.77%,主因已经很集中。不过 568.800 ms 是墙上时间,不代表主线程一直占用 CPU。对应的线程状态为:Running 297.172 ms、Sleeping 254.992 ms、不可中断睡眠 D 14.260 ms、Runnable 2.376 ms。

这组数据把后续方向分开了。Running 段要继续找计算、类加载和对象创建;Sleeping 段要对齐锁、条件等待、同步 Binder 或任务同步;D 状态常见于内核不可中断等待,仍需结合 blocked function 和 I/O 事件确认。把 568.800 ms 全部归为"主线程计算太重",会漏掉约一半的等待时间。
案例里的 LoadSimulator_AppInit 持续 568.602 ms,几乎覆盖整个 Application 回调,里面又包含 SQLite、I/O、计算和任务等待。父子 Slice 有嵌套关系,不能把 app.onCreate、LoadSimulator_AppInit 和内部 ChaosTask 的时长直接相加。最外层 Slice 用于确定总墙上时间,子 Slice 用于解释这段时间花在哪里。
Activity 事务为什么晚了 468 ms
system_server 在 1656.082 ms 已经调用 App 的 IApplicationThread.scheduleTransaction。这笔 Binder 调用很短,说明事务已经送达 App 进程;主线程仍被 bindApplication 占用,消息只能留在主线程队列里。
bindApplication 在约 2123.276 ms 结束,主线程随后处理 clientTransactionExecuted,2124.303 ms 进入 activityStart。因此,启动链上的这段等待不属于 Binder 传输慢,也不属于 Activity.onCreate 慢;它来自更早的 Application 初始化占住主线程。

Activity 阶段的关键时间如下:
| Slice | 开始时间 | 耗时 |
|---|---|---|
activityStart |
2124.303 ms | 832.190 ms |
performCreate:...MainActivity |
2128.458 ms | 827.614 ms |
RealInflation |
2131.156 ms | 104.325 ms |
LoadSimulator_ActivityInit |
2235.486 ms | 710.060 ms |
performStart |
2956.522 ms | 5.377 ms |
activityResume |
2961.960 ms | 6.795 ms |
LoadSimulator_ActivityInit 占 performCreate 的 85.80%,Activity 创建慢主要由业务初始化造成。RealInflation 的 104.325 ms 则覆盖布局加载的一部分。继续优化时应把两段分开:布局与 View 构建属于 UI 初始化,710.060 ms 的模拟任务属于业务工作;迁移、延迟或拆分的手段并不相同。
performCreate 内的线程状态也能防止误判:Running 563.086 ms、Sleeping 251.329 ms、D 11.188 ms、Runnable 2.012 ms。主线程既有大量 CPU 工作,也存在约 262.5 ms 的等待。只做异步化可能减少等待,未必能消除 563 ms 的主线程计算;只优化算法,也不会自动解决同步等待。
第一帧怎样从主线程走到屏幕
activityResume 在 2968.755 ms 左右结束,第一帧紧接着开始:
| 时间 | 线程 | Trace 证据 |
|---|---|---|
| 2968.790 ms | App main | Choreographer#doFrame -1 |
| 2968.938 ms | App main | traversal |
| 2970.803 ms | App main | relayoutWindow,首次创建窗口 Surface |
| 2975.684 ms | App main | draw-VRI[MainActivity] |
| 2976.170 ms | RenderThread | DrawFrames -1 |
| 2976.285 ms | RenderThread | dequeueBuffer |
| 2979.789 ms | RenderThread | queueBuffer |
| 2981.102 ms | App main | reportDrawFinished |

relayoutWindow 让 ViewRootImpl 从 WindowManagerService 获得窗口和 Surface;主线程在 draw 中记录 DisplayList,syncAndDrawFrame 把渲染任务交给 RenderThread。RenderThread 通过 dequeueBuffer 取得可写 Buffer,执行 HWUI/Vulkan 渲染,再由 queueBuffer 把完成的 Buffer 交给 BufferQueue。reportDrawFinished 通知 system_server 首次绘制完成。
这一帧在 App 的 FrameTimeline 轨道中没有对应的 Expected / Actual Slice,应用 FrameTimeline 从更晚的帧才开始出现。缺少 FrameTimeline 记录不代表没有首帧;doFrame、DrawFrames、queueBuffer、reportDrawFinished 和 SurfaceFlinger 的 Layer 事件仍能组成证据链。系统版本、采集项和首帧 token 都会影响 FrameTimeline 的可见性。
App 报告绘制完成后,Buffer 还要被 SurfaceFlinger 接收和合成。案例中,Framework 的 completed-warm 标记出现在 2983.847 ms;SurfaceFlinger 于 2986.828 ms 看到 MainActivity Layer 的新内容,随后执行 composite 1411776,present 从 2987.698 ms 开始。几个边界相隔只有数毫秒,含义并不相同。

Android App Startups 的结束位置接近 TTID 边界,用来衡量初始内容何时完成首绘。它不等于应用已经可交互。网络数据、异步初始化、首屏占位替换等工作可能继续发生;这部分要靠 reportFullyDrawn 报告 TTFD。案例中没有实际的 reportFullyDrawn 调用记录,因此 Trace 只能确认首绘链路,不能给出"页面已经完全可用"的时间。
启动慢怎样从 Trace 定位
启动 Slice 先用来限定范围,实际的优化点要按阶段判断。

进程创建前后很长。 对齐 Launcher、system_server、Start proc、PostFork 和 ActivityThreadMain。若请求已经发出而进程迟迟没有运行,继续看 system_server 处理、Zygote、CPU 调度和系统负载;若进程由预启动产生,还要把预启动时间与正式启动指标分开。
bindApplication 很长。 进入 APK/Dex、资源、Provider、makeApplication 和 Application.onCreate。同时看 Thread State,区分 CPU、调度、锁、I/O 与同步调用。Provider 的顺序早于 Application,排查三方初始化时不能只搜 onCreate。
performCreate 很长。 展开 inflate、Compose 初始组合、Fragment、ViewModel、业务初始化和同步 Binder。父 Slice 确定总耗时,子 Slice 解释归属;同一层的兄弟 Slice 才适合比较和求和。
Activity 很快,首帧仍晚。 从 Choreographer#doFrame 进入 traversal,再沿 RenderThread、BufferQueue 和 SurfaceFlinger 检查。主线程没有长 Slice时,RenderThread 的 Runnable 延迟、GPU、Buffer 等待与 SurfaceFlinger 合成都可能延后显示。
startup 指标不长,用户仍觉得慢。 检查 TTID 之后的异步加载和 reportFullyDrawn。启动窗口消失、第一帧显示、主要内容完成、页面可交互是不同时间点,只有 TTID 时无法评价后两项。
这份 Trace 最终得到三条可执行结论:正式 warm start 为 1338.654 ms;主线程在启动请求到达后先等待 Application 初始化收尾,约损失 468.221 ms;Activity 创建中的业务初始化又占用 710.060 ms。第一帧自身从 doFrame 到 queueBuffer 约 10.999 ms,没有成为主要矛盾。优化顺序应先处理 Application 和 Activity 初始化,再评估首帧渲染。
采集时保留哪些数据
启动分析至少需要 am、wm、view、gfx、binder_driver 和 sched。要追到首帧显示,还应保留 FrameTimeline、频率、GPU、BufferQueue 与 SurfaceFlinger 相关数据。应用自己的初始化阶段可以增加少量自定义 Trace,但名称要覆盖真实作用域;标记只包住一小段回调时,不能拿它代替整个 Application.onCreate 或 Activity.onCreate。
一份可复核的启动结论应包含四项:启动边界与类型、最长的墙上时间、该线程在区间内的状态、主导耗时的子 Slice 或跨进程调用。只给"Application 慢""主线程忙"这类判断,无法说明时间从哪里来,也无法验证优化是否命中。