1.概述

手机点击一个APP,用户希望应用能够及时响应并快速加载。启动时间过长的应用不能满足这个期望,并且可能会令用户失望。这种糟糕的体验可能会导致用户在 Play 商店针对您的应用给出很低的评分,甚至完全弃用您的应用。

本篇就来将帮助您分析和优化应用的启动时间;首先介绍启动过程的内部机制;然后,讨论如何剖析启动性能(检测启动时间以及分析工具),最后给出通用启动优化方案;

2.了解应用启动内部机制

应用有三种启动状态,每种状态都会影响应用向用户显示所需的时间:冷启动、温启动或热启动。在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台。建议您始终在假定冷启动的基础上进行优化。这样做也可以提升温启动和热启动的性能。

  • 冷启动  冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动。这种启动给最大限度地减少启动时间带来了最大的挑战,因为系统和应用要做的工作比在另外两种启动状态中更多。
  • 热启动 应用的热启动比冷启动简单得多,开销也更低。在热启动中,系统的所有工作就是将您的 Activity 带到前台。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局膨胀和呈现。例如,按home键到桌面,然后又点图标启动应用。
  • 温启动 温启动包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。例如:用户按返回键退出应用后又重新启动应用。这时进程已在运行,但应用必须通过调用 onCreate() 从头开始重新创建 Activity。

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

冷启动开始时,系统有三个任务,它们是:

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

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

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

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

显示系统进程和应用进程之间如何交接工作如下图:

上图实际上对启动流程的简要概括; 

3.优化核心思想

问题来了,启动优化是对 启动流程的那些步骤进行优化呢?

这是一个好问题。我们知道,用户关心的是:点击桌面图标后 要尽快的显示第一个页面,并且能够进行交互。 根据启动流程的分析,显示页面能和用户交互,这是主线程做的事情。那么就要求 我们不能再主线程做耗时的操作。启动中的系统任务我们无法干预能干预的就是在创建应用和创建 Activity 的过程中可能会出现的性能问题。这一过程具体就是:

  • Application的attachBaseContext
  • Application的onCreate
  • activity的onCreate
  • activity的onStart
  • activity的onResume

activity的onResume方法完成后才开始首帧的绘制。所以这些方法中的耗时操作我们是要极力避免的。

并且,通常情况下,一个应用的主页的数据是需要进行网络请求的,那么用户启动应用是希望快速进入主页以及看到主页数据,这也是我们计算启动结束时间的一个依据。

4.时间检测

4.1Displayed

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

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

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

我们APP启动报告的日志打印似于以下示例:

2021-12-05 18:30:21.559 7678-7678/com.XXX.XXX D/GOA_APP:: onResume begin
2021-12-05 18:30:21.559 7678-7678/com.XXX.XXX D/GOA_APP:: onResume 显示第一帧
2021-12-05 18:30:21.559 7678-7678/com.XXX.XXX D/GOA_APP:: onResume end

2021-12-05 18:30:23.932 1558-1637/? D/SmartisanLaunch: cold launch: package:com.XXX.XXX  activity:com.XXX.XXX/.ui.activity.LauncherActivity   start_time:931490   end_time:941368   duration:9878ms
2021-12-05 18:30:23.932 1558-1637/? I/ActivityManager: Displayed com.XXX.XXX/.ui.activity.LauncherActivity: +9s809ms

“Displayed”的时间打印是在添加window之后,而添加window是在onResume方法之后;冷启动(cold launch)时间记录及消耗总时间;

如果您从命令行或在终端中跟踪 logcat 输出,查找经过的时间很简单。要在 Android Studio 中查找经过的时间,必须在 logcat 视图中停用过滤器。停用过滤器是必要的,因为提供此日志的是系统服务器,不是应用本身。

一旦进行了正确的设置,即可轻松搜索正确术语来查看时间。下图展示了一个 logcat 输出示例,其中显示了如何停用过滤器,并且在输出内容的倒数第二行中显示了 Displayed 时间;

也可以查看其它页面或者APP启动时间,例如今日头条

2021-12-06 10:31:24.366 1559-1602/? I/ActivityManager: Displayed com.ss.android.article.news/.activity.MainActivity: +2s199ms

4.2adb shell

您也可以使用 ADB Shell Activity Manager 命令运行应用来测量初步显示所用时间:

adb [-d|-e|-s <serialNumber>] shell am start -S -W[ApplicationId]/[根Activity的全路径] -c android.intent.category.LAUNCHER-a android.intent.action.MAIN
当ApplicationId和package相同时,根Activity全路径可以省略前面的packageName。

adb命令使用参考:Android 调试桥 (adb)

Displayed 指标和以前一样出现在 logcat 输出中。您的终端窗口还应显示以下内容:

2021-12-05 19:02:46.937 1558-1637/? I/ActivityManager: Displayed com.XXX.XXX/.ui.activity.LauncherActivity: +1s827ms

您的终端窗口在adb命令执行后还应显示以下内容:

C:\Users\dongdawei1>adb shell am start -W com.XXX.XXX/.ui.activity.LauncherActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.XXX.XXX/.ui.activity.LauncherActivity }
Status: ok
Activity: com.XXX.XXX/.ui.activity.LauncherActivity
ThisTime: 1827
TotalTime: 1827
WaitTime: 1859
Complete

我们关注TotalTime即可,即应用的启动时间,包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程;

4.3reportFullyDrawn()

您可以使用 reportFullyDrawn()  (API19及以上)方法测量从应用启动到完全显示所有资源和视图层次结构所用的时间。在应用执行延迟加载时,此数据会很有用。在延迟加载中,应用不会阻止窗口的初步绘制,但会异步加载资源并更新视图层次结构。

如果由于延迟加载,应用的初步显示不包括所有资源,您可能会将完全加载和显示所有资源及视图视为单独的指标:例如,您的界面可能已完全加载,并绘制了一些文本,但尚未显示应用必须从网络中获取的图片。 要解决此问题,您可以手动调用 reportFullyDrawn(),让系统知道您的 Activity 已完成延迟加载。当您使用此方法时,logcat 显示的值为从创建应用对象到调用 reportFullyDrawn() 时所用的时间。以下是 logcat 输出的示例:

Activity@Overrideprotected void onResume() {super.onResume();new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}runOnUiThread(new Runnable() {@Overridepublic void run() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {reportFullyDrawn();}}});}}).start();}

使用子线程睡1秒来模拟数据加载,然后调用reportFullyDrawn(),以下是 logcat 的输出;

2021-12-05 19:11:05.045 1558-1637/? I/ActivityManager: Displayed com.XXX.XXX/.ui.activity.LauncherActivity: +1s842ms
2021-12-05 19:11:05.824 1558-2747/? I/ActivityManager: Fully drawn com.XXX.XXX/.ui.activity.LauncherActivity: +2s622ms

4.4代码打点

写一个打点工具类,开始结束时分别记录,把时间上报到服务器;

此方法可带到线上,但代码有侵入性;

开始记录的位置放在Application的attachBaseContext方法中,attachBaseContext是我们应用能接收到的最早一个生命周期回调方法;

计算启动结束时间的两种方式

  • 一种是在 onWindowFocusChanged 方法中计算启动耗时。 onWindowFocusChanged 方法只是 Activity 的首帧时间,是 Activity 首次进行绘制的时间,首帧时间和界面完整展示出来还有一段时间差,不能真正代表界面已经展现出来了;(onWindowFocusChanged():当Activity的当前Window获得或失去焦点时会被回调此方法;当回调了这个方法时表示Activity是完全对用户可见的(只是可见,还一片黑呼呼的,有待draw..);当对话框弹起/消失及Activity新创建及回退等都会调用此方法;)
//APP启动开始时间
public class GZszApplication extends BaseApplication {public static long AppStartTime = -1;@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base);AppStartTime = System.currentTimeMillis();}
}
//APP启动到第一帧时间
public class LauncherActivity extends Activity implements ViewTreeObserver.OnWindowFocusChangeListener{public void onWindowFocusChanged(boolean hasFocus){MethodUtils.i("启动开始结束时间"+(System.currentTimeMillis()-GZszApplication.AppStartTime));}
}

输出日志

//第一帧显示的时候

2021-12-05 19:33:12.111 15759-15759/com.XXX.XXX I/GOA_APP:: 启动开始结束时间1853

//再次焦点变化时日志,通过上面日志发现第一帧显示的时候的时间不是Activity完全显示的时间

2021-12-05 19:33:15.053 15759-15759/com.XXX.XXX I/GOA_APP:: 启动开始结束时间4795

  • 按首帧时间计算启动耗时并不准确,我们要的是用户真正看到我们界面的时间。 正确的计算启动耗时的时机是要等真实的数据展示出来,比如在列表第一项的展示时再计算启动耗时。 (在 Adapter 中记录启动耗时要加一个布尔值变量进行判断,避免 onBindViewHolder 方法多次被调用导致不必要的计算;)
//第一个item 且没有记录过,启动APP到输出显示第一条内容的时间
if (viewHolder.getLayoutPosition() == 0 && !mHasRecorded) {mHasRecorded = true;viewHolder.itemView.findViewById(R.id.zsz_banner).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {@Overridepublic boolean onPreDraw() {viewHolder.itemView.findViewById(R.id.home_page_tablayout_layout).getViewTreeObserver().removeOnPreDrawListener(this);MethodUtils.i("启动开始到Adapter显示第一条结束时间"+(System.currentTimeMillis()- GZszApplication.AppStartTime));return true;}});
}

4.5AOP(Apsect Oriented Programming)打点

面向切面编程,可以使用AspectJ。例如可以切Application的onCreate方法来计算其耗时。 特点是是对代码无侵入性、可带到线上

5.分析工具介绍

分析方法耗时的工具,SystraceTraceView,两个是互相补充的关系,我们要在不同的场景下使用不用的工具,这样才能发挥工具的最大作用;

5.1TraceView

TraceView能以图形的形式展示代码的执行时间和调用栈的信息,而且TraceView提供的信息非常全面,因为它包含了所有线程

TraceView的使用可以分为两步:开始跟踪,分析结果;我们来看看具体操作;

通过Debug.startMethodTracing(tracepath)开始跟踪方法,记录一段时间内的CPU使用情况;Debug.stopMethodTracing()停止跟踪方法,然后系统就会为我们生成一个.trace文件,我们可以通过TraceView查看这个文件记录的内容;

文件生成的位置默认在Android/data/包名/files下,下面看一个例子:

  @Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {//默认生成路径:Android/data/包名/files/dmtrace.traceDebug.startMethodTracing();//也可以自定义路径                //Debug.startMethodTracing(getExternalFilesDir(null)+"test.trace");super.onCreate(savedInstanceState);setContentView(getContentViewId());setStatusBar();mContext = this;titleBar = getTitleBar();if (titleBar != null) {titleBar.setLeftLayoutClickListener(v -> finish());}EventBusUtil.register(this, this.needEventBus());initView();initData();AppManager.getAppManager().addActivity(this);MethodUtils.e("addActivity", this.TAG);Debug.stopMethodTracing();
//        TraceCompat.endSection();}

MainActivity的onCreate前后方法中分别调用开始停止记录方法,运行打开应用进入首页后,我们定位到 /sdcard/android/data/包名/files/ 目录下查看文件管理器确实是有.trace文件:

 然后双击打开dmtrace.trace文件

以图形来呈现方法跟踪数据或函数跟踪数据,其中调用的时间段和时间在横轴上表示,而其被调用方则在纵轴上显示。 所以我们可以看到具体的方法及其耗时。

详细介绍参考官方文档《使用 CPU Profiler 检查 CPU 活动》。

可以看到在onCreate方法中,最耗时的是setContentView方法,设置要显示的根布局。

5.2Systrace

Systrace 结合了 Android 内核数据,分析了线程活动后会给我们生成一个非常精确 HTML 格式的报告。

Systrace原理:在系统的一些关键链路(如SystemServcie、虚拟机、Binder驱动)插入一些信息(Label)。然后,通过Label的开始和结束来确定某个核心过程的执行时间,并把这些Label信息收集起来得到系统关键路径的运行时间信息,最后得到整个系统的运行性能信息。其中,Android Framework 里面一些重要的模块都插入了label信息,用户App中也可以添加自定义的Lable。

Systrace 提供的 Trace 工具类默认只能 API 18 以上的项目中才能使用,如果我们的兼容版本低于 API 18,我们可以使用 TraceCompat。 Systrace 的使用步骤和 Traceview 差不多,分为下面两步。

  • 调用跟踪方法
  • 查看跟踪结果

来看示例,在onCreate前后分别使用TraceCompat.beginSection、TraceCompat.endSection方法:

@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {TraceCompat.beginSection("MainActivity onCreate");super.onCreate(savedInstanceState);setContentView(getContentViewId());setStatusBar();mContext = this;titleBar = getTitleBar();if (titleBar != null) {titleBar.setLeftLayoutClickListener(v -> finish());}EventBusUtil.register(this, this.needEventBus());initView();initData();AppManager.getAppManager().addActivity(this);MethodUtils.e("addActivity", this.TAG);TraceCompat.endSection();}

运行app后,手动杀掉。然后cd 到SDK 目录下的 platform-tools/systrace 下,使用命令:

python systrace.py -t 10 -o /Users/ddw/trace.html -a com.xxx.xxx

其中:-t 10是指跟踪10秒,-o 表示把文件输出到指定目录下,-a 是指定应用包名。

输入完这行命令后,可以看到开始跟踪的提示。看到 “Starting tracing ”后,手动打开我们的应用。

示例如下:

ddwMacBook-Pro:~ ddw$ cd  /Users/ddw/Library/Android/sdk/platform-tools/systraceddwdeMacBook-Pro:systrace ddw$ python systrace.py -t 10 -o /Users/ddw/trace.html  -a com.xxx.xxxStarting tracing (10 seconds)
Tracing completed. Collecting output...
Outputting Systrace results...
Tracing complete, writing resultsWrote trace HTML file: file:///Users/ddw/trace.html

跟踪10秒,然后就在指定目录生成了html文件,我们打开看看:

这里我们同样可以看到具体的耗时,以及每一帧渲染耗费的时间。具体参考官方文档《Systrace 概览》

小结 Traceview 的两个特点

  • 可埋点 Traceview 的好处之一是可以在代码中埋点,埋点后可以用 CPU Profiler 进行分析。 因为我们现在优化的是启动阶段的代码,如果我们打开 App 后直接通过 CPU Profiler 进行记录的话,就要求你有单身三十年的手速,点击开始记录的时间要和应用的启动时间完全一致。 有了 Traceview,哪怕你是老年人手速也可以记录启动过程涉及的调用栈信息。
  • 开销大 Traceview 的运行时开销非常大,它会导致我们程序的运行变慢。 之所以会变慢,是因为它会通过虚拟机的 Profiler 抓取我们当前所有线程的所有调用堆栈。 因为这个问题,Traceview 也可能会带偏我们的优化方向。 比如我们有一个方法,这个方法在正常情况下的耗时不大,但是加上了 Traceview 之后可能会发现它的耗时变成了原来的十倍甚至更多。

Systrace 的两个特点

  • 开销小 Systrace 开销非常小,不像 Traceview,因为它只会在我们埋点区间进行记录。 而 Traceview 是会把所有的线程的堆栈调用情况都记录下来。
  • 直观 在 Systrace 中我们可以很直观地看到 CPU 利用率的情况。 当我们发现 CPU 利用率低的时候,我们可以考虑让更多代码以异步的方式执行,以提高 CPU 利用率。

Traceview 与 Systrace 的两个区别

  • 查看工具 Traceview 分析结果要使用 Profiler 查看。 Systrace 分析结果是在浏览器查看 HTML 文件。
  • 埋点工具类 Traceview 使用的是 Debug.startMethodTracing()。 Systrace 用的是 Trace.beginSection() 和 TraceCompat.beginSection()

6.启动方案优化

启动优化可以分为两个方向:

  • 视觉优化,启动耗时没有变少,但是启动过程中给用户更好的体验,指APP创建进程创建显示系统窗口时;
  • 速度优化,减少主线程的耗时,真实做到快速启动-APP进程已创建,指Application初始化到Activity执行onResume的时间段;

6.1视图优化

在《Activity的启动》中提到,在Activity启动前会展示一个名字叫StartingWindow的window,这个window的背景是取要启动Activity的Theme中配置的WindowBackground。

因为启动根activity前是需要创建进程等一系列操作,需要一定时间,而展示StartingWindow的目的是 告诉用户你点击是有反应的,只是在处理中,然后Activity启动后,Activity的window就替换掉这个StartingWindow了。如果没有这个StartingWindow,那么点击后就会一段时间没有反应,给用户误解

而这,就是应用启动开始时 会展示白屏的原因了。

那么视觉优化的方案 也就有了:替换第一个activity(通常是闪屏页)的Theme,把白色背景换成Logot图,然后再Activity的onCreate中换回来。 这样启动时看到的就是你配置的logo图了。

具体操作一下:

<activityandroid:name="cn.xxx.launch.LaunchActivity"android:theme="@style/app_AppSplash"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter>
</activity>

这里我的而第一个activity是LaucherActivity,配置了theme是R.style.app_AppSplash,来看下:

<style name="app_AppSplash" parent="@style/Theme.AppCompat.Light.NoActionBar"><item name="android:windowBackground">@drawable/app_launch_bg_splash</item><item name="android:windowNoTitle">true</item><item name="android:windowFullscreen">true</item>
</style>

看到 android:windowBackground已经配置成了自定义的drawable,这个就是关键点了,而默认是windowBackground是白色。看看自定义的drawable:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"><item><!--两层--><shape><solid android:color="@color/app_color_FFFFFF" /></shape></item><item><bitmapandroid:dither="true"android:gravity="center"android:src="@drawable/app_launch"android:tileMode="disabled" /></item>
</layer-list>

drawable的根节点是<layer-list>,然后一层是白色底,一层就是我们的logo图片了。

不仅可以通过android:theme设置theme,还可以在activity的onCreate中把Theme换回R.style.app_AppSplash即可(要在setContentView()之前);

  protected void onCreate(Bundle savedInstanceState) {setTheme(R.style.app_AppSplash);super.onCreate(savedInstanceState);setContentView(layoutId)}

效果如下:

可以看到,确实视觉上体验比白屏好很多。

但实际上启动速度并没有变快,下面就来看看可以真实提高启动速度的方案有哪些。

6.2异步初始化

前面提到 提高启动速度,核心思想就是 减少主线程的耗时操作。启动过程中 可控住耗时的主线程 主要是Application的onCreate方法、Activity的onCreate、onStart、onResume方法。

通常我们会在Application的onCreate方法中进行较多的初始化操作,例如第三方库初始化,那么这一过程是就需要重点关注。

减少主线程耗时的方法,又可细分为异步初始化、延迟初始化,即把 主线程任务 放到子线程执行 或 延后执行。 下面就先来看看异步初始化是如何实现的。

执行异步请求,一般是使用线程池,例如:

Runnable initTask = new Runnable() {@Overridepublic void run() {//init task}};ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadCount);fixedThreadPool.execute(initTask);

但是通过线程池处理初始化任务的方式存在三个问题:

  • 代码不够优雅 假如我们有 100 个初始化任务,那像上面这样的代码就要写 100 遍,提交 100 次任务。
  • 无法限制在 onCreate 中完成 有的第三方库的初始化任务需要在 Application 的 onCreate 方法中执行完成,虽然可以用 CountDownLatch 实现等待,但是还是有点繁琐。
  • 无法实现存在依赖关系 有的初始化任务之间存在依赖关系,比如极光推送需要设备 ID,而 initDeviceId() 这个方法也是一个初始化任务。

那么解决方案是啥?启动器

,即启动器,是针对这三个问题的解决方案,结合CountDownLatch对线程池的再封装,充分利用CPU多核,自动梳理任务顺序

使用方式:

  • 引入依赖
  • 划分任务,确认依赖和限制关系
  • 添加任务,执行启动

首先依赖引入:

然后把初始化任务划分成一个个任务;厘清依赖关系,例如任务2要依赖任务1完成后才能开始;还有例如3任务需要在onCreate方法结束前完成;任务4要在主线程执行

然后添加这些任务,开始任务,设置等待

具体使用也比较简单,代码如下:

public class MainApplication extends Application {@Overridepublic void onCreate() {super.onCreate();Log.i(TAG, "onCreate: taskDispatcher.start()");// task2依赖task1;// task3未完成时launcher.breakPoint(BreakPoints.TYPE_APPLICATION_CREATE)处需要等待;// test4在主线程执行//每个任务都耗时一秒AppLauncher launcher = new AppLauncher.Builder().addHeadTask(new SimpleLauncherTaskOne()).addTask(new SimpleLauncherTaskTwo()).addTask(new SimpleLauncherTaskThree()).addTask(new SimpleLauncherTaskFour()).start(); //开始launcher.breakPoint(BreakPoints.TYPE_APPLICATION_CREATE); //等待线程3执行完往下走Log.v(TAG, "onCreate Finished.");}private static class SimpleLauncherTaskOne extends LaunchTask {@Overrideprotected void call() {randomSleepTest();Log.v(TAG, "SimpleLauncherTaskOne run on " + getThreadName() + ", depends on " + getDependsOnString());}}private static class SimpleLauncherTaskTwo extends LaunchTask {@Overridepublic List<Class<? extends ILaunchTask>> dependsOn() {List<Class<? extends ILaunchTask>> dependsOn = new ArrayList<>();//依赖task1,等task1执行完再执行dependsOn.add(SimpleLauncherTaskOne.class);return dependsOn;}@Overrideprotected void call() {randomSleepTest();Log.v(TAG, "SimpleLauncherTaskTwo run on " + getThreadName() + ", depends on " + getDependsOnString());}}private static class SimpleLauncherTaskThree extends LaunchTask {@Overrideprotected void call() {randomSleepTest();Log.v(TAG, "SimpleLauncherTaskThree run on " + getThreadName() + ", depends on " + getDependsOnString());}@Overridepublic List<String> finishBeforeBreakPoints() {//task3未完成时,在 launcher.breakPoint(BreakPoints.TYPE_APPLICATION_CREATE)处需要等待。这里就是保证在onCreate结束前完成。List<String> breakPoints = new ArrayList<>(1);breakPoints.add(BreakPoints.TYPE_APPLICATION_CREATE);return breakPoints;}}private static class SimpleLauncherTaskFour extends LaunchTask {@Overrideprotected void call() {randomSleepTest();Log.v(TAG, "SimpleLauncherTaskFour run on " + getThreadName() + ", depends on " + getDependsOnString());}@Overridepublic Schedulers runOn() {//运行在主线程return Schedulers.MAIN;}}
}

有4个初始化任务,都耗时1秒,若都在主线程执行,那么会耗时4秒。这里使用启动器执行,并且保证了上面描述的任务要求限制。执行完成后日志如下:

2021-12-07 16:50:03.887 32276-32276/com.ryan.github.launcher I/LauncherSample: onCreate: taskDispatcher.start()
2021-12-07 16:50:04.898 32276-32276/com.ryan.github.launcher V/LauncherSample: SimpleLauncherTaskFour run on main, depends on
2021-12-07 16:50:04.899 32276-32293/com.ryan.github.launcher V/LauncherSample: SimpleLauncherTaskThree run on launcher-compute-2, depends on
2021-12-07 16:50:04.899 32276-32292/com.ryan.github.launcher V/LauncherSample: SimpleLauncherTaskOne run on launcher-compute-1, depends on
2021-12-07 16:50:04.899 32276-32276/com.ryan.github.launcher V/LauncherSample: onCreate Finished.
2021-12-07 16:50:05.936 32276-32467/com.ryan.github.launcher V/LauncherSample: SimpleLauncherTaskTwo run on launcher-compute-3, depends on SimpleLauncherTaskOne | 

可见主线程耗时只有1秒。 另外,要注意的是,TaskThree、TaskFour一定是在onCreate内完成了,TaskOne、TaskTwo都可能是在onCreate结束后一段时间才完成,所以在Activity中就不能使用TaskOne、TaskTwo相关的库了。那么 在划分任务,确认依赖和限制关系时就要注意了

异步初始化就说这么多,原理部分可直接阅读源码,很容易理解。接着看延迟初始化。

6.3 延迟初始化

在 Application 和 Activity 中可能存在优先级不高的初始化任务,可以考虑把这些任务进行 延迟初始化。延迟初始化并不是减少了主线程耗时,而是让耗时操作让位、让资源给UI绘制,将耗时的操作延迟到UI加载完毕后。

那么问题来了,如何延迟呢

  • 使用new Handler().postDelay()方法、或者view.postDelay()——但是延迟时间不好把握,不知道啥时候UI加载完毕。
  • 使用View.getViewTreeObserver().addOnPreDrawListener()监听——可以保证view绘制完成,但是此时发生交互呢,例如用户在滑动列表,那么就会造成卡顿了。

那么解决方案是啥?延迟启动器

延迟启动器,利用IdleHandler特性,在CPU空闲时执行,对延迟任务进行分批初始化, 这样 执行时机明确、也缓解界面UI卡顿。 延迟启动器就是上面的AppLauncher中的处理的。

public class AppLauncher implements IAppLauncher {private Queue<ILaunchTask> mDelayTasks;private MessageQueue mMainQueue;@Overridepublic void start() {checkThread();if (mMainQueue == null) {mMainQueue = Looper.myQueue();}}private void checkThread() {if (Thread.currentThread() != Looper.getMainLooper().getThread()) {throw new IllegalStateException("AppLauncher.start() must be executed on the main thread.");}}public Builder addDelayTask(ILaunchTask task) {mDelayTasks.offer(task);return this;}@Overridepublic void onceTaskFinish() {if (mFinishedCount.incrementAndGet() == mTaskCount) {markState(STATE_FINISHED);handleOnFinished();handDelayTasks();}}private void handDelayTasks() {if (mMainQueue == null || mState == STATE_PREPARE) {throw new IllegalStateException("The launcher has not started yet.");}if (mState == STATE_SHUTDOWN){throw new IllegalStateException("The launcher has been shut down.");}mMainQueue.addIdleHandler(new MessageQueue.IdleHandler() {@Overridepublic boolean queueIdle() {ILaunchTask task = mDelayTasks.poll();executeTasks(task);return !mDelayTasks.isEmpty();}});}
}

执行延时任务:

Application    @Overridepublic void onCreate() {super.onCreate();Log.i(TAG, "onCreate: taskDispatcher.start()");AppLauncher launcher = new AppLauncher.Builder().addTask(new SimpleLauncherTaskOne()).addTask(new SimpleLauncherTaskTwo()).addTask(new SimpleLauncherTaskThree()).addTask(new SimpleLauncherTaskFour()).addDelayTask(new DelaySimpleLauncherTask())    //延时任务.start();launcher.breakPoint(BreakPoints.TYPE_APPLICATION_CREATE);Log.v(TAG, "onCreate Finished.");}//延时任务
private static class DelaySimpleLauncherTask extends LaunchTask {@Overrideprotected void call() {randomSleepTest();Log.v(TAG, "DelaySimpleLauncherTask run on " + getThreadName() + ", depends on " + getDependsOnString());}
}

执行结果

2021-12-07 17:24:18.581 4692-4692/com.ryan.github.launcher I/LauncherSample: onCreate: taskDispatcher.start()
2021-12-07 17:24:19.587 4692-4692/com.ryan.github.launcher V/LauncherSample: SimpleLauncherTaskFour run on main, depends on 
2021-12-07 17:24:19.640 4692-4708/com.ryan.github.launcher V/LauncherSample: SimpleLauncherTaskOne run on launcher-compute-1, depends on 
2021-12-07 17:24:19.659 4692-4709/com.ryan.github.launcher V/LauncherSample: SimpleLauncherTaskThree run on launcher-compute-2, depends on 
2021-12-07 17:24:19.660 4692-4692/com.ryan.github.launcher V/LauncherSample: onCreate Finished.
2021-12-07 17:24:20.668 4692-4771/com.ryan.github.launcher V/LauncherSample: SimpleLauncherTaskTwo run on launcher-compute-3, depends on SimpleLauncherTaskOne |

2021-12-07 17:24:22.167 4692-4847/com.ryan.github.launcher V/LauncherSample: DelaySimpleLauncherTask run on launcher-compute-4, depends on

经反复测试,确实是在是在其他任务执行完成以后开始任务。但是如果耗时较长(例如睡眠是3秒),过程中滑动屏幕,是不能及时响应的,会感觉到明显的卡顿。

所以,能异步的task优先使用异步启动器在Application的onCreate方法中加载,对于不能异步且耗时较少的task,我们可以利用延迟启动器进行加载。如果任务可以到用时再加载,可以使用懒加载的方式。

IdleHandler原理分析:

//MessageQueue.javaMessage next() {// Return here if the message loop has already quit and been disposed.// This can happen if the application tries to restart a looper after quit// which is not supported.final long ptr = mPtr;if (ptr == 0) {return null;}int pendingIdleHandlerCount = -1; // -1 only during first iterationint nextPollTimeoutMillis = 0;for (;;) {if (nextPollTimeoutMillis != 0) {Binder.flushPendingCommands();}nativePollOnce(ptr, nextPollTimeoutMillis);synchronized (this) {// Try to retrieve the next message.  Return if found.final long now = SystemClock.uptimeMillis();Message prevMsg = null;Message msg = mMessages;if (msg != null && msg.target == null) {// Stalled by a barrier.  Find the next asynchronous message in the queue.do {prevMsg = msg;msg = msg.next;} while (msg != null && !msg.isAsynchronous());}if (msg != null) {if (now < msg.when) {// Next message is not ready.  Set a timeout to wake up when it is ready.nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);} else {// Got a message.mBlocked = false;if (prevMsg != null) {prevMsg.next = msg.next;} else {mMessages = msg.next;}msg.next = null;if (DEBUG) Log.v(TAG, "Returning message: " + msg);msg.markInUse();return msg;}} else {// No more messages.nextPollTimeoutMillis = -1;}// Process the quit message now that all pending messages have been handled.if (mQuitting) {dispose();return null;}// If first time idle, then get the number of idlers to run.// Idle handles only run if the queue is empty or if the first message// in the queue (possibly a barrier) is due to be handled in the future.if (pendingIdleHandlerCount < 0&& (mMessages == null || now < mMessages.when)) {pendingIdleHandlerCount = mIdleHandlers.size();}if (pendingIdleHandlerCount <= 0) {// No idle handlers to run.  Loop and wait some more.mBlocked = true;continue;}if (mPendingIdleHandlers == null) {mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];}mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);}// Run the idle handlers.// We only ever reach this code block during the first iteration.for (int i = 0; i < pendingIdleHandlerCount; i++) {final IdleHandler idler = mPendingIdleHandlers[i];mPendingIdleHandlers[i] = null; // release the reference to the handlerboolean keep = false;try {keep = idler.queueIdle();} catch (Throwable t) {Log.wtf(TAG, "IdleHandler threw exception", t);}if (!keep) {synchronized (this) {mIdleHandlers.remove(idler);}}}// Reset the idle handler count to 0 so we do not run them again.pendingIdleHandlerCount = 0;// While calling an idle handler, a new message could have been delivered// so go back and look again for a pending message without waiting.nextPollTimeoutMillis = 0;}}

从消息队列取消息时,如果没有取到消息,就执行 空闲IdleHandler,执行完就remove。

6.4 Multidex预加载优化

安装或者升级后 首次 MultiDex 花费的时间过于漫长,我们需要进行Multidex的预加载优化。

5.0以上默认使用ART,在安装时已将Class.dex转换为oat文件了,无需优化,所以应判断只有在主进程及SDK 5.0以下才进行Multidex的预加载

抖音BoostMultiDex优化实践:

抖音BoostMultiDex优化实践:Android低版本上APP首次启动时间减少80%(一)

Github地址:BoostMultiDex

快速接入:

  • build.gradle的dependencies中添加依赖:
dependencies {// For specific version number, please refer to app demoimplementation 'com.bytedance.boost_multidex:boost_multidex:1.0.1'
}
复制代码
  • 与官方MultiDex类似,在Application.attachBaseContext的最前面进行初始化即可:
public class YourApplication extends Application {@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base);BoostMultiDex.install(base);}
复制代码

今日头条5.0以下,BoostMultiDex、MultiDex启动速度对比

6.5声明主 DEX 文件中需要的类

为 Dalvik 可执行文件分包构建每个 DEX 文件时,构建工具会执行复杂的决策制定来确定主要 DEX 文件中需要的类,以便应用能够成功启动。如果启动期间需要的任何类未在主 DEX 文件中提供,那么您的应用将崩溃并出现错误 java.lang.NoClassDefFoundError。
该情况不应出现在直接从应用代码访问的代码上,因为构建工具能识别这些代码路径,但可能在代码路径可见性较低(如使用的库具有复杂的依赖项)时出现。例如,如果代码使用自检机制或从原生代码调用 Java 方法,那么这些类可能不会被识别为主 DEX 文件中的必需项。
因此,如果您收到 java.lang.NoClassDefFoundError,则必须使用构建类型中的 multiDexKeepFile 或 multiDexKeepProguard 属性声明它们,以手动将这些其他类指定为主 DEX 文件中的必需项。如果类在 multiDexKeepFile 或 multiDexKeepProguard 文件中匹配,则该类会添加至主 DEX 文件。

APK方法数超过65535及MultiDex解决方案 - dongweiq - 博客园

6.6 页面数据预加载

闪屏页、首页的数据预加载:闪屏广告、首页数据 加载后缓存到本地,下次进入时直接读取缓存。 首页读取缓存到内存的操作还可以提前到闪屏页。

6.7 页面绘制优化

闪屏页与主页的绘制优化,这里涉及到绘制优化相关知识了,例如减少布局层级等。

6.8不关闭Activity退回主界面

Intent launcherIntent = new Intent(Intent.ACTION_MAIN);launcherIntent.addCategory(Intent.CATEGORY_HOME);startActivity(launcherIntent);

七、总结

我们先介绍了启动流程、优化思想、耗时检测、分析工具,然后给出了常用优化方案:异步初始化、延迟初始化。涉及了很多新知识和工具,一些地方文章中没有展开,可以参考给出的连接详细学习。毕竟性能优化是多样技术知识的综合使用,需要系统掌握对应工作流程被、分析工具、解决方案,才能对性能进行深层次的优化。

参考:

Android中的onWindowFocusChanged()方法详解 - roccheung - 博客园

APK方法数超过65535及MultiDex解决方案 - dongweiq - 博客园

Android启动优化 :学会这些让应用启动速度提高10倍! - 简书

https://github.com/lovepli/AppLauncher

​​​​​​Android 自定义View的post(Runnable)方法非100%执行的原因和处理方法解析_Xavier__S的博客-​​​​​​CSDN博客

Android的multidex带来的性能问题-减慢app启动速度 - 泡在网上的日子

APK方法数超过65535及MultiDex解决方案 - dongweiq - 博客园

Android启动优化实战(有效降低APP启动时间)相关推荐

  1. Android 性能优化---(8)APP启动时间优化指南

    本文可以帮助你优化应用的启动时间:首先描述应用启动过程的内部机制:然后讨论如何分析启动性能:最后,列举了一些常见的影响启动时间的问题,并就如何解决这些问题给出一些提示. 第 1 部分:启动过程内部机制 ...

  2. 深入探索Android 启动优化(七) - JetPack App Startup 使用及源码浅析

    本文首发我的微信公众号:徐公,想成为一名优秀的 Android 开发者,需要一份完备的 知识体系,在这里,让我们一起成长,变得更好~. 前言 前一阵子,写了几篇 Android 启动优化的文章,主要是 ...

  3. 太牛了!我把阿里、腾讯、字节跳动、美团等Android性能优化实战整合成了一个PDF文档

    安卓开发大军浩浩荡荡,经过近十年的发展,Android技术优化日异月新,如今Android 11.0 已经发布,Android系统性能也已经非常流畅,可以在体验上完全媲美iOS. 但是,到了各大厂商手 ...

  4. 启动优化·基础论·浅析 Android 启动优化

    " [小木箱成长营]启动优化系列文章(排期中): 启动优化 · 工具论 · 启动优化常见的六种工具 启动优化 · 方法论 · 这样做启动优化时长降低 70% 启动优化 · 实战论 · 手把手 ...

  5. Android主线程耗时动画卡顿,Android性能优化实战之界面卡顿

    原标题:Android性能优化实战之界面卡顿 作者:红橙Darren https://www.jianshu.com/p/18bb507d6e62 今天是个奇怪的日子,有三位同学找我,都是关于界面卡顿 ...

  6. android布局优化方案,Android启动优化-布局优化

    Android启动优化-布局优化 安卓应用开发发展到今天,已经成为一个非常成熟的技术方向,从目前的情况看,安卓开发还是一个热火朝天的发展,但高级人才却相对较少,如今移动互联网的开发者也逐渐开始注重插入 ...

  7. Android 启动优化(五)- AnchorTask 1.0.0 版本正式发布了

    今天,更新一下 Android 启动优化有向无环图系列的最后一篇文章.最近一段时间,暂时不会更新这方面的文章了.系列文章汇总如下: Android 启动优化(一) - 有向无环图 Android 启动 ...

  8. Android 启动优化(一) - 有向无环图

    前言 说到 Android 启动优化,大家第一时间可能会想到异步加载.将耗时任务放到子线程加载,等到所有加载任务加载完成之后,再进入首页. 多线程异步加载方案确实是 ok 的.但如果遇到前后依赖的关系 ...

  9. Android 系统性能优化(21)---App启动原理分析及启动时间优化

    一.启动原理解析 Android是基于Linux内核的,当手机启动,加载完Linux内核后,会由Linux系统的init祖先进程fork出Zygote进程,所有的Android应用程序进程以及系统服务 ...

最新文章

  1. HTML基础部分(1)字体,照片,链接
  2. 正则表达式笔试题php,2017年初级PHP程序员笔试题
  3. 【视频课】一课彻底掌握深度学习人脸图像算法,长期更新
  4. pythondistutils安装_安装msi后的python distutils
  5. 黑马Android全套视频无加密完整版
  6. [编程技巧] C++字符串初始化的优化写法
  7. 计算机网络大学教学大纲,《计算机网络》教学大纲
  8. (11)DJBX33A APR哈希默认算法
  9. Go Hack 2017 报名开启:十月魔都约一场 Go 语言烧脑之战
  10. 中断 http请求 正在加载 取消http请求
  11. 一起谈.NET技术,NET下RabbitMQ实践 [配置篇]
  12. armv6, armv7, armv7s, arm64 的区别
  13. 跨数据库跨系统,数据脱敏有新招
  14. 佳能打印机 出现5100错误怎么办
  15. 2019年安徽省模块七满分多少_2019年安徽中考总分是多少 考试科目及分值
  16. 按下鼠标滚轮c语言,C++检测鼠标某键是否按下
  17. pmp考前冲刺 项目管理中的工具与技术
  18. idea 选中代码生成方法
  19. 9.2.1 Python图像处理之图像数学形态学-二值形态学应用之噪声消除
  20. HJ卫星数据的下载与打开

热门文章

  1. 第十八届全国大学生智能车竞赛东北赛区成绩与奖项
  2. 【CSS】padding,border,margin与width宽度的关系
  3. 使用游标(Cursors)将多行查询结果逐行处理
  4. 数据挖掘---频繁项集挖掘Apriori算法的C++实现
  5. ALERT! UUID=xxxxxxxxx does not exist. Dropping to a shell!
  6. 数组A[10][15]的每个元素都是占4个字节的数据,将其按列优先次序存储
  7. 为什么说Tomcat是服务器?
  8. 2023年7月14日~15日
  9. SSO、CAS、OAuth、OIDC
  10. phpwind 论坛 转移