关于android:App启动优化Google官方指导

75次阅读

共计 7579 个字符,预计需要花费 19 分钟才能阅读完成。

前言

== 本次次要内容包含:==

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。此时,用户能够开始应用利用。

如下图:利用冷启动的重要局部的可视示意

在创立利用和创立 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

留神:

这种形式在加载并显示所有资源之前,logcat 输入中的 Displayed 指标不肯定会捕捉工夫:它会省去布局文件中未援用的资源或利用作为对象初始化一部分创立的资源。它排除这些资源的起因是加载它们属于一个内嵌过程,并且不会阻止利用的初步显示。

能够应用 ADB Shell Activity Manager 命令运行利用来测量初步显示所用工夫。
以咱们我的项目为例:

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

这是优化前显示所用的工夫

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 启动时长如下:

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 的启动工夫?


关注我的技术公众号

正文完
 0