乐于分享
好东西不私藏

我们应该如何测量Android APP的启动时间

我们应该如何测量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)的判断条件是:

// ActivityMetricsLogger            if (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 time   if (processStartTime != null) {     return processStartTime;   }   // Fallback: static initializer time (during class-loadofaFirebaseclass   return getClassLoadTimeCompat(); } private @NonNull Timer getClassLoadTimeCompat() {   // Prefered: static-initializer time of the 1st Firebase classduringinit   if (firebaseClassLoadTime != null) {     return firebaseClassLoadTime;   }   // Fallback: static-initializer time of the current class   return 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() 显式调用的时刻为结束点

    这些差异会导致同一应用的测量结果不同,选择时需注意其时间点定义,避免误读。

    本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 我们应该如何测量Android APP的启动时间

    评论 抢沙发

    2 + 6 =
    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址
    ×
    订阅图标按钮