启动优化:App启动分类与流程

目录

  • app启动分类
  • app启动时间统计
  • app启动主要流程
  • 问题
    1:App冷启动时间到底是如何计算、触发的?
    2:如何实时统计App冷启动时间?

app启动分类

  • 冷启动
  • 热启动
  • 温启动
冷启动
  • 定义:通过Launcher点击应用图标或者在第三方启动一个目标app,如果“App所运行的进程未创建,则需要先通过Zygote进程fork子进程,然后再执行Application的创建、根Activity的创建、根视图树的解析、绘制等操作”。
  • 场景:
    • 设备启动以来首次启动应用程序
    • 应用的进程被系统kill掉了,进程没有缓存在主存中。点击应用图标需要重新走一遍app启动的整体流程。
  • 冷启动的事件组成:从整体看,冷启动过程主要分为“系统为启动app做准备”与“app进程启动完毕,继续后续的app进程内部操作”
    • 系统为启动app做准备
      • “加载并启动应用程序”:Launcher与SystemServer进程中的AMS通信过程。
      • “启动后立即显示应用程序的空白启动窗口”:AMS从Intent解析目标Activity,并根据Manifest注册的Activity生成ActivityRecord,把该ActivityRecord 添加到ActivityStack中。根据指定的Theme,显示其中定义的"预览窗口"。
      • “创建应用程序进程” :AMS如果检测到app运行在的进程未创建,则需要调用AMS.startProcessLocked()通知Zygite fork子进程。
    • app进程内部操作
      • 启动主线程:app进程创建完毕后,会执行ActivityThread的main()用于启动app进程内的主线程。
      • 创建Application:app进程创建完毕,会在main()中执行其attach(),经过层层调用会调用ActivityThread的bindApplication:会向ActivityThread的Handler中发送一个BIND_APPLICATION消息。等Handler接收到消息之后,会执行ActivityThread的handleBindApplication()实例化Application并执行相应生命周期方法。
      • 创建根Activity:等Application对象初始化完毕之后,会调用ASS.realStartActivityLocked(),该方法内部经过层层调用会向ActivityThread中的Handler发送一个LAUNCH_ACTIVITY消息,然后进行目标Activity初始化以及执行其相应生命周期方法。
      • 解析视图树:一般的我们会在onCreate()中通过setContentView()方式设置要显示的 视图布局。该视图布局通过LayoutInflate进行inflate()并添加到DecorView中。
      • 布局屏幕:等添加完视图树后,会在ActivityThread的handleResumeActivity()执行WindowManager的addView(),该操作用来通知运行在SystemServer进程的WMS服务,用来“添加Window中设置的DecorView”。等添加View完毕,会执行WM的updateViewLayout(),该方法经过层层调用会从WindowManagerGlobal中找到与目标View绑定的ViewRootImpl,然后通过ViewRootImpl内部的“编舞(信号处理程序)”来注册用于接收“Vsync信号”,等接收到此信号后会执行performTraversals(),该方法内部会执行整棵视图树的measure、layout、draw操作。
      • 执行初始绘制

冷启动流程图

启动流程.jpg

热启动
  • 定义:如果应用程序的进程仍然缓存在内存中,热启动只是 把后台进程切换至前台。
  • 优点:热启动比冷启动过程简单且开销低。
    具体的:应用从后台切换至前台的操作,因为app还没被kill掉,application以及相应Activity没有被destory掉,这样就不会重新执行Activity的创建、初始化以及视图树的解析、measure、layout、draw操作。

app启动时间统计

  • 通过系统log

    • app启动过程中,会通过logcat打印一个包含"Displayed"的log信息。
      该信息包含的是:从初始化Activity组件、执行onCreate/onStart()/onResume()等相应生命周期方法、Activity组件设置的视图树第一次绘制到屏幕上之后,这3步所花费的时间

    • 该log信息中必定会包含一个"Display Time",可能包含一个"Total Time"。


      1.png

      2.png
    • Display-Time

      • 如果,app运行的进程已存在,则表示“启动目标Activity并首次完成视图树的绘制” 所花费的时间。该时间具体统计的是:

        • Create and initialize the activity.
        • Inflate the layout.
        • Draw your application for the first time.
      • 如果,app运行的进程还未创建,则表示“从launcher点击app开始,到相应Activity的layout首次绘制完毕”,该时间就是“Total-Time”统计的时间。具体的该时间统计的是以下事件花费的时间:

        • Launch the process.
        • Initialize the objects.
        • Create and initialize the activity.
        • Inflate the layout.
        • Draw your application for the first time.
    • Total-Time

      • 含义:“app启动完毕所花费的总时间。该时间大于等于Display-Time”。
      • 统计的事件
        • Launch the process.
        • Initialize the objects.
        • Create and initialize the activity.
        • Inflate the layout.
        • Draw your application for the first time.**
      • 产生条件:
        1. 如果在app启动过程中,包括“另一个首先启动但未向屏幕显示任何内容的Activity()”,此时就会在log中打印出“Total-Time”。具体理解如下:

          1. 首先,启动的Activity不是最终要展示的Activity。
            好多应用的根Activity一般用于“闪屏显示”,等在这个页面显示一段广告或者其他内容之后才会跳转至“主页面”。
          2. 其次,无论此Activity是否通过setContentView()设置了要显示的视图树,只要“在一定时间内通过startActivity()跳转至app的“主页面”了(此处需要满足:在跳转之前,闪屏页的视图树还没来得及通过SurfaceFlinger显示在屏幕上)。
        2. 此时不会显示2行启动时间的log,只会显示一行关于“启动至主页”的时间log,且该log中会包含“Total-Time”。那么此时的“Total-Time”表示app启动完毕所花费的时间。 此时的“Display-Time”只是统计了:“主Activity创建以及第一次绘制完该Activity的视图树的时间”。

      • 如何让系统打印出此时间:
        就如“产生条件-1.2”中描述的那样 ,具体的可以:在onCreate()或者其他生命周期方法中启动那个最终要显示的Activity”,这样就会把“Total-Time”打印出来。
        3.jpg
  • 通过adb shell脚本启动app
    这种方式获取的时间与 第一种方式 从logcat中获取的时间一致。

    time2.png

  • Display/Total-Time是在哪统计的?
    具体的是通过ActivityRecord.reportLaunchTimeLocked()输出到logcat。

    • 源码:
private void reportLaunchTimeLocked(final long curTime) {
  //ActivityRecord记录的是“已经push到ActivityTask中的Activity信息”。
  //它是Activity栈中存储的数据的“基本元素”。
    final ActivityStack stack = getStack(); 
    if (stack == null) {
        return;
    }
    final long thisTime = curTime - displayStartTime;  
    final long totalTime = stack.mLaunchStartTime != 0 
 ? (curTime - stack.mLaunchStartTime) : thisTime;
    if (SHOW_ACTIVITY_START_TIME) {
        .......
        StringBuilder sb = service.mStringBuilder;
        sb.setLength(0);
        sb.append("Displayed ");
        sb.append(shortComponentName);
        sb.append(": ");
        TimeUtils.formatDuration(thisTime, sb);
        if (thisTime != totalTime) {
            sb.append(" (total ");
            TimeUtils.formatDuration(totalTime, sb);
            sb.append(")");
        }
        Log.i(TAG, sb.toString());
    }
    .......
}
  • 参数说明:
    • curTime:传入的,用于标识“何时调用的该方法”。
    • displayStartTime:用于记录“启动当前Activity的时间点”。该变量会在ActivityTask.setLaunchTime()被赋值,而setLaunchTime()方法又是在ASS调用startSpecificActivitylocked()用于实际启动一个目标Activity内部被执行的。
    • stack.mLaunchStartTime:Activity栈中通过mLaunchStartTime记录根Activity的启动时间。
    • thisTime:表示“当前Activity从启动到第一帧绘制完毕的时间”。
    • totalTime:表示“app从创建进程至目标Activity启动到绘制完第一帧的时间”。
      该时间就是“目标app的冷启动时间”。
    • waitTime:表示总耗时(包括系统耗时+app启动总耗时),是从“Launcher点击应用图标开始,到App启动结束”这段时间内的耗时。
总结:

1:thisTime:表示“当前Activity从启动到第一帧绘制完毕的时间”
2:totalTime:表示“app从创建进程至栈顶Activity启动到绘制完第一帧的时间”。该时间就是“目标app的冷启动时间”。
3:如果启动的Activity不是栈顶Activity,且启动的Activity没有要显示的视图树,那么thisTime统计的时长是不等于totalTime统计的时长的。
3.1:此种情况下,thisTime只是统计的是“最后一个Activity也就是栈顶Activity”的启动到绘制完第一帧的时长。
3.2:如果最终启动的Activity就是栈顶Activity,也就是说“启动的是根Activity”此种情况下thisTime统计的时长与totalTime一致。

4:waitTime:表示“总耗时”,其中包括 系统创建app进程前的操作耗时+app启动总耗时。
5:waitTime、totalTime、thisTime 3者时长关系:waitTime>totalTime>=thisTime。
6:displat-time:logcat中输出的displatTime就是thisTime。
7:total-time:logcat中输出的totle就是totlaTime。


App启动主要流程(详细介绍请戳这里)

app启动流程总体上分为:
  • 系统为启动app做准备(主要是Launcher进程与SystemServer进程交互):

    1. 通过Intent解析待启动的根Activity组件。
    2. 创建一个新的Activity栈(TaskRecord)并把与该Activity对应的ActivityRecord压栈。
    3. 获得Activity组件中声明的theme并显示“预览窗口”。
  • 创建App进程:

    1. AMS接收到Launcher发送的请求后,检测到app进程未创建。通过AMS.startProcessLocked()先向Zygote进程发送Socket请求创建子进程(IPC:SystemServer进程与Zygote进程通信)。
    2. Zygote接收到AMS发送的请求后,fork子进程、创建Binder线程池、初始化虚拟机。
  • App进程内部操作:

    1. 启动主线程:Zygote进程启动完app进程后,App进程内部会执行ActivityThread的main()用来启动主线程、做一些初始化操作(IPC:Zygote进程与SystemServer通信用于fork子进程。main()中主要是做的ActivityThread的初始化操作,其中包括初始化ApplicationThread类型的属性“mAppThread”)。
    2. 创建Application:在main()内部初始化ActivityThread之后会执行其attach(),该方法内部经过层层调用最终会执行ActivityThread.bindApplication(),该方法会向ActivitThread中的H类型的Handler中发送一个BIND_APPLICATION消息,在接收到该消息后会,创建ContextImpl实例、实例化Application并执行Application的attach(),onCreate()等方法。
    3. 创建根Activity:执行ASS.realStartActivityLocked(),其内部会调用app进程的ApplicationThread类中的scheduleLaunchActivity()创建、启动Application(Application在第4步已经创建了,则此方法中不会再创建进而也不会执行Application的生命周期方法)、Activity,并执行相应生命周期方法。
    4. 添加以及绘制视图树到Window:app进程通过WindowManager的addView()通知AMS添加Window,并通过WindowManager的updateViewLayout()执行View树的meausre、layout、draw等操作。(addView()操作涉及到IPC,client端是app进程,SystemServer进程是服务端)。

问题

1:App冷启动时间到底是如何计算、触发的?

参考:

https://developer.android.com/topic/performance/vitals/launch-time
https://blog.csdn.net/qian520ao/article/details/81908505
https://www.androidperformance.com/2015/12/31/How-to-calculation-android-app-lunch-time/