我们应该如何测量Android APP的启动时间
01
必要性
启动时间是用户对应用的第一印象,更快的启动速度可以促进用户与应用的持续互动,从而减少过早退出、重启实例或前往其他应用的情况。
冷启动时间等性能指标也是应用能否在应用商店上获得成功(包括通过审核、获得推荐、赢得用户)的关键因素之一。Google Play主要通过Android Vitals进行强制要求。Android Vitals 是一个在Play控制台中展示的核心性能指标面板,是Google Play管理应用性能最重要的机制。Android Vitals 判定应用启动时间过长的条件:
-
冷启动用了 5 秒或更长时间。
-
温启动用了 2 秒或更长时间。
-
热启动用了 1.5 秒或更长时间。
02
Android Startup
Android Vitals 使用 TTFD 作为检测和报告应用启动时间(冷/温启动)的核心指标。TTID 是其背后用于性能分析和问题诊断的关键辅助数据。
在解释这两个指标之前首先需要介绍下Android Startup。
Android Startup机制从Android 7.0+的ActivityMetricsLogger基础监控,演进到Android 10+ (2019年)的ApplicationStartInfo细粒度追踪,再经过Android 15+的持续增强,目前已成为系统级的启动信息追踪系统,为应用和系统提供启动过程的详细数据,支持性能优化和问题诊断。
01
三种启动方式
在Android Startup机制中,应用的启动分为三种:常见的冷启动和热启动、以及温启动。三种启动类型的主要区别在于进程和 Activity 的初始状态。
冷启动(Cold Start)的判断条件是应用进程不存在;温启动(Warm Start)和热启动(Hot Start)的判断条件是:
// ActivityMetricsLoggerif (processRunning) {transitionType = !newActivityCreated && r.attachedToProcess()? TYPE_TRANSITION_HOT_LAUNCH: TYPE_TRANSITION_WARM_LAUNCH;
即:进程存在(processRunning = true)并且Activity已存在且已附加到进程(!newActivityCreated && r.attachedToProcess())时就是热启动,此时只需将 Activity 带到前台;否则就是温启动,需要重新创建Activity。
02
关键时间戳
另外Android Startup机制中有如下时间戳:
|
时间戳 |
值 |
记录位置 |
说明 |
|
START_TIMESTAMP_LAUNCH |
0 |
ActivityMetricsLogger.notifyActivityLaunching() |
系统决定启动应用的时间 |
|
START_TIMESTAMP_FORK |
1 |
ProcessList.startProcess() |
Zygote fork出应用进程的时间 |
|
START_TIMESTAMP_BIND_APPLICATION |
3 |
ActivityManagerService.attachApplicationLocked() |
bindApplication调用的时间 |
|
START_TIMESTAMP_APPLICATION_ONCREATE |
2 |
ActivityThread.handleBindApplication() |
Application.onCreate()被调用的时间 |
|
START_TIMESTAMP_INITIAL_RENDERTHREAD_FRAME |
6 |
ViewRootImpl.draw() |
RenderThread首次开始绘制的时间(TTID) |
|
START_TIMESTAMP_FIRST_FRAME |
4 |
ActivityMetricsLogger.notifyWindowsDrawn() |
所有窗口绘制完成的时间(TTFD) |
|
START_TIMESTAMP_SURFACEFLINGER_COMPOSITION_COMPLETE |
7 |
ViewRootImpl通过presentFence |
SurfaceFlinger完成合成的时间 |
|
START_TIMESTAMP_FULLY_DRAWN |
5 |
Activity.reportFullyDrawn() |
应用主动调用reportFullyDrawn的时间 |
其中:
TTID (Time To Initial Display)
含义:从启动到 RenderThread 开始首次绘制的时间(渲染开始,不是显示完成)
计算公式:TTID = START_TIMESTAMP_INITIAL_RENDERTHREAD_FRAME – START_TIMESTAMP_LAUNCH
TTFD (Time To First Display)含义:从启动到首帧显示完成的时间(首帧显示完成)计算公式:TTFD = START_TIMESTAMP_FIRST_FRAME – START_TIMESTAMP_LAUNCH
如果 TTID 很长:问题出在初始化阶段到第一帧绘制,原因可能是主线程阻塞、Application.onCreate() 太重、启动 Activity 的 onCreate() 太复杂等。
如果 TTID 很短,但 TTFD 很长:问题出在第一帧绘制之后到内容完全就绪,原因可能是异步数据加载慢、网络请求、复杂布局的渲染或列表数据的填充等。
如何获取这些时间戳的值呢?
首先需要获取 ApplicationStartInfo 对象。
有两种方式:
方式1:通过回调监听(推荐)
ActivityManager activityManager = getSystemService(ActivityManager.class);activityManager.addApplicationStartInfoCompletionListener(executor,(ApplicationStartInfo startInfo) -> {// 处理启动信息processStartInfo(startInfo);});
方式2:获取历史记录
ActivityManager activityManager = getSystemService(ActivityManager.class);// 获取最近的启动记录List<ApplicationStartInfo> startInfos =activityManager.getHistoricalProcessStartReasons(10); // 获取最近10条// 或者获取指定包名的启动记录List<ApplicationStartInfo> startInfos =activityManager.getExternalHistoricalProcessStartReasons("com.example.app", 10);然后通过ApplicationStartInfo对象实例获取对应时间戳的时间:// 获取启动信息ApplicationStartInfo startInfo = ...;Map<Integer, Long> timestamps = startInfo.getStartupTimestamps();// 冷启动时间 = 首帧时间 - 启动开始时间Long launchTime = timestamps.get(ApplicationStartInfo.START_TIMESTAMP_LAUNCH);Long firstFrameTime = timestamps.get(ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME);if (launchTime != null && firstFrameTime != null) {long coldStartTime = (firstFrameTime - launchTime) / 1_000_000; // 转换为毫秒// 这就是完整的冷启动时间}
03
reportFullyDrawn()的用途
从上一小节中可以看到应用真正可用的时间点是:
TART_TIMESTAMP_FULLY_DRAWN
表示:应用认为 UI 完全可用(UI 绘制完成 + 数据加载完成)。但是reportFullyDrawn()需要由应用主动调用。
如果应用主动调用了这个方法,系统就可以及时触发 StartupCompletedTask(通过 VMRuntime.notifyStartupCompleted())。
如果应用没有主动调用,则系统会通过超时保护机制,从进程 fork 开始5 秒后自动触发 StartupCompletedTask。
StartupCompletedTask会进行如下优化:
a) 生成运行时 App Image
在启动完成后生成运行时 App Image,加速后续启动
b) 清理启动时资源
-
释放启动时专用的 LinearAlloc
-
清理启动时 DexCache 资源
-
释放 App Image 的元数据
-
删除启动线程池
c) ProfileSaver 优化
ProfileSaver 会等待启动完成(通过 NotifyStartupCompleted() 通知),然后标记启动阶段收集的类和方法为”启动类/方法”,用于后续 JIT/AOT 优化。
04
reportFullyDrawn()调用时机
对于大多数场景,推荐在数据加载完成的回调中调用:
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initViews();// 加载关键数据loadCriticalData(() -> {// 关键数据加载完成,UI已绘制,应用处于可用状态reportFullyDrawn();});// 非关键数据可以延迟加载loadNonCriticalData(); // 不等待这个完成}
或者使用FullyDrawnReporter(Android 12+,支持多任务):
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);FullyDrawnReporter reporter = getFullyDrawnReporter();// 任务1:加载网络数据reporter.addReporter();loadNetworkData(() -> {updateUI();reporter.removeReporter();});// 任务2:加载本地数据reporter.addReporter();loadLocalData(() -> {updateUI();reporter.removeReporter();});// 当所有reporter都被移除后,自动调用reportFullyDrawn()}
03
开源框架
01
Matrix(腾讯)
功能:性能监控与调试,包含启动时间监控
特点:支持启动耗时、卡顿、内存等监控
GitHub:
https://github.com/Tencent/matrix
Matrix的核心实现:
-
Hook ActivityThread 的 Handler 捕获启动时间点
-
监听 Activity 生命周期判断启动阶段
-
使用 AppMethodBeat 记录方法调用栈
-
计算各阶段耗时并上报
该方案在应用启动早期即可开始监控,能准确捕获从进程启动到首屏显示的完整耗时。
核心组件:
StartupTracer:启动监控的主要类
ActivityThreadHacker:Hook ActivityThread 的 Handler 以获取启动时间点
AppMethodBeat:方法调用追踪,记录启动过程中的方法调用栈
提供四个耗时统计:
applicationCost:即Application.onCreate() 耗时。
– 开始时间点:
第一次调用被插桩的方法(AppMethodBeat.i())时,会触发初始化流程,然后在ActivityThreadHacker.hackSysHandlerCallback()中设置开始时间点(sApplicationCreateBeginTime)
– 结束时间点:
Hook的Handler处理第一个关键消息,如LAUNCH_ACTIVITY (100)、CREATE_SERVICE (114)、RECEIVER (113) 等
firstScreenCost:从应用启动监控开始到首屏 Activity 获得焦点的耗时
开始时间点:第一次调用被插桩方法时,Hook ActivityThread Handler 的时间点(sApplicationCreateBeginTime)
结束时间点:第一个 Activity 的 onWindowFocusChanged(true) 被调用时(即 Activity 获得焦点时)
coldCost:从启动监控开始到主Activity获得焦点的耗时(通常等于或大于 firstScreenCost)。
使用者可以通过 TraceConfig.Builder.splashActivities(String) 传入 Splash Activity 的全限定类名,如果不配置 splashActivities(或传入空字符串),Matrix 会认为没有 Splash Activity,coldCost 会直接计算到第一个 Activity 获得焦点的时间。
warmCost:从创建新 Activity 到其获得焦点的耗时。
判断热启动的关键:activeActivityCount == 0 && coldCost > 0,表示所有 Activity 已销毁且已完成一次冷启动,此时创建新 Activity 视为热启动。
开始时间点:所有 Activity 销毁后,创建新 Activity 的时间
结束时间点:新 Activity 获得焦点时
Matrix中应用的开始时间是:插桩方法AppMethodBeat.i()第一次被调用时。下面这个流程图可以帮助大家理解:
1.进程创建2.ActivityThread.main()3.ActivityThread.bindApplication() ← 【系统 Bind Application】4.Application 对象创建5.Application.attachBaseContext() ← 【第一次插桩方法调用】└─> AppMethodBeat.i() 被调用└─> realExecute() 触发└─> hackSysHandlerCallback() 设置开始时间 ← 【Matrix 开始监控】6.Application.onCreate()7.Activity 创建和显示
结束时间点是:onWindowFocusChanged(true)时刻,与Android Startup的TTID、TTFD相比,差别从下图可以看到:
1
Activity.onResume()
2
【ViewRootImpl 开始绘制流程】
├─> measure()
├─> layout()
└─> draw()
3
【首次绘制完成】← 【TTID 触发点】
4
onWindowFocusChanged(true): ← 【Matrix firstScreenCost 结束点】
5
notifyWindowsDrawn():系统检测到窗口绘制完成,【TTFD 触发点】
6
reportFullyDrawn():开发者通知系统 UI 完全准备好(最后,需手动调用)
这个顺序反映了从窗口可见 → 窗口绘制完成 → UI 完全就绪的过程。
02
Firebase Performance Monitoring(Google)
功能:能够自动捕获应用启动时间、屏幕追踪、慢帧、冻结帧等帧率指标。
应用启动时间:自动追踪冷启动耗时
屏幕追踪:自动追踪每个 Activity 的渲染性能
帧率指标:自动统计总帧数、慢帧数(>16ms)、冻结帧数(>700ms)
GitHub:https://github.com/firebase/firebase-android-sdk
这里只讲一下Firebase Performance Monitoring 是如何获取应用启动(冷启动)的耗时统计。
开始时间点:
private @NonNull Timer getStartTimerCompat() {// Preferred: Android system API provides BIND_APPLICATION timeif (processStartTime != null) {return processStartTime;}// Fallback: static initializer time (during class-load) ofaFirebaseclassreturn getClassLoadTimeCompat();}private @NonNull Timer getClassLoadTimeCompat() {// Prefered: static-initializer time of the 1st Firebase classduringinitif (firebaseClassLoadTime != null) {return firebaseClassLoadTime;}// Fallback: static-initializer time of the current classreturn PERF_CLASS_LOAD_TIME;}
其依据是应用的启动流程:
Zygote fork 进程
BIND_APPLICATION 开始 ← 系统记录 elapsedRealtime
加载 Application 类
ContentProvider 初始化
Application.onCreate()
Activity 生命周期
开始时间点首选processStartTime时刻,通过系统 API:Process.getStartElapsedRealtime() 获取系统在 BIND_APPLICATION 阶段记录的进程启动时的 elapsedRealtime(仅在 API 24 (Android 7.0) 及以上可用);
然后利用 Java 的类加载机制,在类首次被引用时自动执行静态初始化器,从而捕获类加载的时间点。其中
firebaseClassLoadTime记录的是FirebaseInitProvider (继承自ContentProvider)类的加载时间;PERF_CLASS_LOAD_TIME记录的是AppStartTrace 类的加载时间。
结束时间点是:第一个 Activity 的 onResume() 方法完成时。
03
PerfSuite-Android(Booking.com)
perfsuite-android 是一个轻量级 Android 性能监控库,提供:
-
App Cold Startup Time(应用冷启动时间)
-
Rendering performance per Activity(每个 Activity 的渲染性能)
-
Time to Interactive & Time to First Render(TTI 和 TTFR)
GitHub: https://github.com/bookingcom/perfsuite-android
其中冷启动的统计逻辑是:
-
起始时间:进程启动时间(Process.getStartUptimeMillis()),异常时回退到 ContentProvider 的 onCreate 时间。
-
结束时间:第一个 Activity 的第一帧绘制完成时(通过 封装的Activity.doOnFirstDraw方法检测)。
问:这里的Process.getStartUptimeMillis()和上面Firebase使用的Process.getStartElapsedRealtime() 有啥区别?
答:getStartUptimeMillis()排除了设备的深度睡眠时间(比如锁屏),getStartElapsedRealtime包含了设备的深度睡眠时间。
统计结束时间点:由自定义Activity.doOnFirstDraw 方法来记录,它是基于系统 API:ViewTreeObserver.OnDrawListener 和 androidx.core.view.doOnAttach来实现的。
这里我们再来回顾上述几个统计框架关键时间点的统计时序:
Activity.onResume()↓Activity.onWindowFocusChanged(true) ← Window 获得焦点↓ViewRootImpl.performTraversals() ← 开始绘制流程├─ measure()├─ layout()└─ draw()├─ View.draw()├─ ViewTreeObserver.dispatchOnDraw()│ └─ OnDrawListener.onDraw() ← 首帧绘制完成回调└─ finishDraw()└─ scheduleTraversals() → Choreographer└─ notifyWindowsDrawn() ← 通知 WMS 窗口绘制完成
除了冷启动时间外,PerfSuite还提供了TTI 和 TTFR两个指标。
|
指标 |
定义 |
起始时间 |
结束时间 |
|
TTFR |
首帧渲染时间 |
onScreenCreated() |
自定义方法View.doOnNextDraw 的回调执行时刻(即系统API ViewTreeObserver.OnDrawListener.onDraw() 被调用时) |
|
TTI |
可交互时间 |
onScreenCreated() |
可用内容绘制完成时,需要手动调用ViewTtiTracker.onScreenIsUsable(screen: String, rootView: View)告知框架。 |
其中起始时间onScreenCreated() 是PerfSuite SDK的自定义方法,对应的系统API为:
Activity:Application.ActivityLifecycleCallbacks.onActivityPreCreated()该API引入版本:API Level 29(Android 10),需要注意版本兼容。Fragment:FragmentManager.FragmentLifecycleCallbacks.onFragmentPreCreated()
这两个指标计算的完整流程为:
1. onScreenCreated() ← 自动调用(记录起始时间)↓2. onScreenViewIsReady() ← 自动调用(计算 TTFR)↓3. [应用逻辑:加载数据、显示内容...]↓4. onScreenIsUsable() ← 手动调用(计算 TTI)↓等待下一帧绘制完成↓listener.onFirstUsableFrameIsDrawn(screen, duration)
04
Android Cold Startup Instrumentation(OkCredit)
这是一个 Android 应用启动性能监控库,用于追踪应用的启动响应时间。监控冷启动、热启动、温启动的性能指标,包括从进程启动到首帧绘制的各阶段耗时(进程 fork、ContentProvider 初始化、Application.onCreate、Activity 创建到绘制等)。追踪应用状态信息,包括首次安装、更新、崩溃后的启动状态,以及 Activity 的 Referrer、Intent 等上下文信息。
GitHub: https://github.com/okcredit/android-cold-startup-instrumentation
冷启动耗时计算:
起始时间:进程 fork 时间(processForkTime)获取方式:
-
Android N+:使用 Process.getStartUptimeMillis()
-
低版本:从 /proc/[pid]/stat 读取
容错:如果读取到的进程fork时间与 ContentProvider 启动时间差超过 30 秒,回退到 contentProviderStartedTime
结束时间:第一帧绘制完成(firstDrawTime)
-
检测方式:通过 ViewTreeObserver.OnDrawListener 监听 onDraw() 回调
-
时机:在 Activity 的 decorView 第一次绘制时触发
04
总结
测量 Android 应用启动时间有多种方案,各有侧重:
-
Android StartUp:系统官方机制,提供基础指标
-
腾讯 Matrix:适合国内环境,功能全面
-
Firebase Performance Monitoring:云端监控,便于数据分析和追踪
-
PerfSuite-Android:开源方案,可定制
-
Android Cold Startup Instrumentation:专注冷启动,提供细粒度阶段划分
各方案在耗时统计的起始和结束时间节点上存在差异:
-
起始时间:多数从进程创建开始,部分从 Application.onCreate 开始
-
结束时间:差异更明显
a) 部分以第一帧绘制完成(ViewRootImpl.draw())为结束点,对应 TTID(Time To Initial Display)
b) 部分以窗口绘制完成(notifyWindowsDrawn())为结束点,对应 TTFD(Time To Fully Drawn)
c) 部分以 reportFullyDrawn() 显式调用的时刻为结束点
这些差异会导致同一应用的测量结果不同,选择时需注意其时间点定义,避免误读。
夜雨聆风
