Android 启动优化(二):TTID、TTFD 与 Macrobenchmark 启动性能测量

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

重要的事情说三遍

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

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

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

上一篇我们梳理了一次 Android 冷启动的大致过程:

Launcher → Zygote → ActivityThread → Application → Activity → 首帧显示。

知道启动过程之后,下一个问题就是:

我的 App 启动到底慢不慢?

启动优化不能靠"感觉"。

比如我们把一个 SDK 延迟初始化之后,感觉页面好像出来得更快了,但到底快了多少?是快了 20ms,还是 200ms?

如果没有统一的测量方式,优化前后的结果就无法比较。

所以启动优化真正开始之前,需要先建立:

启动性能基线。


一、启动速度到底测什么?

很多人刚接触启动优化时,会把:

Application.onCreate() 耗时

或者:

Activity.onCreate() 耗时

直接当成启动耗时。

这是不准确的。

因为真正的启动过程远不止这两个方法。

例如一次冷启动还包括:

  • 创建 App 进程

  • ClassLoader 初始化

  • ContentProvider 初始化

  • Application 初始化

  • Activity 创建

  • Layout Inflate

  • Measure

  • Layout

  • Draw

  • RenderThread

  • 首帧显示

因此:

单独测一个生命周期方法,不能代表完整启动时间。

真正评价启动体验,Android 中最重要的两个指标是:

TTID

和:

TTFD


二、什么是 TTID?

TTID 全称:

Time To Initial Display

可以理解成:

从用户启动 App,到第一帧真正显示出来需要多长时间。

例如:

用户点击 App:

0ms

系统创建进程

Application 初始化

MainActivity 创建

首页布局绘制

第一帧显示:

340ms

那么这次:

TTID ≈ 340ms

TTID 关注的是:

用户什么时候第一次看到 App 的页面。

所以它非常适合衡量:

启动首屏速度。


三、TTID 不代表页面已经完全可用

这里有一个很容易误解的地方。

假设首页启动过程是这样的:

0ms

用户点击 App。

300ms

Activity 第一帧出来。

页面上已经有:

Logo、标题、底部导航。

800ms

数据库读取完成。

1100ms

首页列表加载完成。

1300ms

页面所有关键功能可以使用。

那么:

TTID 可能只有:

300ms

但用户真正可以正常使用页面,却需要:

1300ms

所以:

首帧快,不代表真正启动体验一定好。

于是就有了另外一个指标:

TTFD。


四、什么是 TTFD?

TTFD 全称:

Time To Full Display

可以理解成:

从启动 App,到页面真正完成关键内容加载的时间。

TTID 关注:

"页面出来了吗?"

TTFD 关注:

"页面真正准备好了吗?"

例如一个新闻 App。

首屏 300ms 出现:

  • Toolbar

  • Tab

  • 白色列表区域

但是新闻数据 900ms 才显示。

那么:

TTID:

大约 300ms

TTFD:

可能接近 900ms

这两个指标关注的是不同阶段。


五、TTID 和 TTFD 怎么理解最简单?

可以简单记成:

TTID

用户什么时候看到东西。

TTFD

用户什么时候真正可以正常使用。

举个更加直观的例子。

一个外卖 App:

点击图标。

300ms:

首页框架出来了。

600ms:

定位完成。

800ms:

商家列表出现。

1000ms:

优惠、推荐等数据全部加载完成。

那么:

TTID 可能是 300ms。

而:

TTFD 可能接近 800~1000ms。

所以一个比较完整的启动优化,不能只盯着:

第一帧。

最终还要关注:

用户真正进入可用状态的时间。


六、为什么不能自己简单打时间戳?

早期我们很容易这么做:

在 Application 开始的位置记一个时间。

然后在 Activity.onResume() 再记一个时间。

两者相减:

这就是启动时间。

这种方式可以辅助分析某段业务代码,但作为完整启动性能指标并不可靠。

因为它没有包含完整的系统启动过程。

比如:

Application 开始计时之前,系统已经做过:

  • 进程创建

  • Runtime 初始化

  • ClassLoader 初始化

  • 一部分 Framework 工作

另外:

Activity.onResume() 被调用,也并不代表:

第一帧已经真正显示到屏幕。

所以自己打点更适合:

分析具体业务阶段耗时。

而完整启动性能测试,更适合使用系统提供的性能测试工具。

现在官方比较推荐的一套方案就是:

Macrobenchmark


七、Macrobenchmark 是什么?

Macrobenchmark 是 Android Jetpack 提供的一套:

应用级性能测试工具。

它和普通 Unit Test 不一样。

Macrobenchmark 可以直接针对一个真实 App 做性能测试,例如:

  • 冷启动

  • 温启动

  • 热启动

  • 页面滚动

  • 页面跳转

  • Frame 性能

  • Baseline Profile 相关性能验证

对于启动优化来说,它最大的作用就是:

建立稳定、可重复的启动性能基线。

我们可以让它自动:

启动 App

记录性能数据

关闭 App

重新启动

重复多次

输出最终结果。

相比自己拿秒表或者打印 Log,要可靠得多。


八、为什么 Benchmark 要单独建一个模块?

Macrobenchmark 通常不会直接写在业务 App 模块里面。

项目结构一般类似:

app

或者我们当前项目里的:

ReDexSample

负责真正运行的 App。

另外建立:

benchmark

模块。

benchmark 模块负责:

从外部控制目标 App,并测量它。

也就是说:

benchmark

启动

ReDexSample

记录启动性能。

这种设计非常重要。

因为我们想测的是:

App 从外部真正启动时的完整性能。

而不是 App 自己启动之后再开始计时。


九、Macrobenchmark 启动测试的核心

一个典型的启动 Benchmark,本质上主要需要告诉系统三件事情:

第一:

测谁?

例如:

com.sample.redex

第二:

测什么?

例如:

StartupTimingMetric

第三:

怎么启动?

例如:

StartupMode.COLD

然后 Macrobenchmark 会多次执行:

停止 App

冷启动 App

记录启动耗时

保存 Trace

再次执行。


十、StartupMode.COLD 是什么?

Macrobenchmark 支持不同启动模式。

最重要的就是:

COLD

冷启动。

App 进程不存在。

需要重新:

  • 创建进程

  • Application 初始化

  • Activity 创建

  • 首帧绘制

这是启动优化最关注的模式。


WARM

温启动。

进程通常仍然存在,但是 Activity 需要重新创建或恢复相应状态。


HOT

热启动。

App 和 Activity 的大部分状态仍然存在。

主要是把页面重新带回前台。


在实际启动优化中:

通常优先分析 Cold Startup。

因为冷启动链路最长,也最容易暴露问题。


十一、StartupTimingMetric 测什么?

Macrobenchmark 做启动测试时,经常会使用:

StartupTimingMetric

它主要帮助我们获取启动相关的时间指标。

其中最常见的就是:

timeToInitialDisplayMs

也就是:

TTID。

如果应用正确上报"页面已经完全准备完成"的时机,还可以进一步观察 Full Display 相关指标。

所以我们后面看到:

timeToInitialDisplayMs

基本可以直接理解为:

从启动到首帧显示耗时。


十二、Iteration 是什么意思?

启动性能存在波动。

同一个 App 连续启动五次,不可能每一次都是:

340.2ms

可能是:

第一次:

352ms

第二次:

325ms

第三次:

387ms

第四次:

340ms

第五次:

329ms

原因很多,例如:

  • CPU 调度

  • 系统后台任务

  • IO

  • 温度

  • 缓存状态

  • GC

  • 设备负载

所以性能测试不能只跑:

一次。

Macrobenchmark 会通过:

Iterations

重复执行测试。

例如:

5 次。

然后输出:

  • min

  • median

  • max

等结果。


十三、为什么我更关注 median?

假设 5 次结果是:

325ms

330ms

340ms

350ms

387ms

其中:

最小值:

325ms

最大值:

387ms

中位数:

340ms

如果只看最小值:

可能过于乐观。

如果只看最大值:

又可能被某次系统抖动影响。

所以实际分析时,我通常更关注:

Median,中位数。

它更适合作为当前版本的性能基线。

当然,真正线上做性能监控时,还会进一步看:

P50、P90、P95 等分位数。


十四、一次真实的 Macrobenchmark 测试

我目前测试的项目是:

Chapter22-master

目标 App 模块:

ReDexSample

Benchmark 模块:

benchmark

测试设备:

小米 M2102K1AC

系统:

Android 14 / API 34

测试的是:

Cold Startup

运行:

5 次 Iteration

最终 Macrobenchmark 输出:

ExampleStartupBenchmark_startup

指标:

timeToInitialDisplayMs

结果:

  • min:324.7ms

  • median:340.2ms

  • max:387.0ms

因此当前可以先建立一个启动性能基线:

TTID median ≈ 340.2ms

这一步非常重要。

因为从现在开始,后面所有启动优化都必须和这个数字比较。


十五、340ms 到底算不算慢?

看到:

340ms

很多人第一反应就是:

这个数字到底好不好?

实际上不能简单脱离场景判断。

启动速度会受到很多因素影响:

  • 设备性能

  • Android 版本

  • App 复杂程度

  • 首页布局

  • SDK 数量

  • 数据加载方式

  • 是否使用 Profile

  • Release / Debug 构建方式

所以性能优化更重要的不是:

和别人比一个绝对数字。

而是:

建立自己的基线,然后持续对比。

例如现在:

优化前:

340.2ms

如果优化 Application 后:

315ms

再优化首页布局:

290ms

再使用 Baseline Profile:

260ms

那么我们就能非常清楚地知道:

每一次优化到底有没有效果。


十六、Macrobenchmark 最大的价值不是"测一个数字"

Macrobenchmark 真正有价值的地方还有一个:

它会生成 System Trace。

例如我们跑 5 次测试以后,会看到:

Iteration 0

Iteration 1

Iteration 2

Iteration 3

Iteration 4

每一次启动都对应一条 Trace。

这些 Trace 可以使用:

Perfetto / System Trace

打开。

然后我们就可以继续往下分析:

340ms 到底花在哪里?

例如:

是:

bindApplication

比较慢?

还是:

Application.onCreate

比较慢?

还是:

Activity.onCreate

比较慢?

还是:

traversal

比较慢?

又或者:

主线程大量:

Runnable

一直抢不到 CPU?

这就把启动优化从:

"感觉慢"

变成了:

"有数据、有 Trace、有证据。"


十七、Macrobenchmark 和 Perfetto 的关系

这两个工具千万不要混淆。

可以简单记成:

Macrobenchmark

回答:

有多慢?

例如:

TTID:

340.2ms


Perfetto

回答:

为什么慢?

例如:

Application:

60ms

Activity:

90ms

Layout:

40ms

IO:

30ms

线程等待:

20ms

......

所以完整流程应该是:

Macrobenchmark

测出:

340ms

Perfetto

分析:

340ms 花在哪里。

这才是真正进入启动优化。


十八、为什么性能测试最好使用真机?

启动性能和:

  • CPU

  • 内存

  • IO

  • 调度

  • 系统版本

都有直接关系。

模拟器的运行环境和真实手机差别非常大。

所以性能优化中一个很重要的原则就是:

尽量使用真机测试。

并且最好固定:

  • 同一台设备

  • 同一个系统版本

  • 同一种构建类型

  • 相同测试场景

否则前后结果可能不具备可比性。

例如:

优化前用旗舰机测试。

优化后换了一台老设备。

那两个数字比较基本没有意义。


十九、启动性能测试还要注意环境稳定

即使使用同一台设备,也需要尽量减少外部干扰。

例如测试过程中:

后台正在下载文件。

或者:

手机正在发热降频。

或者:

系统正在执行大量后台任务。

都会影响结果。

因此一次性能优化最好不要只看:

某一次测试结果。

而是通过多次测试观察:

整体趋势。

例如:

优化前:

Median = 340ms

连续测试基本在:

330~360ms

优化后:

Median = 285ms

连续测试基本在:

275~300ms

这种下降才更有说服力。


二十、不要为了数字而优化

还有一点非常重要。

启动优化不是为了:

把 Benchmark 数字做到最低。

最终还是要回到:

用户体验。

例如一种"优化"方式:

App 300ms 就显示一个空白首页。

所有真正内容:

2 秒以后才加载。

那么 TTID 的确非常漂亮。

但用户体验可能反而更差。

所以真正完整的启动优化,最终应该同时关注:

首帧速度

和:

页面真正可用速度。

也就是:

TTID

TTFD

而不是单纯追求一个更小的首帧数字。


二十一、建立启动优化基线

到这里,我们现在已经有了第一组非常重要的数据:

当前版本:

Cold Startup

设备:

小米 M2102K1AC / Android 14

测试工具:

Macrobenchmark

Iterations:

5

TTID:

  • min:324.7ms

  • median:340.2ms

  • max:387.0ms

因此:

340.2ms 就是我们当前启动优化实验的基准线。

以后每做一次优化:

都应该重新跑 Macrobenchmark。

例如:

业务初始化优化:

重新测。

Baseline Profile:

重新测。

Startup Profile:

重新测。

DEX 类重排:

重新测。

最终用数据判断:

到底有没有真正改善启动性能。


二十二、小结

这一篇主要解决了一个问题:

Android 启动速度到底应该怎么测?

我们先认识了两个核心指标:

TTID

表示:

从启动到第一帧显示。

TTFD

表示:

从启动到页面真正完成关键内容加载。

然后使用:

Macrobenchmark

建立稳定的启动性能基线。

当前实测结果:

TTID median ≈ 340.2ms

但这时候我们仍然只知道:

App 启动需要大约 340ms。

还不知道:

这 340ms 到底花在哪里。

因此下一篇就正式进入启动优化最关键的一步:

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

下一篇我们会开始真正看:

  • Main Thread

  • bindApplication

  • Application

  • Activity

  • Running

  • Runnable

  • Waiting

  • Wall Duration

  • CPU Duration

  • IO

  • Binder

  • GC

  • Choreographer

  • traversal

从:

"测出来慢"

正式进入:

"找到为什么慢"。

相关推荐
Python私教1 小时前
别急着加 llms.txt:企业官网面向 AI 搜索的工程清单
前端·人工智能·seo
东方小月1 小时前
从零开发一个 Coding Agent(十三):实现安全的 read 文件读取工具
前端·人工智能·全栈
东方小月1 小时前
从零开发一个 Coding Agent(十二):实现版本化 JSONL 与真实 CLI 入口
前端·设计模式·前端框架
FL16238631292 小时前
室内易燃物识别易燃评估室内易燃程度识别分割数据集labelme格式1015张85类别
java·服务器·前端
用户059540174462 小时前
把 AI 长期记忆去重测试从 20 分钟压到 40 秒,重复率从 18% 干到 1.5%
前端·css
kyriewen3 小时前
我把 AI 写的并发请求控制器手写了一遍——3 个语义我当时根本讲不清
前端·javascript·面试
_codemonster3 小时前
Vue中的ref和reactive到底在干嘛
前端·javascript·vue.js
IT_陈寒4 小时前
为什么我的JavaScript闭包总是漏掉那个变量?
前端·人工智能·后端
两只羊ovo4 小时前
前端八股之 storage & this:两个最容易卡住的点,一次讲透
前端