App启动优化-Google官方指导

heiyl

前言

==本次主要内容包括:==

1、App的启动方式

2、启动过程分析以及优化方案

3、启动耗时统计

一、App的启动方式

谷歌官方文档

App启动有三种状态,每种状态都会影响App对用户可感知的时间:冷启动,热启动和温启动。

在冷启动中,应用从头开始启动。在其他状态下,系统需要将后台运行中的应用带入前台。建议您始终在假定冷启动的基础上进行优化。这样做也可以提升温启动和热启动的性能。

要优化应用以实现快速启动,了解系统和应用层面的情况以及它们在各个状态中的互动方式很有帮助。

1、冷启动

冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动。这种启动给最大限度地减少启动时间带来了最大的挑战,因为系统和应用要做的工作比在其他启动状态下更多。

特点:

冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化启动(SplishActivity)类(包括一系列的测量、布局、绘制),最后显示在界面上。

2、热启动

应用的热启动比冷启动简单得多,开销也更低。在热启动中,系统的所有工作就是将您的 Activity 带到前台。如果应用的所有 Activity 都还驻留在内存中,则应用可以无须重复对象初始化、布局扩充和呈现。

但是,如果一些内存为响应内存整理事件(如 onTrimMemory())而被完全清除,将需要重新创建这些对象,以响应热启动事件。

热启动显示的屏幕上行为和冷启动场景相同:系统进程显示空白屏幕,直到应用完成 Activity 呈现。

特点:

热启动因为会从已有的进程中来启动,所以热启动就不会走Application这步了,而是直接走MainActivity(包括一系列的测量、布局、绘制),所以热启动的过程只需要创建和初始化一个启动类(SplishActivity)就行了,而不必创建和初始化Application

3、温启动

温启动涵盖在冷启动期间发生的操作的一些子集;同时,它的开销比热启动多。有许多潜在状态可视为温启动。例如:

  • 用户退出您的应用,但之后又重新启动。进程可能已继续运行,但应用必须通过调用 onCreate() 从头开始重新创建 Activity。
  • 系统将您的应用从内存中逐出,然后用户又重新启动它。进程和 Activity 需要重启,但传递到 onCreate() 的已保存实例状态包对于完成此任务有一定助益。

二、启动过程分析以及优化方案

冷启动包含了整个启动流程所有环节,需要创建App进程, 加载相关资源, 启动Main Thread, 初始化首屏Activity等.

在冷启动开始时,系统有三个任务。这三个任务是:

  1. 加载并启动应用。
  2. 在启动后立即显示应用的空白启动窗口。
  3. 创建应用进程。

系统一创建应用进程,应用进程就负责后续阶段:

  1. 创建应用对象。
  2. 启动主线程。
  3. 创建主 Activity。
  4. 扩充视图。
  5. 布局屏幕。
  6. 执行初始绘制。

一旦应用进程完成第一次绘制,系统进程就会换掉当前显示的后台窗口,替换为主 Activity。此时,用户可以开始使用应用。

如下图:应用冷启动的重要部分的可视表示

image

在创建应用和创建 Activity 的过程中可能会出现性能问题。

Application用创建

当应用启动时,空白启动窗口将保留在屏幕上,直到系统首次完成应用绘制。完成后,系统进程会换掉应用的启动窗口,允许用户开始与应用互动。

如果您在自己的应用中使 Application.onCreate() 过载,系统将在应用对象上调用 onCreate() 方法。之后,应用衍生主线程,也称为界面线程,让其执行创建主 Activity 的任务。

从此时开始,系统级和应用级进程根据应用生命周期阶段继续运行。

Activity 创建

在应用进程创建 Activity 后,Activity 将执行以下操作:

  1. 初始化值。
  2. 调用构造函数。
  3. 根据 Activity 的当前生命周期状态,相应地调用回调方法,如 Activity.onCreate()。

通常,onCreate() 方法对加载时间的影响最大,因为它执行工作的开销最高:加载和扩充视图,以及初始化运行 Activity 所需的对象。

含主题背景的启动屏幕

您可能希望为应用的加载体验设置主题背景,从而使应用的启动屏幕在主题背景上与应用的其余部分保持一致,而不是与系统主题背景一致。这样做可以隐藏缓慢的 Activity 启动。

实现含主题背景的启动屏幕的常见方式是使用 windowDisablePreview 主题背景属性关闭启用应用时系统进程绘制的初始空白屏幕。但是,此方法可能导致启动时间比不抑制预览窗口的应用更长。此外,它还迫使用户在没有反馈的情况下等待 Activity 启动完成,使其不确定应用是否正常运行。

提供的解决方案:可以使用 Activity 的 windowBackground 主题背景属性,为启动 Activity 提供简单的自定义可绘制资源。

例如,您可能创建新的可绘制文件,并从布局 XML 和应用清单文件中引用它,如下所示:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
      <!-- The background color, preferably the same as your normal theme -->
      <item android:drawable="@android:color/white"/>
      <!-- Your product logo - 144dp color version of your app icon -->
      <item>
        <bitmap
          android:src="@drawable/product_logo_144dp"
          android:gravity="center"/>
      </item>
    </layer-list>

清单文件:

<activity ...
    android:theme="@style/AppTheme.Launcher" />

为什么出现白屏

冷启动白屏持续时间可能会很长,它的启动速度是由于以下引起的:

1、Application的onCreate流程,对于APP来说,通常会在这里做大量的通用组件的初始化操作;
建议:很多第三方SDK都放在Application初始化,我们可以放到用到的地方才进行初始化操作。

2、Activity的onCreate流程,特别是UI的布局与渲染操作,如果布局过于复杂很可能导致严重的启动性能问题;
建议:Activity仅初始化那些立即需要的对象,xml布局减少冗余或嵌套布局。

优化APP启动速度意义重大,启动时间过长,可能会使用户直接卸载APP。

总结:

关于启动加速方案,Google给出的建议是:

1.利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;

2.避免在启动时做密集沉重的初始化;

3.定位问题:避免I/O操作、反序列化、网络操作、布局嵌套等。

三、启动耗时统计

要正确诊断启动时间性能,您可以跟踪一些显示应用启动所需时间的指标。

统计App启动初步显示时间

在 Android 4.4(API 级别 19)及更高版本中,logcat 包括一个输出行,其中包含名为 Displayed 的值。此值代表从启动进程到在屏幕上完成对应 Activity 绘制所经过的时间。经过的时间包括以下事件序列:

  1. 启动进程。
  2. 初始化对象。
  3. 创建并初始化 Activity。
  4. 扩充布局。
  5. 首次绘制应用。

项目中日志行类似于以下示例:

2019-12-21 19:20:53.327 1458-1552/? I/ActivityManager: Displayed com.pxwx.student/.ui.activity.SplashActivity: +281ms

image

注意:

这种方式在加载并显示所有资源之前,logcat 输出中的 Displayed 指标不一定会捕获时间:它会省去布局文件中未引用的资源或应用作为对象初始化一部分创建的资源。它排除这些资源的原因是加载它们属于一个内嵌进程,并且不会阻止应用的初步显示。

可以使用 ADB Shell Activity Manager 命令运行应用来测量初步显示所用时间。
以我们项目为例:

adb shell am start -S -W com.pxwx.student/.ui.activity.SplashActivity

这是优化前显示所用的时间

image

adb shell am start -S -W com.pxwx.student/.ui.activity.SplashActivity
Stopping: com.pxwx.student
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.pxwx.student/.ui.activity.SplashActivity }
Status: ok
Activity: com.pxwx.student/.ui.activity.SplashActivity
ThisTime: 2298
TotalTime: 2298
WaitTime: 2359
Complete
  • ==ThisTime== 表示一连串启动 Activity 的最后一个 Activity 的启动耗时,一般和TotalTime时间一样,除非在应用启动时开了一个透明的Activity预先处理一些事再显示出主Activity,这样将比TotalTime小。
  • ==TotalTime==:应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示
  • ==WaitTime== 返回从 startActivity 到应用第一帧完全显示这段时间. 就是总的耗时,一般比TotalTime大点,包括系统影响的耗时

所以我们只需要以TotalTime为准就可以。

从上面看出优化前app启动时长约为2.3s

对比一下优化后app启动时长如下:

image

C:\Users\heiyulong>adb shell am start -S -W com.pxwx.student/.ui.activity.SplashActivity
Stopping: com.pxwx.student
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.pxwx.student/.ui.activity.SplashActivity }
Status: ok
Activity: com.pxwx.student/.ui.activity.SplashActivity
ThisTime: 858
TotalTime: 858
WaitTime: 981
Complete

优化后TotalTime为858ms不到1s,有了大幅的提升,启动优化提升60%。

App启动时间分析

以6.0.1源码看

命令“adb shell am start -S -W” 的实现是在"frameworks\base\cmds\am\src\com\android\commands\am\Am.java"文件中

am脚本

adb shell am命令会执行am脚本:

\frameworks\base\cmds\am\am

#!/system/bin/sh
#
# Script to start "am" on the device, which has a very rudimentary
# shell.
#
base=/system
export CLASSPATH=$base/framework/am.jar
exec app_process $base/bin com.android.commands.am.Am "$@"

脚本会调用\frameworks\base\cmds\am\src\com\android\commands\am\Am.java的main方法:

public class Am extends BaseCommand {
    private IActivityManager mAm;

    /**
     * Command-line entry point.
     *
     * @param args The command-line arguments
     */
    public static void main(String[] args) {
        (new Am()).run(args);
    }

    @Override
    public void onRun() throws Exception {

        mAm = ActivityManagerNative.getDefault();
        if (mAm == null) {
            System.err.println(NO_SYSTEM_ERROR_CODE);
            throw new AndroidException("Can't connect to activity manager; is the system running?");
        }

        String op = nextArgRequired();

        if (op.equals("start")) {//走这里 start
            runStart();
        } else if (op.equals("startservice")) {
            runStartService();
        } else if (op.equals("stopservice")) {
            runStopService();
        } else if (op.equals("force-stop")) {
            runForceStop();
        } else if (op.equals("kill")) {
            runKill();
            //...
        } else {
            showError("Error: unknown command '" + op + "'");
        }
    }

    private void runStart() throws Exception {
        //...
        IActivityManager.WaitResult result = null;
            int res;
            final long startTime = SystemClock.uptimeMillis();
            if (mWaitOption) {
                result = mAm.startActivityAndWait(null, null, intent, mimeType,
                            null, null, 0, mStartFlags, profilerInfo, null, mUserId);
                res = result.result;
            } else {
                res = mAm.startActivityAsUser(null, null, intent, mimeType,
                        null, null, 0, mStartFlags, profilerInfo, null, mUserId);
            }
            final long endTime = SystemClock.uptimeMillis();
            //...
             if (mWaitOption && launched) {
                if (result == null) {
                    result = new IActivityManager.WaitResult();
                    result.who = intent.getComponent();
                }
                System.out.println("Status: " + (result.timeout ? "timeout" : "ok"));
                if (result.who != null) {
                    System.out.println("Activity: " + result.who.flattenToShortString());
                }
                //下面代码得知result中包含thisTime、totalTime、WaitTime
                if (result.thisTime >= 0) {
                    System.out.println("ThisTime: " + result.thisTime);
                }
                if (result.totalTime >= 0) {
                    System.out.println("TotalTime: " + result.totalTime);
                }
                System.out.println("WaitTime: " + (endTime-startTime));
                System.out.println("Complete");
            }
    }

发现最终跨Binder调用ActivityManagerService.startActivityAndWait() 接口

\frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java

public final class ActivityManagerService extends ActivityManagerNative
        implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
    @Override
    public final WaitResult startActivityAndWait(IApplicationThread caller, String callingPackage,
            Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
            int startFlags, ProfilerInfo profilerInfo, Bundle options, int userId) {
        enforceNotIsolatedCaller("startActivityAndWait");
        userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId,
                false, ALLOW_FULL_ONLY, "startActivityAndWait", null);
        WaitResult res = new WaitResult();
        // TODO: Switch to user app stacks here.
        mStackSupervisor.startActivityMayWait(caller, -1, callingPackage, intent, resolvedType,
                null, null, resultTo, resultWho, requestCode, startFlags, profilerInfo, res, null,
                options, false, userId, null, null);
        return res;
    }
}

这个接口返回的结果包含上面打印的ThisTime、TotalTime时间.

可参考:
Android 中如何计算 App 的启动时间?


关注我的技术公众号

image

阅读 122
0 声望
1 粉丝
0 条评论
0 声望
1 粉丝
宣传栏