序、众所周知,每一次开始写这种框架系类的文章的时候,就是我在找工作了,有闲暇时间可以写点东西。哈哈哈哈,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 启动耗时
-
哪些任务必须立即初始化
-
哪些任务应该延迟
-
哪些任务适合异步
-
为什么"全部扔线程池"并不是正确的启动优化
从下一篇开始,就正式从:
分析问题
进入:
解决问题。