天天看点

Android 启动时长分析

应用启动介绍

我们平时在写应用的时候,一般会指定一个 ​

​MainActivity​

​​, 用户在桌面上点击这个 Activity 的时候,系统会直接起这个 Activity. 我们知道 Activity 在启动的时候会走 ​

​onCreate/onStart/onResume​

​. 这几个回调函数.

许多书里讲过,当执行完 onResume 函数之后,应用就显示出来了…其实这是一种不准确的说法,因为从系统层面来看,一个 Activity 走完 ​

​onCreate/onStart/onResume​

​ 这几个生命周期之后, 只是完成了应用自身的一些配置, 比如 ​

​window​

​​ 的一些属性的设置 ​

​View​

​​ 树的建立(只是建立,并没有显示,也就是说只是调用了 ​

​inflate​

​ 而已) . 后面 ViewRootImpl 还会调用两次performTraversals ,初始化 Egl 以及 measure/layout/draw. 等.

所以我们定义一个 Android 应用的启动时间, 肯定不能在 Activity 的回调函数上下手.而是以用户在手机屏幕上看到你在 onCreate 的 setContentView 中设置的 layout 完全显示为准,也就是我们常说的应用第一帧.

1. 启动的类型

参考自 – ​​Android 开发之 App 启动时间统计​​

  • 冷启动 ------- 当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化 Application 类,再创建和初始化 MainActivity 类,最后显示在界面上。
  • 热启动 ------- 当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。热启动因为会从已有的进程中来启动,所以热启动就不会走 Application 这步了,而是直接走 MainActivity,所以热启动的过程不必创建和初始化 Application,因为一个应用从新进程的创建到进程的销毁,Application 只会初始化一次。
  • 首次启动 ------- 首次启动严格来说也是冷启动,之所以把首次启动单独列出来,一般来说,首次启动时间会比非首次启动要久,首次启动会做一些系统初始化工作,如缓存目录的生产,数据库的建立,SharedPreference的初始化,如果存在多 dex 和插件的情况下,首次启动会有一些特殊需要处理的逻辑,而且对启动速度有很大的影响,所以首次启动的速度非常重要,毕竟影响用户对 App 的第一映像。

2. 统计启动时间

adb 统计及其参数介绍

具体细节请参考: ​​https://www.androidperformance.com/2015/12/31/How-to-calculation-android-app-lunch-time/​​

adb shell am start -w pageage/activityname  
//通过这条命令启动,可以获得启动时间。      
$ adb shell am start -W com.abc.test/com.abc.test.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=
Status: ok
Activity: com.speed.test/.HomeActivity
ThisTime: 496
TotalTime: 496
WaitTime: 503
Complete      

​adb shell am start -W​

​​ 的实现在 ​

​frameworks\base\cmds\am\src\com\android\commands\am\Am.java​

​​文件中。其实就是跨 ​

​Binder​

​​ 调用 ​

​ActivityManagerService.startActivityAndWait()​

​​ 接口(后面将​

​ActivityManagerService​

​​ 简称为 ​

​AMS​

​​),这个接口返回的结果包含上面打印的 ​

​ThisTime​

​​、​

​TotalTime​

​ 时间.

  • ​startTime​

    ​​ 记录的刚准备调用 ​

    ​startActivityAndWait()​

    ​的时间点
  • ​endTime​

    ​​ 记录的是 ​

    ​startActivityAndWait()​

    ​ 函数调用返回的时间点
  • ​WaitTime = startActivityAndWait()​

    ​ 调用耗时。
  • ​WaitTime​

    ​ 就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;
  • ​ThisTime​

    ​ 表示一连串启动 Activity 的最后一个 Activity 的启动耗时;
  • ​TotalTime​

    ​ 表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用 Activity pause 的耗时。也就是说,开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。

总结下:

  • 如果只关心某个应用自身启动耗时,参考 ​

    ​TotalTime​

    ​;
  • 如果关心系统启动应用耗时,参考 ​

    ​WaitTime​

    ​;
  • 如果关心应用界面 ​

    ​Activity​

    ​ 启动耗时,参考 ​

    ​ThisTime​

    ​。

3. 起始时间点

  • 冷启动启动时间一般可以在​

    ​Application.attachBaseContext()​

    ​ 开始的位置记录起始时间点,因为在这之前 Context 还没有初始化,一般也干不了什么事情,当然这个是要视具体情况来定,其实只要保证在 App 的具体业务逻辑开始执行之前记录起始时间点即可。
  • 热启动启动时间点可以在​

    ​Activity.onRestart()​

    ​ 中记录起始时间点。

4. 结束时间点

结束时间点理论上要选在 App 显示出第一屏界面的时候,但是在什么位置 App 显示出第一屏界面呢?网上很多文章说在 Activity 的 onResume 方法执行完成之后,Activity 就对用户可见了,实际上并不是,一个 Activity 走完onCreate onStart onResume 这几个生命周期之后,只是完成了应用自身的一些配置,比如 Activity 主题设置 window 属性的设置 View 树的建立,但是其实后面还需要各个 View 执行 measure layout draw等。所以在 OnResume 中记录结束时间点的 Log 并不准确,大家可以注意一下上面流程中最后一个函数 ​

​Activity.onWindowFocusChanged​

​,下面是它的注释:

/**
*Called when the current {@link Window} of the activity gains or loses
* focus.  This is the best indicator of whether this activity is visible
* to the user.  The default implementation clears the key tracking
* state, so should always be called.
...
*/      

通过注释我们可以看到,这个函数是判断 ​

​activity​

​ 是否可见的最佳位置,所以我们可以在 ​

​Activity.onWindowFocusChanged​

​ 记录应用启动的结束时间点,不过需要注意的是 该函数在 ​

​Activity​

​ 焦点发生变化时就会触发,所以要做好判断,去掉不需要的情况。

5. 应用的主要启动流程

  • 通过​

    ​Launcher​

    ​ 启动应用时,点击应用图标后,​

    ​Launcher​

    ​ 调用 ​

    ​startActivity​

    ​ 启动应用。

    ​Launcher Activity​

    ​ 最终调用 ​

    ​Instrumentation​

    ​ 的 ​

    ​execStartActivity​

    ​ 来启动应用。
  • ​Instrumentation​

    ​​ 调用 ​

    ​ActivityManagerProxy​

    ​ (​

    ​ActivityManagerService​

    ​ 在应用进程的一个代理对象) 对象的 ​

    ​startActivity​

    ​ 方法启动 ​

    ​Activity​

    ​。
  • 到目前为止所有过程都在​

    ​Launcher​

    ​ 进程里面执行,接下来 ​

    ​ActivityManagerProxy​

    ​ 对象跨进程调用 ​

    ​ActivityManagerService​

    ​ (运行在 ​

    ​system_server​

    ​ 进程)的 ​

    ​startActivity​

    ​ 方法启动应用。
  • ​ActivityManagerService​

    ​​ 的 ​

    ​startActivity​

    ​ 方法经过一系列调用,最后调用 ​

    ​zygoteSendArgsAndGetResult​

    ​ 通过 ​

    ​socket​

    ​ 发送给 ​

    ​zygote​

    ​ 进程,​

    ​zygote​

    ​ 进程会孵化出新的应用进程。
  • ​zygote​

    ​​ 进程孵化出新的应用进程后,会执行 ​

    ​ActivityThread​

    ​ 类的 ​

    ​main​

    ​ 方法。在该方法里会先准备好 ​

    ​Looper​

    ​ 和消息队列,然后调用 ​

    ​attach​

    ​ 方法将应用进程绑定到 ​

    ​ActivityManagerService​

    ​,然后进入 ​

    ​loop​

    ​ 循环,不断地读取消息队列里的消息,并分发消息。
  • ​ActivityManagerService​

    ​​ 保存应用进程的一个代理对象,然后 ​

    ​ActivityManagerService​

    ​ 通过代理对象通知应用进程创建入口 ​

    ​Activity​

    ​ 的实例,并执行它的生命周期函数。

总结过程就是:

  1. 用户在​

    ​Launcher​

    ​​ 程序里点击应用图标时,会通知​

    ​AMS​

    ​​ 启动应用的入口​

    ​Activity​

  2. ​AMS​

    ​​ 发现这个应用还未启动,则会通知​

    ​Zygote​

    ​​ 进程孵化出应用进程,然后在这个应用进程里执行​

    ​ActivityThread​

    ​​ 的​

    ​main​

    ​ 方法
  3. 应用进程接下来通知​

    ​AMS​

    ​​应用进程已启动,​

    ​AMS​

    ​​保存应用进程的一个代理对象,这样​

    ​AMS​

    ​可以通过这个代理对象控制应用进程
  4. 然后​

    ​AMS​

    ​​ 通知应用进程创建入口​

    ​Activity​

    ​ 的实例,并执行它的生命周期函数。

6. 冷启动的流程

-> Application 构造函数
-> Application.attachBaseContext()
-> Application.onCreate()
-> Activity 构造函数
-> Activity.setTheme()
-> Activity.onCreate()
-> Activity.onStart
-> Activity.onResume
-> Activity.onAttachedToWindow
-> Activity.onWindowFocusChanged      

7. Android 启动优化

应用在冷启动之前,要执行三个任务:

  • 加载启动App;
  • App启动之后立即展示出一个空白的Window;
  • 创建App的进程;

而这三个任务执行完毕之后会马上执行以下任务:

  • 创建App对象;
  • 启动Main Thread;
  • 创建启动的Activity对象;
  • 加载View;
  • 布置屏幕;
  • 进行第一次绘制;

而一旦App进程完成了第一次绘制,系统进程就会用 ​

​MainActivity​

​​ 替换已经展示的 ​

​Background Window​

​​,此时用户就可以使用 ​

​App​

​了。

Android 启动时长分析

作为普通应用,​

​App​

​ 进程的创建等环节我们是无法主动控制的,可以优化的也就是 ​

​Application​

​、​

​Activity​

​ 创建以及回调等过程。

同样,Google也给出了启动加速的方向:

  • 利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;
  • 避免在启动时做密集沉重的初始化(Heavy app initialization)
  • 定位问题:避免I/O操作、反序列化、网络操作、布局嵌套等。
  • 利用主题快速显示界面;
  • 异步初始化组
  • 梳理业务逻辑,延迟初始化组件、操作
  • 正确使用线程
  • 去掉无用代码、重复逻辑等

参考链接

  • ​​知乎 – 怎么计算apk的启动时间​​
  • ​​Android 中如何计算 App 的启动时间?​​
  • ​​google 官方文档 – App startup time​​
  • ​​Android性能优化(一)之启动加速35%​​