06 从 Trace 看 Android 启动流程

应用启动横跨 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_typewarm。旁边还有厂商轨道 launching: com.example...,起点是 1657.844 ms。两个起点不同,说明它们采用了不同的事件边界,分析时不能混用。

冷、温、热启动的区分也不能只看某个生命周期方法:

类型 启动请求到达时的状态 Trace 中常见证据
Cold 目标进程不存在 启动范围内出现进程创建、PostForkZygoteInitActivityThreadMainbindApplication
Warm 进程存在,Activity 需要重新创建 没有新的进程创建;主线程执行 activityStartperformCreateperformStartactivityResume
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 后的运行时整理,随后是 ZygoteInitActivityThreadMain。主线程进入 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 启动,案例中 ProcessLifecycleInitializerProfileInstallerInitializerEmojiCompatInitializer 都位于 app.onCreate 之前。Application 回调很短时,启动成本仍可能藏在 Provider 和三方库自动初始化里。

app.onCreatebindApplication 的 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.onCreateLoadSimulator_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_ActivityInitperformCreate 的 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 记录不代表没有首帧;doFrameDrawFramesqueueBufferreportDrawFinished 和 SurfaceFlinger 的 Layer 事件仍能组成证据链。系统版本、采集项和首帧 token 都会影响 FrameTimeline 的可见性。

App 报告绘制完成后,Buffer 还要被 SurfaceFlinger 接收和合成。案例中,Framework 的 completed-warm 标记出现在 2983.847 ms;SurfaceFlinger 于 2986.828 ms 看到 MainActivity Layer 的新内容,随后执行 composite 1411776present 从 2987.698 ms 开始。几个边界相隔只有数毫秒,含义并不相同。

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

启动慢怎样从 Trace 定位

启动 Slice 先用来限定范围,实际的优化点要按阶段判断。

进程创建前后很长。 对齐 Launcher、system_server、Start procPostForkActivityThreadMain。若请求已经发出而进程迟迟没有运行,继续看 system_server 处理、Zygote、CPU 调度和系统负载;若进程由预启动产生,还要把预启动时间与正式启动指标分开。

bindApplication 很长。 进入 APK/Dex、资源、Provider、makeApplicationApplication.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 初始化,再评估首帧渲染。

采集时保留哪些数据

启动分析至少需要 amwmviewgfxbinder_driversched。要追到首帧显示,还应保留 FrameTimeline、频率、GPU、BufferQueue 与 SurfaceFlinger 相关数据。应用自己的初始化阶段可以增加少量自定义 Trace,但名称要覆盖真实作用域;标记只包住一小段回调时,不能拿它代替整个 Application.onCreateActivity.onCreate

一份可复核的启动结论应包含四项:启动边界与类型、最长的墙上时间、该线程在区间内的状态、主导耗时的子 Slice 或跨进程调用。只给"Application 慢""主线程忙"这类判断,无法说明时间从哪里来,也无法验证优化是否命中。

参考资料

相关推荐
数据库小学妹3 小时前
空间数据库查询慢怎么排查?索引失效、表膨胀、SQL优化实战(附排查命令)
数据库·性能优化·信创·故障排查·索引调优·空间数据库
爱喝水的鱼丶14 小时前
SAP-ABAP:接口与报表场景专项优化:RFC/ODATA 接口、ALV 报表的性能提升方案
运维·性能优化·接口·sap·abap·rfc·经验交流
灯澜忆梦1 天前
【MySQL11】进阶篇 | 索引_#3使用规则
数据库·sql·mysql·性能优化
starzhang1 天前
05 从 Perfetto 看懂 Binder 调用链
性能优化
大龄秃头程序员1 天前
一次关于 LRUCache 的工程化落地:从数据结构到 Feed 图片缓存实践
性能优化
jeffwang1 天前
我把同一个压测做错了三次,第四次才发现真正的瓶颈
分布式·性能优化·rust
番茄炒鸡蛋加糖1 天前
专项2:项目性能优化&可量化指标
性能优化
DsirNg1 天前
中级前端开发知识体系:从“会做页面”到“能独立交付中型项目”
javascript·性能优化·typescript·工程化·浏览器原理·web 安全·前端进阶
灯澜忆梦2 天前
【MySQL10】进阶篇 | 索引_#2性能优化
数据库·sql·mysql·性能优化