之前有人在知乎提问:“怎么计算apk的启动时间?” :
利用python或者直接用adb命令怎么计算apk的启动时间呢?就是计算从点击图标到apk完全启动所花费的时间。比如,对游戏来说就是点击游戏图标到进入到登录界面的这段时间。
已知的两种方法貌似可以获取,但是感觉结果不准确:一种是,adb shell am start -w packagename/activity,这个可以得到两个值,ThisTime和TotalTime,不知道两个有什么区别,而且与实际启动时间不匹配,两者相加都可能比实际启动时间小(测试游戏的时候差别更大);另外一种是通过adb logcat的方式,感觉获取的结果也与实际有差别。
我和另外一个同事郭启发 针对两个方面进行了回答,不过毕竟知乎上看的人会比较少,所以我在征得他的同意之后,将这两个答案整理了一下,记录到博客中,一来算是一个小的总结,之后自己看得时候比较方便,二来给需要的同学一个更加方便的途径。
版本说明(2026-10-13 核查):原文三列输出按 AOSP
android-5.1.1_r1解释;android-4.4.4_r2的命令没有打印 WaitTime,而android-16.0.0_r1已不再打印 ThisTime。请按系统版本、启动状态和计时起止点理解结果。本文保留历史例子,补充 TTID、TTFD 与当前测量方法;没有把旧数值当成本次设备测试。
1 应用启动场景
启动时间能测量,但先要定义起点和终点:是系统收到启动请求到首次显示,还是用户点击图标到登录页可操作?这些不是同一个指标。游戏和普通应用都可能在首帧后继续加载内容,不能简单按“游戏/非游戏”决定一种统一口径。
1.1 应用启动
桌面通常通过 MAIN/LAUNCHER 入口启动 Activity,目标不一定叫 MainActivity,也可能经过路由或已有任务。如果创建新实例,会经历 onCreate/onStart/onResume;复用实例的路径不一定重走全部回调。
onResume() 返回不保证界面已经绘制或显示。View 路径后续可能进行 traversal,按需执行 measure/layout/draw;遍历次数不是固定两次。第一帧也可能只有背景、占位图或加载状态,并不等于 setContentView 中的业务内容已全部可用。
因此要区分系统的初始显示时间 TTID 与应用声明完整可用状态的 TTFD。只在生命周期回调前后打点,不能覆盖完整启动路径。
下面先解释命令返回值。命令是大写 -W,不是题目中小写的 -w;它触发一次 shell 启动请求,不会自动计入用户触摸、Launcher 处理和宿主机 adb 启动的全部开销。
1.2 冷启动:目标进程不存在
冷启动的条件是目标进程尚未运行,不是“安装后的第一次”,也不只是手机重启后的第一次。进程被回收后再次进入,也可能是冷启动。首次安装、首次登录、已有缓存和代码编译状态还应单独记录。
下面保留原文的历史输出,单位为毫秒;355/365 不是性能目标或当前设备结果:
1 | ➜ adb shell am start -W com.media.painter/com.media.painter.PainterMainActivity |
不能统一以 WaitTime 为准,也不能将三个值相加。按 android-5.1.1_r1 的源码看,它们的边界不同。
frameworks/base/cmds/am/src/com/android/commands/am/Am.java 中,等待分支调用 AMS 的 startActivityAndWait()。原稿第一张源码图已无法辨认,改为对应的计时代码摘录:
1 | final long startTime = SystemClock.uptimeMillis(); |
随后输出 WaitTime = endTime - startTime。这是命令内部的等待区间,包含这次启动调用和等待返回的开销;不从用户的物理触摸时刻开始。三个数值中它通常覆盖更外层的区间,但不能只把差值解释成上一应用的 pause 时间。
ActivityRecord.reportLaunchTimeLocked(curTime) 计算另两个值:
1 | final ActivityStack stack = task.stack; |

curTime:此处由windowsDrawn()传入的SystemClock.uptimeMillis()。displayStartTime:当前 Activity 的显示启动统计起点,由ActivityStack.setLaunchTime()等路径维护,不是固定等于onCreate()入口。stack.mLaunchStartTime:该 stack 尚未结束的启动统计区间起点;为 0 时 TotalTime 回退为 ThisTime。
单 Activity 的两个起点一致时,ThisTime 与 TotalTime 相等。如果同一未结束统计区间中先经过未报告绘制完成的 Activity,再到后续 Activity,两者可能不同。不能把它概括成任意“第一个 Activity 到最后一个 Activity”:一次报告结束会清零 mLaunchStartTime,有界面的中间启动页也可能已经结束前一段统计。

图中的 TotolTime 是旧图拼写错误,应读作 TotalTime;画在一起的端点也不表示窗口绘制报告与命令返回完全同时。①②③只是一条典型旧启动路径的示意:
- ①可能包含 AMS 的任务选择、前台 Activity pause 和调度等准备工作。
- ②可能包含新进程创建、前一个 Activity 的初始化及跳转。
- ③不只包含
onCreate/onResume,还要到系统收到目标窗口已绘制的报告;它仍不是登录页等业务内容就绪的保证。
ThisTime 与 TotalTime 是重叠区间,WaitTime 也不是另一个应累加的阶段。看应用窗口这段启动统计时可参考 TotalTime,看命令等待时参考 WaitTime;两者都不能直接替代“点击到业务可用”的端到端测量。
此标签的 am_activity_launch_time 事件和 Displayed 日志由同一个 reportLaunchTimeLocked() 写出。与同一次成功等待的返回值可以对应,但不要跨启动实例、超时分支或系统版本硬套字段名称。已有任务直接回到前台时,旧实现还有返回 0 或按可见性等待的分支,并非重新走上述首次绘制公式。
最后纠正结束点:旧代码在 ActivityRecord.windowsDrawn() 中上报耗时,WMS 侧的 drawn 与 visible 状态也是分别计算的;StartingWindow 被排除在应用绘制计数之外。这是系统窗口绘制统计信号,不是光学意义的“屏幕像素已经显示”时间戳,更不证明异步内容已加载完整。
1.3 温启动与热启动
进程仍在、Activity 需要重建的情况通常是温启动,例如退出页面后重建 Activity。进程和 Activity 都保留,只需把现有 Activity 带到前台,才是常说的热启动。不能只看“第二次打开”或“按过 Back”分类:Back 行为、任务状态和系统版本都可能影响实例是否保留。
AOSP Android 16 的 WaitResult 还区分 RELAUNCH(保留进程,但已有 Activity 需要销毁后重建,例如未自行处理的配置变化)和 UNKNOWN 等内部状态,不能把所有返回都硬归到冷/温/热的同一个样本组。对照实验应明确准备过程,并记录实际状态。
2 游戏启动场景
命令行同样可以启动游戏 Activity,并观察系统窗口启动统计;它的限制是不能自动知道游戏业务何时可操作。普通应用的联网首页、登录态恢复或数据库加载也有同样的问题。
2.1 系统部分
游戏通常也由 Activity 承载窗口。系统部分可以观察进程、Activity 和初始窗口绘制;如果入口来自 shell,就不要把起点标成用户点击 Launcher。
2.2 游戏部分
主 Activity 出现后,游戏可能继续加载资源、联网更新、读取配置或初始化引擎。首个 Surface 或窗口帧可能只是加载画面;何时到达可以操作的登录页,需要游戏自己定义可验证的就绪条件。
不必只靠 Log:可以结合单调时钟打点、Trace 事件,以及在应用达到完整可用状态时报告 reportFullyDrawn()。自绘或游戏引擎仍需把资源就绪与实际渲染的关系说明白,不能只看到下载完成就宣布界面已可操作。日志两端要使用同一时钟口径;从应用 onCreate 开始的计时,不能倒推出之前的图标触摸和进程创建耗时。
对于游戏的启动时间,我们更倾向于计算从点击桌面图标到用户可以与游戏进行交互的时间。需要明确这两个事件如何被观测和关联。
3 总结
计算机最让人着迷的一点就是其准确性,1+1 永远等于 2,启动耗时多久就是多久,每一次可能不一样,但每一次的时间都是这一次的准确时间。
不过每个公司由于对应用的定位不同,所以对应用启动的要求也不一样。比如有的做 ROM 的公司,其内置应用的启动时间一定是要非常快的,这样给用户的第一感觉就是快、流畅;互联网公司的 App 则不是很关心启动速度,大部分互联网公司的应用都有一个启动页,用来展示广告或者功能介绍之类的,然后才会进入到主界面。需求不一样,这么做也无可厚非,不过从消费者的角度来看,越早见到主界面当然越好。
所以在做一个 Android App 的时候,一定要记得将应用的启动时间作为一个性能指标,毕竟:
天下武功,唯快不破!
4. 2026 年补充:怎样做可比较的测量
先固定启动入口与状态。 本地粗测可以用下面的命令,将包名和组件替换为待测应用的真实入口。-S 会先 force-stop 目标包,再启动 Activity;它不会把安装、登录或磁盘缓存恢复成“首次安装”状态,也不模拟一次真实手指点击。
1 | adb shell am start -W -S \ |
先检查 Status、目标 Activity、超时或“已有任务被带到前台”等提示,再解释耗时。所核对的 Android 16 ActivityManagerShellCommand 打印 LaunchState、非负的 TotalTime 和 WaitTime,不再打印 ThisTime;不要把缺失列填成 0。该实现的 force-stop 和随后等待发生在 startTime 之前,宿主机整条 adb 命令的墙钟耗时也不等于 WaitTime。
比较版本性能时使用 Macrobenchmark。 在独立 benchmark 模块中使用 StartupTimingMetric(1.1.0 起),明确 StartupMode.COLD/WARM/HOT、CompilationMode、数据/登录状态及重复次数。使用接近 release 的非 debuggable 目标构建、真机和一致的测试条件,按所用版本要求配置 profileable。观察多次样本的中位数和分布,同时查看生成的 Trace;不要只拿一次最快结果,也不要混比“无 AOT”与“已有 Baseline Profile”的结果。
指标含义要分开:
timeToInitialDisplayMs:系统收到启动 Intent 到目标 Activity 首帧渲染的初始显示统计。它不是所有业务内容就绪时间。timeToFullDisplayMs:依赖应用的 fully-drawn 信号,结束于包含该信号或紧随其后的首帧完成渲染。应用没有正确报告、录制过早结束,或 API 29 之前指标不可用时,不能把缺失当成 0 或达标。测试的 measureBlock 需要等待真实的就绪条件,才能把异步内容纳入观察。- 若要测“物理点击到登录页可交互”,还需明确输入起点、就绪条件与所用时钟,并用系统 Trace/显示端证据核对;不能用命令列相加替代。
可从官方 Macrobenchmark 示例与配置说明 建立测试。Android 12 / API 31 起结合 Perfetto FrameTimeline 看应用帧、GPU 和 SurfaceFlinger 的关联;DrawFrame CPU 区间结束也不等于该帧已经呈现。线上 Android Vitals 的统计另有采样与口径,需和自己的业务可用性指标分别解读。
核对来源
- AOSP 5.1.1 Am.java, ActivityRecord.java, ActivityStack.java, ActivityStackSupervisor.java, AppWindowToken.java
- AOSP 4.4.4 Am.java
- AOSP 16 ActivityManagerShellCommand.java, WaitResult.java, ActivityMetricsLogger.java
- App startup time, StartupTimingMetric, Macrobenchmark configuration, Perfetto FrameTimeline
关于我 && 博客
下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!
- 博主个人介绍 :里面有个人的微信和微信群链接。
- 本博客内容导航 :个人博客内容的一个导航。
- 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
- Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。
一个人可以走的更快 , 一群人可以走的更远
