性能优化Android

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

2022-07-15  本文已影响0人  super可乐

1.概述

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

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

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

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

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

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

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

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

3.优化核心思想

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

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

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
 @Override
    protected void onResume() {
        super.onResume();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public 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是我们应用能接收到的最早一个生命周期回调方法;

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

//APP启动开始时间
public class GZszApplication extends BaseApplication {
    public static long AppStartTime = -1;
    @Override
    protected 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
//第一个item 且没有记录过,启动APP到输出显示第一条内容的时间
if (viewHolder.getLayoutPosition() == 0 && !mHasRecorded) {
    mHasRecorded = true;
    viewHolder.itemView.findViewById(R.id.zsz_banner).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public 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.分析工具介绍

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

5.1TraceView

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

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

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

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

  @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        //默认生成路径:Android/data/包名/files/dmtrace.trace
        Debug.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.2 Systrace

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

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

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

@Override
    protected 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/systrace
 
ddwdeMacBook-Pro:systrace ddw$ python systrace.py -t 10 -o /Users/ddw/trace.html  -a com.xxx.xxx
 
Starting tracing (10 seconds)
Tracing completed. Collecting output...
Outputting Systrace results...
Tracing complete, writing results
 
Wrote 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.启动方案优化

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

6.1视图优化

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

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

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

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

具体操作一下:

<activity
    android: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>
        <bitmap
            android: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() {
            @Override
            public void run() {
                //init task
            }
        };
 
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadCount);
        fixedThreadPool.execute(initTask);

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

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

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

使用方式:

首先依赖引入:

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

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

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

public class MainApplication extends Application {
 
    @Override
    public 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 {
        
        @Override
        protected void call() {
            randomSleepTest();
            Log.v(TAG, "SimpleLauncherTaskOne run on " + getThreadName() + ", depends on " + getDependsOnString());
        }
    }
 
    private static class SimpleLauncherTaskTwo extends LaunchTask {
 
        @Override
        public List<Class<? extends ILaunchTask>> dependsOn() {
            List<Class<? extends ILaunchTask>> dependsOn = new ArrayList<>();
             //依赖task1,等task1执行完再执行
            dependsOn.add(SimpleLauncherTaskOne.class);
            return dependsOn;
        }
 
        @Override
        protected void call() {
            randomSleepTest();
            Log.v(TAG, "SimpleLauncherTaskTwo run on " + getThreadName() + ", depends on " + getDependsOnString());
        }
 
    }
    
    private static class SimpleLauncherTaskThree extends LaunchTask {
 
        @Override
        protected void call() {
            randomSleepTest();
            Log.v(TAG, "SimpleLauncherTaskThree run on " + getThreadName() + ", depends on " + getDependsOnString());
        }
 
        @Override
        public 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 {
 
        @Override
        protected void call() {
            randomSleepTest();
            Log.v(TAG, "SimpleLauncherTaskFour run on " + getThreadName() + ", depends on " + getDependsOnString());
        }
 
        @Override
        public 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加载完毕后。

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

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

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

public class AppLauncher implements IAppLauncher {
    private Queue<ILaunchTask> mDelayTasks;
    private MessageQueue mMainQueue;
    
    @Override
    public 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;
    }
 
        @Override
    public 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() {
            @Override
            public boolean queueIdle() {
                ILaunchTask task = mDelayTasks.poll();
                executeTasks(task);
                return !mDelayTasks.isEmpty();
            }
        });
    }
}

执行延时任务:

Application    
    @Override
    public 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 {
 
    @Override
    protected 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.java
    Message 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 iteration
        int 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 handler
 
                boolean 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

快速接入:

dependencies {
    // For specific version number, please refer to app demo
    implementation 'com.bytedance.boost_multidex:boost_multidex:1.0.1'
}
复制代码

与官方MultiDex类似,在Application.attachBaseContext的最前面进行初始化即可:
public class YourApplication extends Application {

@Override
protected 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);

七、总结

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

上一篇下一篇

猜你喜欢

热点阅读