Android 启动优化(三):Perfetto 实战——启动时间到底花在哪里?

序、众所周知,每一次开始写这种框架系类的文章的时候,就是我在找工作了,有闲暇时间可以写点东西。哈哈哈哈,26年找工作,简直就是。。。。。

重要的事情说三遍

有岗位的请 CALL 我,谢谢大家了。。。。

有岗位的请 CALL 我,谢谢大家了。。。。

有岗位的请 CALL 我,谢谢大家了。。。。

上一篇我们已经用 Macrobenchmark 建立了启动性能基线。

当前测试结果:

  • min:324.7ms

  • median:340.2ms

  • max:387.0ms

也就是说,我们现在已经知道:

App 冷启动大约需要 340ms。

但这个数字只能告诉我们:

启动有多快。

它并不能告诉我们:

这 340ms 到底花在哪里?

所以启动优化的下一步,就是分析启动过程。

这时候就需要:

Perfetto


一、Macrobenchmark 和 Perfetto 分别解决什么问题?

这两个工具很容易混。

可以简单记成:

Macrobenchmark:测量。

回答:

启动用了多长时间?

例如:

TTID = 340ms

而:

Perfetto:分析。

回答:

这 340ms 为什么花掉了?

比如:

  • Application 初始化太慢

  • Activity.onCreate 太重

  • 首屏 Layout 太复杂

  • 启动阶段做了大量 IO

  • 主线程等待 Binder

  • 线程创建太多导致 CPU 竞争

  • 启动阶段发生 GC

所以完整流程应该是:

Macrobenchmark

发现启动耗时

Perfetto

定位具体瓶颈

再决定怎么优化。


二、什么是 Perfetto?

Perfetto 是 Android 现在非常重要的一套系统性能分析工具。

它可以记录系统运行过程中的大量信息,比如:

  • CPU 调度

  • 线程状态

  • Binder

  • IO

  • 内存

  • GC

  • Frame

  • Choreographer

  • RenderThread

  • Activity 生命周期

  • 系统服务

它最大的价值是:

把 Android 系统运行过程放在一条时间线上展示出来。

也就是说,我们不再只是看到:

Application.onCreate = 50ms

而是可以看到:

这 50ms 中:

  • 真正在 CPU 执行了多久

  • 等待了多久

  • 有没有被调度出去

  • 有没有发生 IO

  • 有没有等待锁

  • 有没有 Binder 调用

这也是 Perfetto 比简单 Log 打点更有价值的地方。


三、Macrobenchmark 为什么会自动产生 Trace?

我们之前运行 Macrobenchmark 时:

测试了 5 次启动。

因此最终会看到:

  • Iteration 0

  • Iteration 1

  • Iteration 2

  • Iteration 3

  • Iteration 4

每一次启动都可以对应一条 System Trace。

打开之后,就可以看到:

这一次真实启动过程中,CPU、线程、Activity、渲染到底发生了什么。

所以 Macrobenchmark 和 Perfetto 天然可以组成一套启动优化工具链:

Macrobenchmark 负责测。

Perfetto 负责分析。


四、打开 Trace 后,第一件事看什么?

第一次打开 Perfetto,最容易出现的问题就是:

信息太多了,不知道从哪里看。

里面可能有几十甚至上百条线程:

  • main

  • RenderThread

  • Binder

  • FinalizerDaemon

  • HeapTaskDaemon

  • ReferenceQueueDaemon

  • Profile Saver

  • 系统线程

  • GPU 线程

实际上做启动优化时,不需要一开始全部看。

第一原则:

先找到自己的 App 主线程。

也就是:

Main Thread。

绝大多数 App 启动问题,首先都应该从主线程开始分析。


五、为什么 Main Thread 最重要?

Android 的很多启动工作都发生在主线程。

例如:

  • Application.onCreate

  • ContentProvider 初始化

  • Activity 生命周期

  • View Inflate

  • Measure

  • Layout

  • Draw

  • 部分 Binder 调用

  • 部分文件读取

  • SDK 初始化

如果这些工作耗时过长:

首帧就无法及时显示。

所以启动优化初期,可以先记住一句话:

先查主线程做了什么。

而不是一上来研究 RenderThread 或 GPU。


六、先确定启动区间

找到主线程以后,不要把整个 Trace 都看一遍。

我们只关注:

启动这一段。

一次典型冷启动,大致可以看到:

进程创建

bindApplication

Application

Activity 创建

Choreographer

traversal

首帧

这和我们第一篇讲过的 Android 冷启动流程其实是对应的。

所以理解启动原理之后,再看 Perfetto 就会简单很多。


七、bindApplication 为什么值得重点看?

启动过程中经常可以看到:

bindApplication

它通常对应:

ActivityThread.handleBindApplication()

附近的应用初始化流程。

这个阶段可能包含:

  • 创建 Application

  • ContentProvider 初始化

  • 加载资源

  • ClassLoader 相关工作

  • Application.attach

  • Application.onCreate

  • SDK 初始化

如果发现:

bindApplication

这一整段特别长,

就说明:

问题很可能发生在应用初始化阶段。

这时候就继续向内部展开。


八、Application.onCreate 应该怎么看?

很多 App 的启动性能问题都集中在:

Application.onCreate()

例如里面同时做:

  • 数据库初始化

  • 图片库初始化

  • 网络初始化

  • 埋点初始化

  • 推送初始化

  • 地图 SDK 初始化

  • SharedPreferences 读取

如果 Application.onCreate 占用了几十甚至上百毫秒,

那么它就是一个非常明确的优化目标。

但是需要注意:

Application.onCreate 快,不代表整个应用初始化就一定快。

因为:

ContentProvider 可能比它更早初始化。

所以 Perfetto 中还需要关注 Application 前面的耗时。


九、Activity 阶段看什么?

Application 初始化完成后,就开始进入首页 Activity。

重点关注:

Activity.onCreate()

以及:

setContentView()

附近。

这里常见的问题包括:

  • Activity.onCreate 业务逻辑过多

  • XML Inflate 很慢

  • 首页 Fragment 太多

  • 初始化大量 View

  • 同步读取数据库

  • 同步加载配置

  • 首屏计算量太大

如果启动前半段很快,

但进入 Activity 之后突然出现一个很宽的耗时块,

那么大概率就是:

首页本身太重。


十、不要只看一个 Trace 块"很宽"

这是 Perfetto 非常重要的一点。

假设看到:

Choreographer

Wall Duration:

132ms

很多人可能马上得出结论:

Choreographer 执行了 132ms。

其实不一定。

因为:

Wall Duration

和:

CPU Duration

不是一个概念。


十一、什么是 Wall Duration?

Wall Duration 可以理解为:

从这段任务开始,到这段任务结束,现实世界一共过去了多久。

例如:

一个任务:

10:00:00 开始。

10:00:00.100 结束。

那么:

Wall Duration = 100ms。

但这 100ms 里面,线程不一定一直在 CPU 上执行。

它可能:

  • 等 CPU

  • 等锁

  • 等 Binder

  • Sleeping

  • Waiting

  • 被系统调度出去

所以:

Wall 时间长,不代表代码真的执行了这么久。


十二、什么是 CPU Duration?

CPU Duration 表示:

这段时间里,线程真正占用 CPU 执行了多久。

例如:

Wall Duration:

100ms

CPU Duration:

90ms

说明:

这段任务基本一直在 CPU 上运行。

此时问题可能是:

  • 代码计算量太大

  • View Inflate 太重

  • JSON 解析

  • 类加载

  • 大量对象创建

  • 复杂业务逻辑


另外一种情况:

Wall Duration:

100ms

CPU Duration:

10ms

那就说明:

这 100ms 里,大部分时间线程其实没有在真正执行。

那接下来就应该问:

它在等什么?


十三、线程状态:Running

Perfetto 中非常重要的一个状态:

Running

代表:

线程当前正在 CPU 上真正执行。

例如:

Main Thread 连续 Running 80ms,

说明主线程这 80ms 基本一直在干活。

这时候就需要展开调用范围,看具体是什么任务。

如果启动过程中出现大量长时间 Main Thread Running,

通常说明:

主线程工作量太大。


十四、线程状态:Runnable

Runnable 表示:

线程已经准备好了,也想执行,但是暂时没有拿到 CPU。

这个状态非常重要。

例如启动阶段创建了大量后台线程:

线程 A 想运行。

线程 B 想运行。

线程 C 想运行。

线程 D 也想运行。

CPU 核心数量有限,

这些线程开始互相竞争 CPU。

这时候主线程也可能出现:

Runnable

但无法马上获得 CPU。

最终导致:

首帧反而更慢。

所以一个非常常见的误区就是:

把所有初始化都扔到线程池,就是启动优化。

实际上不一定。

如果启动瞬间创建大量线程,

可能造成:

CPU 竞争。

所以异步只是手段,

并不代表一定更快。


十五、线程状态:Waiting

Waiting 可以简单理解成:

当前线程正在等待某件事情完成。

例如:

  • 等锁

  • 等 Binder

  • 等其他线程

  • 等 Condition

  • 等系统服务返回结果

如果主线程在启动阶段:

Waiting 很久

就要继续追:

主线程到底在等谁?

例如某个初始化逻辑:

主线程启动一个后台线程。

然后马上:

wait()

等待后台线程完成。

这种所谓"异步",实际上对启动并没有帮助。

因为:

主线程最终还是被阻塞了。


十六、Sleeping 是不是性能问题?

Sleeping 表示:

线程当前没有执行任务。

但不能看到 Sleeping 就认为:

这里浪费了启动时间。

线程休眠可能是系统正常调度的一部分。

例如等待:

  • VSync

  • 定时任务

  • 系统事件

所以 Perfetto 分析时不能单独看到某种状态就下结论。

要结合:

整个启动时间线

一起判断。


十七、启动阶段为什么要重点查 IO?

Android 启动过程中非常忌讳:

在主线程执行大量磁盘 IO。

例如:

  • 读取文件

  • SQLite 查询

  • SharedPreferences

  • 大文件解析

  • 读取缓存

  • 文件解压

CPU 的速度非常快。

但磁盘 IO 延迟相对更高,而且存在波动。

如果首屏必须等待这些 IO 完成,

就很容易拖慢启动。

Perfetto 中如果发现启动阶段存在明显:

open

read

或者其他文件系统行为,

就应该进一步检查。


十八、SharedPreferences 也可能影响启动

SharedPreferences 看起来非常轻量,

所以很多项目喜欢在启动阶段大量读取配置。

少量简单读取通常问题不大。

但是如果:

  • 文件很大

  • 配置项很多

  • 频繁读取

  • 启动阶段同步加载

依然可能造成 IO 和解析成本。

所以做启动优化时:

"代码看起来简单"不代表没有性能问题。

最终还是应该回到 Trace 和数据。


十九、Binder 为什么会拖慢启动?

Android 中大量系统服务调用最终都会经过:

Binder IPC。

例如:

应用进程需要向系统服务请求某些信息。

主线程发出 Binder 调用后:

系统服务处理

返回结果

在这个过程中,

主线程可能需要等待。

所以如果 Perfetto 中发现启动阶段存在大量 Binder transaction,

需要考虑:

这些系统服务调用真的必须在启动阶段完成吗?

例如某些设备信息、配置、权限状态、服务查询,

可能完全可以延迟。


二十、启动阶段发生 GC 怎么看?

启动时通常需要创建大量对象:

  • Application

  • Activity

  • View

  • Drawable

  • Fragment

  • SDK 对象

  • JSON 对象

如果启动阶段对象创建过于激进,

可能触发:

GC。

GC 不一定代表一定有严重问题。

但如果:

首屏之前频繁 GC,

就需要警惕。

可能存在:

  • 大量临时对象

  • 大图片

  • 集中初始化

  • JSON 大量解析

  • View 创建过多

GC 本身会增加启动阶段的系统工作量。


二十一、Choreographer 是什么?

进入页面绘制阶段后,

Perfetto 中通常会看到:

Choreographer

它是 Android UI 系统中的帧调度器。

可以简单理解:

系统准备开始处理这一帧了。

它会协调:

  • Input

  • Animation

  • Traversal

  • Draw

等工作。

在启动阶段,我们重点关注:

第一帧什么时候被真正处理出来。


二十二、traversal 为什么重要?

Perfetto 中经常会看到:

traversal

它和:

ViewRootImpl.performTraversals()

密切相关。

这一阶段主要完成:

Measure

Layout

Draw

如果 traversal 很重,

通常说明:

首屏 View 本身可能存在性能问题。

例如:

  • 布局层级过深

  • View 数量过多

  • 自定义 View 计算复杂

  • 重复 measure

  • 首页一次加载过多模块

所以可以简单区分:

Application 慢:

初始化问题。

Activity 慢:

页面创建或业务逻辑问题。

Traversal 慢:

UI 布局和绘制问题。


二十三、RenderThread 要不要看?

需要看,

但是启动优化第一阶段不要把重点放在这里。

RenderThread 主要参与:

  • Display List

  • RenderNode

  • 图形渲染

  • GPU 提交相关工作

如果 Main Thread 本身就被:

Application、Activity、IO、锁

堵了 300ms,

这时候研究 RenderThread 意义不大。

所以正确顺序通常是:

先主线程,再渲染线程。


二十四、Perfetto 启动分析可以按照什么顺序?

如果以后拿到一条启动 Trace 不知道从哪里开始,

可以按照下面的顺序:

第一步

找到:

App Main Thread


第二步

找到:

启动时间范围

从进程创建一直看到首帧。


第三步

观察:

bindApplication

是否异常。


第四步

看:

Application / ContentProvider

有没有重初始化。


第五步

看:

Activity.onCreate

和首屏布局。


第六步

看线程状态:

  • Running

  • Runnable

  • Waiting


第七步

检查:

  • IO

  • Binder

  • GC


第八步

最后看:

Choreographer / traversal / RenderThread

这套顺序基本可以覆盖大部分常见启动问题。


二十五、一个很实用的分析口诀

如果不想记太多,可以记这一句话:

先主线程,再生命周期;看 CPU,再看等待;查 IO、Binder、GC;最后看首帧。

展开就是:

Main Thread

Application / Activity

CPU Duration

Running / Runnable / Waiting

IO / Binder / GC

Traversal / RenderThread

这样分析 Trace 就不会特别乱。


二十六、Perfetto 不是优化工具

最后再强调一个很容易混淆的问题。

Perfetto 本身并不会帮我们优化启动。

它的作用是:

定位问题。

例如通过 Perfetto,我们发现:

Application 初始化:

80ms。

其中:

某个地图 SDK:

45ms。

这时候真正的优化方案可能是:

把地图 SDK 延迟到真正进入地图页面时初始化。

又比如发现:

Activity Layout Inflate:

60ms。

那么真正优化的是:

简化首页布局。

所以:

Perfetto = 找病因。

而:

修改业务代码 = 真正治病。


二十七、启动优化到目前已经形成一个小闭环

到这里,我们已经完成:

第一步:

理解 Android 冷启动流程。

第二步:

Macrobenchmark 测量启动时间。

当前:

TTID median ≈ 340.2ms

第三步:

Perfetto 分析启动耗时。

现在终于可以开始回答:

340ms 到底花在哪里?

下一步就可以真正进入:

业务启动优化。


二十八、小结

Perfetto 做启动分析时,

最重要的不是记住多少工具按钮。

而是建立一个分析思路:

先确定启动时间范围

找到 Main Thread

检查 Application

检查 Activity

看 CPU Duration

看 Running / Runnable / Waiting

检查 IO / Binder / GC

检查首屏 traversal

最终找到:

哪一段才是真正值得优化的启动瓶颈。

下一篇我们继续:

Android 启动优化(四):Application 与第三方 SDK 初始化优化

重点会讲:

  • 为什么 Application.onCreate 越来越重

  • ContentProvider 自动初始化

  • 第三方 SDK 启动耗时

  • 哪些任务必须立即初始化

  • 哪些任务应该延迟

  • 哪些任务适合异步

  • 为什么"全部扔线程池"并不是正确的启动优化

从下一篇开始,就正式从:

分析问题

进入:

解决问题。

相关推荐
地平线开发者1 小时前
征程6|YOLOv5x 在 Horizon J6 平台的完整部署实战(上)
算法
青 春 记 忆1 小时前
LeetCode 104. 二叉树的最大深度|Python 解法详解
python·算法·leetcode
watersink1 小时前
机器学习关联分析
人工智能·算法·机器学习
牛油果子哥q1 小时前
C++STL算法超全精讲:排序/查找/去重/遍历/最值/合并全套API、仿函数、Lambda适配、实战避坑
开发语言·c++·算法
AI服务老曹2 小时前
摄像机安装AI视频分析完整流程:从现场物理安装、硬解配置到算法POC效果验证
人工智能·算法·音视频
孤存5222 小时前
c语言动态内存管理
c语言·开发语言·算法
牛阿大2 小时前
MPC算法
算法
薛定e的猫咪2 小时前
(ICLR2025) C‑MORL :基于约束优化高效挖掘 MORL 帕累托前沿
人工智能·深度学习·算法
祖力552 小时前
快速学会数据结构中的链式队列与循环队列
linux·c语言·数据结构·算法