700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > Android性能优化系列:启动优化

Android性能优化系列:启动优化

时间:2020-03-23 08:05:57

相关推荐

Android性能优化系列:启动优化

文章目录

1 应用启动类型1.1 冷启动1.2 温启动1.3 热启动2 查看启动耗时2.1 adb命令查看2.2 Logcat Displayed查看启动耗时2.3 手动记录启动耗时2.3.1 Application.attachBaseContext()2.3.2 Activity.onWindowFocusChanged()?draw?2.4 AOP记录方法耗时3 启动耗时分析工具3.1 CPU Profiler3.2 TraceView3.2.1 TraceView的操作步骤3.2.2 TraceView使用注意事项3.2.3 TraceView的缺点和使用场景3.3 Systrace3.3.1 Systrace环境安装3.3.2 Systrace的操作步骤3.3.3 Systrace的优点使用场景4 Application初始化的启动优化途径4.1 异步加载:子线程/线程池、TaskDispatcher4.1.1 子线程/线程池4.1.2 异步启动器4.2 延迟加载:IdleHandler4.3 其他启动优化方案5 UI的启动优化途径5.1 修改启动主题背景5.2 UI渲染布局优化5.3 异步inflate

1 应用启动类型

应用的启动类型分为三种:

冷启动

温启动

热启动

三种类型启动耗时时间从大到小排序:冷启动 > 温启动 > 热启动

1.1 冷启动

在Android系统中,系统为每个运行的应用至少分配一个进程(多进程应用申请多个进程)。从进程的角度上讲,冷启动就是在启动应用前,系统中没有该应用的任何进程信息。

冷启动的场景比如设备开机后应用的第一次启动、系统杀掉应用进程后再次启动等。所以,冷启动的启动时间最长,因为相比另外两种启动方式,系统和我们的应用要做的工作最多。

冷启动一般会作为启动速度的一个衡量标准。

冷启动详细的启动过程可以参考:Android 从点击应用图标到界面显示的过程,这里简单说明下启动过程。

梳理上图的冷启动过程:

用户从ClickEvent点击应用图标开始,会经过IPCProcess.start即创建应用进程

ActivityThread是每一个单独进程的入口,相当于java的main方法,在这里会处理消息的循环以及主线程Handler的创建等

bindApplication会通过反射创建Application对象以及走Application的生命周期

LifeCycle就是走的Activity生命周期,如onCreate()onStart()onResume()

ViewRootImpl最后经历界面的measurelayoutdraw

冷启动详细流程可以简单分为三个步骤,其中创建进程步骤是系统做的,启动应用绘制界面是应用做的:

创建进程

启动App

显示一个空白的启动Window

创建应用进程

启动应用

创建Application

启动主线程(UI线程)

创建第一个Activity(MainActivity)

绘制界面

加载视图布局(Inflating)

计算视图在屏幕上的位置排版(Laying out)

首帧视图绘制(Draw)

只有当应用完成首帧绘制时,系统当前展示的空白背景才会消失被Activity的内容视图替换掉。也就是这个时候用户才能和我们的应用开始交互。

下图展示了冷启动过程系统和应用的一个工作时间流,参考自Android官方文档:App startup time

上图是应用启动Application和Activity的两个creation,它们均在View绘制展示之前。所以,在应用自定义的Application和入口Activity,如果它们的onCreate()做的事情越多,冷启动消耗的时间越长。

冷启动优化的方向是Application和Activity的生命周期阶段,这是我们开发者能控制的时间,其他阶段都是系统做的

1.2 温启动

温启动包含在冷启动期间发生的一些操作,它的开销大于热启动。有许多可能的状态可以被认为是温启动,例如:

用户退出应用到Launcher,但随后重新启动应用。此时进程可能还在运行,但应用程序必须通过调用onCreate()重新创建Activity

应用程序因为内存原因被系统强制退出,然后用户重新启动应用。进程和Activity需要被重新启动,但是保存的Bundle实例状态会被传递给onCreate()使用

简单理解温启动就是它会重新走Activity的一些生命周期,它不会重新走进程的创建、Application的生命周期等。

1.3 热启动

热启动比冷启动简单得多且开销更低,在热启动时,系统会将Activity从后台切回到前台,如果应用的所有Activity仍旧驻留在内存中,那么应用可以避免重复对象初始化、布局加载和绘制。

然而,如果应用响应了系统内存清理的通知清理了内存,比如回调onTrimMemory(),那么这些被清理的对象在热启动就会被重新创建。

热启动和冷启动展示在屏幕的行为相同:系统进程展示一个空白屏幕直到应用绘制完成显示出Activity。

2 查看启动耗时

在启动耗时分析之前,有必要了解怎么查看启动耗时。根据输出的启动耗时记录,我们就可以先记录优化前的冷启动耗时,然后再对比优化后的启动耗时时间。

2.1 adb命令查看

确保手机USB连上电脑,可以通过adb命令查看启动耗时:

adb shell am start -W 包名/入口Activity全限定名 例如:adb shell am start -W com.example.test/com.example.test.SplashActivity或adb shell am start -W com.example.test/.SplashActivityStarting: Intent {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.test/.MainActivity }Statys: okActivity: com.example.test/.MainActivityThisTime: 782TotalTime: 1102WaitTime: 1149Complete

上面有三个时间指标:

ThisTime:表示一连串启动Activity的最后一个Activity的启动耗时

TotalTime:表示应用启动的耗时,包括创建启动应用进程和入口Activity的启动耗时,但不包括前一个应用Activity pause的耗时(即所有Activity启动耗时)。一般我们主要关心这个数值,这个时间才是自己应用真正启动的耗时

WaitTime:返回从其他应用进程startActivity()到应用首帧完全显示这段时间,即总的耗时,包括前一个应用Activity pause的时间和新应用启动的时间(即AMS启动Activity的总耗时)

注:前一个应用的Activity pause的时间,需要知道的是Launcher桌面也是一个应用,如果你的应用是在桌面点击app图标启动的,那么这里所说的前一个应用的Activity就是Launcher的Activity。

一般情况下启动耗时对比:This Time<Total Time<Wait Time

上面三个指标简单理解:

如果关心应用界面Activity启动耗时,参考ThisTime

如果只关心某个应用自身启动耗时,参考TotalTime

如果关心系统启动应用耗时,参考WaitTime

测试冷启动前可以先强制杀死进程:

adb shell am force-stop com.example.test

如果需要统计多次可以使用命令:

adb shell am start -S -W -R 10 com.example.test/.MainActivity

-S:关闭Activity所属的App进程后再启动Activity

-W:等待启动完成

-R:重复次数

2.2 Logcat Displayed查看启动耗时

在Android 4.4(API 19)或以上版本,Android提供了一个指标可以让我们在Logcat就可以查看打印出应用的启动时间。这个时间值从应用启动(创建应用进程)开始计算到完成视图的首帧绘制(即Activity内容对用户可见)为止。

在Android Studio的Logcat查看,过滤tag为Displayed,勾选No Filters:

2.3 手动记录启动耗时

在网上搜索其他博客可能会告诉你,要在Application的attachBaseContext()和MainActivity的onWindowFocusChanged()分别记录冷启动的开始和结束时间。那为什么选择在这两个地方记录启动耗时?在这里记录是否就是准确的?

2.3.1 Application.attachBaseContext()

选择在Application的attachBaseContext()记录冷启动开始时间,是因为在创建应用进程时,应用Application会被反射创建,并且跟随的是将Application对象attachContext

static public Application newApplication(Class<?> clazz, Context context) throws InstantiationException, IllegalAccessException,ClassNotFoundException {Application app = (Application) clazz.newInstance();app.attach(context); return app;}

应用的冷启动记录开始是应用被创建时,所以选择Application的attachBaseContext()是一个不错的选择。

2.3.2 Activity.onWindowFocusChanged()?draw?

onWindowFocusChanged(boolean hasFocus)会在当前窗口焦点变化时回调,在Activity生命周期中,onStart()onResume()都不是布局可见的时间点,因为回调onResume()ViewRootImpl并没有开始View的measurelayoutdraw(参考文章:View绘制流程源码解析);而在onWindowFocusChanged()时View已经完成了measurelayout,但是View还没有draw,通过打印可以查看:

-06-26 16:03:46.781 24278-24278/? I/MainActivity: onStart, size = 0,0-06-26 16:03:46.786 24278-24278/? I/MainActivity: onResume, size = 0,0-06-26 16:03:46.837 24278-24278/? I/MainActivity: onMeasure-06-26 16:03:46.864 24278-24278/? I/MainActivity: onMeasure-06-26 16:03:46.865 24278-24278/? I/MainActivity: onLayout-06-26 16:03:46.888 24278-24278/? I/MainActivity: onWindowFocusChanged, size = 112,54-06-26 16:03:46.899 24278-24278/? I/MainActivity: onDraw-06-26 16:03:46.899 24278-24278/? I/MainActivity: dispatchDraw

所以Activity界面展示上在回调onWindowFocusChanged()时只显示一个Window背景,因为后续才开始View的draw

onWindowFocusChanged()源码中文档也有说明:

/*** Called when the current {@link Window} of the activity gains or loses* focus. This is the best indicator of whether this activity is visible* to the user. The default implementation clears the key tracking* state, so should always be called.*/public void onWindowFocusChanged(boolean hasFocus) {}

所以在onWindowFocusChanged()记录启动的结束时间是不准确的,因为我们需要的是界面对用户可见时作为结束时间。那什么时候才记录结束时间呢?

我们可以在第一个View展示给用户时通过ViewTreeObserver在回调记录结束的时间:

// RecyclerView第一个位置的View对用户可见时记录启动结束时间@Overridepublic void onBindViewHolder(...) {if (position == 0 && !mHasRecord) {mHasRecord = true;// 也可以使用addOnDrawListener但要求API 16holder.view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {@Overridepublic boolean onPreDraw() {holder.view.getViewTreeObserver.removeOnPreDrawListener(this);// 记录启动结束时间...return true;}});}}

2.4 AOP记录方法耗时

手动代码记录时间的方式有一定弊端:

代码侵入性强,需要在统计耗时的方法前后打点

工作量大,当涉及到多个统计耗时会很难以维护

使用AOP面向切面编程,在Android中这种方式就是在编译期动态的将要处理的代码插入到目标方法达到目的。

Android的AOP有多种方式:谈谈Android AOP技术方案。在上手难度上Aspect J框架成熟且容易入手,具体Aspect J的使用:AOP面向切面编程:Aspect J的使用。

在项目根目录build.gradle添加编译插件:

classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'

在定义切面和被hook的module在build.gradle添加插件:

apply plugin: 'android-aspectjx'

下面用一个示例说明怎么使用Aspect J实现耗时方法的统计,示例非常简单:点击时模拟执行方法耗时延时3秒,使用Aspect J在点击执行前后记录下开始和结束时间。

findViewById(R.id.text_view).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {SystemClock.sleep(3000);Log.i(TAG, "onClick");}});@Aspectpublic class AopHelper {// 这里为了演示方便下面的表达式是会对所有的点击监听都生效的// 实际项目代码中不要这样使用@Around("execution(* android.view.View.OnClickListener.onClick(..))")public void pointcutOnClick(ProceedingJoinPoint proceedingJoinPoint) {// 点击前记录下开始时间long startTime = System.currentTimeMillis();try {// 执行点击的耗时方法proceedingJoinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();} finally {// 点击后记录下结束时间并计算出方法耗时Log.i(TAG, "total time = " + (System.currentTimeMillis() - startTime));}}}

3 启动耗时分析工具

3.1 CPU Profiler

CPU Profiler是Google在Android Studio 3.0开始推出的性能分析工具。CPU Profiler提供了Call ChartFlame ChartTop DownBottom Up四种可视界面展示CPU数据让我们可以更方便分析,这四种可视界面类型的区别和数据查看可以参考Android Studio Profiler工具解析应用的内存和CPU使用数据。

旧版本CPU Profiler界面:

新版本CPU Profiler界面:

1、位置①:记录的开始到结束的时间范围。如果需要查看具体某个范围的时间可以用鼠标拖动选择

2、位置②:新版本的CPU Profiler更直观的将Call Chart按线程排列出来,如果需要查看具体的某个线程执行的Call Chart,可以双击左边的线程名称展开具体查看。如图显示Thread(9)表示在这段时间有9个线程在运行,main表示我们的主线程,还有展示其他的线程。

其中,Call Chart中橙色表示的是系统或native的方法调用,蓝色表示第三方库的方法调用,绿色表示自己项目的方法调用。

从上到下是方法的调用栈,比如A方法调用了B方法,那么A方法在上面,B方法在下面。

一般情况我们会关注蓝色第三方库和绿色自己项目的方法调用耗时,如果项目有native方法当然也要关注橙色部分。如果调用方法过于耗时,就要考虑将方法异步加载或者延迟加载。

比如现在需要查看initFlavorApp()方法耗时,可以鼠标选中initFlavorApp()或点击Call Chart再对比右边的Top DownFlame ChartBottom Up查看具体方法调用栈耗时:

3、位置③:Top DownFlame ChartBottom Up切换不同的Tab查看具体的方法调用栈耗时。一般情况下我们会用Call ChartTop Down比较多一些

Top Down可以非常直观的从上到下查看具体的方法调用栈。如下图是初始化log库的具体方法调用栈(可以右键点击Jump to Source查看源码调用位置):

Bottom Up则相反,从上到下是查看某个方法是在哪个地方哪个线程被调用。如下图InitRetrofitTaskrun()方法是属于TaskDispatcherPool-1-Thread-1()线程:

关于其中的TotalSelfChildren数值具体表示的是什么,参考文章:Android Studio Profiler工具解析应用的内存和CPU使用数据

4、位置④:当前查看的是哪个线程

5、位置⑤:

Wall Clock Time:程序执行消耗的时间

Thread Time:CPU的执行程序消耗的时间

上面切换到Wall Clock TimeThread Time展示了不同的执行时间。比如上图的Wall Clock Timemain()方法显示了139591(即139ms左右),表示这个方法的程序执行时间就是139ms左右;而Thread Time是82080(即82ms左右),表示CPU实际执行这个方法的时间只有82ms左右。

我们需要明白Wall Clock TimeThread Time的区别,否则有可能会误导我们的优化方向。具体为什么会有可能误导,下面会说明讲解。

3.2 TraceView

TraceView是Android平台特有的数据采集和分析工具,它主要用于分析Android中应用程序的耗时热点。TraceView本身只是一个数据分析工具,而数据的采集则需要使用Android SDK中的Debug类生成.trace文件再结合CPU Profiler分析。

TraceView具备以下特点:

图形的形式展示执行时间、调用栈等

信息全面,包含所有线程

3.2.1 TraceView的操作步骤

在代码中加入Debug.startMethodTracing()Debug.stopMethodTracing()开始和停止CPU记录,运行程序生成.trace文件

public class MyApplication extends Application {@Overridepublic void onCreate() {// 在开始分析的地方调用,传入路径// 如果是放到外部路径,需要添加权限// 默认存储在/sdcard/Android/data/packagename/filesDebug.startMethodTracing("App");...// 在结束的地方调用Debug.stopMethodTracing();}}

.trace文件导入CPU Profiler分析:

3.2.2 TraceView使用注意事项

Debug控制CPU活动的记录,需要将应用部署到Android 8.0(API 26)或以上

Debug应该与用于开始和停止CPU活动记录的其他方法(即Debug和CPU Profiler图形界面中的按钮以及在应用启动时执行的自动记录的记录配置中的设置)分开使用

Debug.startMethodTracing()Debug.stopMethodTracing()是配套使用的,Debug.stopMethodTracing()之前如果有调用多个Debug.startMethodTracing(),它会寻找最近的一个Debug.startMethodTracing()作为开始

Debug.startMethodTracing()Debug.stopMethodTracing()必须在同一个线程中

3.2.3 TraceView的缺点和使用场景

TraceView收集的信息比较全面,比如上面演示TraceView的例子中只是在主线程加了埋点,它就会抓取所有的线程所有执行函数以及顺序。但也是这个工具太强大,所以它也带来了一些问题:

使用TraceView时运行时开销严重:整体App的运行变慢,可能会导致无法区分是不是TraceView影响了启动耗时

可能会带偏优化方向:就如上面提到的,TraceView开销大影响了整体性能,可能方法A正常情况下执行时间并不耗时,但加上TraceView受影响可能就变得耗时

列出上面的问题并不是想表明TraceView就不能作为启动耗时工具分析使用,而是要根据对应的分析场景使用

比如如果单纯的使用CPU Profiler基本不能抓取到准确的启动耗时的,但结合TraceView先在代码埋点之后,运行程序生成.trace文件再导入CPU Profiler分析就是一个很好的方式。

3.3 Systrace

Systrace将来自Android内核的数据(如CPU调度程序、磁盘活动和应用程序线程)结合起来生成一个HTML报告,帮助确定如何最好地提高应用程序的性能。从报告中可以看到各个线程的执行时间、方法耗时、CPU执行时间等,该报告突出了它观察到的问题(如在显示动作或动画时的ui jank),并且提供了有关如何修复这些问题的建议。

我们经常能够在系统源码看到Systrace的调用,通过Trace/TraceCompat.traceBegin()Trace/TraceCompoat.endSection()配套使用,比如Looper.loop()

public static void loop() {...for (;;) {...if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {Trace.traceBegin(traceTag, msg.target.getTraceName(msg)); // 开始记录}...try {msg.target.dispatchMessage(msg);dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} finally {if (traceTag != 0) {Trace.traceEnd(traceTag); // 结束记录}}...}}

3.3.1 Systrace环境安装

Systrace工具存放在目录platform-tools/systrace/systracce.py,在使用前需要安装相关环境:

1、安装python 2.7

2、电脑系统Win10和较高版本Android Studio在运行Systrace准备导出trace.html文件时可能会出现如下问题:

ImportError: No module named win32con

需要安装pywin32(选择python 2.7版本,根据32位或64位系统区分下载):pywin32

3、在安装完pywin32后可能还有提示如下问题:

ImportError: No module named six

需要安装six库:six

然后在解压后的目录使用python安装:

3.3.2 Systrace的操作步骤

1、在代码中加入Trace.beginSection()Trace.endSection()开始和停止记录

// 在开始的地方调用TraceCompat.beginSection("SystraceAppOnCreate");// 在结束的地方调用TraceCompat.endSection();

2、命令行进入到systrace目录启动systrace.py,程序运行启动后回车导出trace.html

cd sdk\platform-tools\systrace// systrace支持的命令参考Android文档:// /topic/performance/tracing/command-line#command_optionspython systrace.py -a packageName sched gfx view wm am app

注:如果是分析的冷启动比如在Application的onCreate()前后加上的Systrace埋点,那么python运行systrace.py要在程序运行前就启动,否则导出的trace.html会无法找到设置的sectionName

3、在Chrome浏览器或 perfetto 打开trace.html分析

Systrace显示了CPU的核数从0到7,说明运行设备是8核的CPU。

后面的数据信息是CPU的时间片(CPU Slice),可以发现8核的CPU并不是时刻都一起使用,有些CPU核运行密集,有些比较稀疏,这种情况也比较普遍,这是和设备有关。有些手机厂商默认情况下是提供8核CPU,有些厂商只提供4核。比如上面的设备就是只使用了4核,CPU 0-3在运行时CPU 4-7是空闲的显示一片空白。

刚才我们使用Systrace埋点的secionNameSystraceAppOnCreate,可以在搜索栏查找,Systrace会将它高亮显示出来:

其中,Wall DurationCPU Duration分别的对应CPU Profiler中的Wall Clock TimeThread Time。这里显示Wall Duration即程序执行的耗时是63ms,而实际上CPU Duration即CPU执行的时间是48ms。

如果Wall DurationCPU Duration差值较大,比如上图显示的数据,Wall Duration执行了515ms,实际CPU执行时间只有175ms,这之间CPU都处于休眠的状态。遇到这种情况就需要考虑是否程序对CPU利用率不高,提高CPU利用率开启一些线程操作,或者分析是不是程序导致锁等待问题。

如果需要单独查看SystraceAppOnCreate的具体信息,可以在Systrace点击并按下m键开启或关闭高亮显示:

关于Systrace更详细的使用方式和相关原理,可以参考文章:Systrace基础知识和实战

3.3.3 Systrace的优点使用场景

相比TraceViewSystrace有它的优点:

轻量级,开销小

直观反映CPU利用率。如上面看到的Wall DurationCPU Duration

walltimecputime(即Systrace列出的Wall DurationCPU Duration)的区别:

walltime是代码执行时间

cputime是代码消耗CPU的时间,它才是我们应该优化的重点指标,根据Systrace展示的信息分析让cputime跑满CPU

为什么walltimecputime会不同呢?一个比较经典的案例是锁冲突问题。

程序执行到a()时它是一个synchronized方法需要拿到锁,而刚好锁被其他程序占用,这就会导致a()一直在等待锁,但其实a()执行并不耗时。这就导致a()walltime耗时很长,但cputime实际却不耗时。

4 Application初始化的启动优化途径

很多时候为了能够在启动应用进入主界面时就可以使用一些功能,我们都会在Application的onCreate()初始化一些第三方库或其他组件,但在Application初始化的地方做太多繁重的事情是可能导致严重启动性能问题的元凶之一。Application里面的初始化操作不结束,其他任意的程序操作都无法进行。

其实很多组件是需要做区队对待的,有些可以做延迟加载,有些可以放到其他的地方做初始化操作,特别需要留意包含Disk IO操作、网络访问等严重耗时的任务,它们会严重阻塞程序的启动。

优化这些问题的解决方案是做延迟加载,可以在Application里面做延迟加载,也可以把一些初始化操作延迟到组件真正被调用到的时候再加载。

上面说明了优化方向,先简单总结下优化启动耗时的两种方式:

异步加载

延迟加载

接下来根据上面的两种处理方式提供对应的一些解决方案。

4.1 异步加载:子线程/线程池、TaskDispatcher

异步加载简单理解就是将一些初始化任务放到子线程异步执行,充分利用CPU由子线程或线程池分担主线程初始化任务。异步初始化的核心就是子线程分担主线程的任务,并行执行减少时间

4.1.1 子线程/线程池

在java中创建子线程就是ThreadRunnable,但是我们一般都不会直接使用它们,而是使用线程池的方式统一管理。java同样提供了 Executors 线程池管理工具帮助我们管理线程。

在使用线程池之前,有必要了解线程池使用场景的任务类型:

IO密集型:IO密集型任务不消耗CPU,核心池可以很大

CPU密集型:核心池大小和CPU核心数相关

Executors提供了四种线程池分别是FixedThreadPoolCacheThreadPoolScheduledThreadPoolSingleThreadPool,让我们可以根据以上任务类型场景选择不同的线程池配置。如果上面的配置不能满足,也可以自定义ThreadPool,比如Android的AsyncTask就是CPU密集型任务类型,AsyncTask内部自定义了线程池:

public abstract class AsyncTask<Params, Progress, Result> {private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();// We want at least 2 threads and at most 4 threads in the core pool,// preferring to have 1 less than the CPU count to avoid saturating// the CPU with background workprivate static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;private static final int KEEP_ALIVE_SECONDS = 30;private static final ThreadFactory sThreadFactory = new ThreadFactory() {private final AtomicInteger mCount = new AtomicInteger(1);public Thread newThread(Runnable r) {return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());}};private static final BlockingQueue<Runnable> sPoolWorkQueue =new LinkedBlockingQueue<Runnable>(128);static {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,sPoolWorkQueue, sThreadFactory);threadPoolExecutor.allowCoreThreadTimeOut(true);THREAD_POOL_EXECUTOR = threadPoolExecutor;}}

使用线程池的方式实现异步初始化的操作:

// 根据不同设备计算设置不同的核心线程数private val CPU_COUNT = Runtime.getRuntime().availableProcessors()private val CORE_POOL_SIZE = max(2, min(CPU_COUNT - 1, 4))val threadPool = Executors.newFixedThreadPool(CORE_POOL_SIZE)threadPool.submit {// initialized}

但在实际场景,异步初始化的第三方库可能进入首页后就要使用,这时候异步任务还没加载完第三方库可能会导致应用崩溃抛异常。可以使用CountDownLatch锁存器等待任务执行结束后再使用,同样的关于CountDownLatch参考博客:java并发

private val CPU_COUNT = Runtime.getRuntime().availableProcessors()private val CORE_POOL_SIZE = max(2, min(CPU_COUNT - 1, 4))private val mCountDownLatch = CountDownLatch(1)val threadPool = Executors.newFixedThreadPool(CORE_POOL_SIZE)threadPool.submit {// initialized...mCountDownLatch.countDown()}// await()之前没有调用countDown()会一直阻塞mCountDownLatch.await()

在使用线程池异步加载时需要考虑一些事情:

任务是否符合需要异步的要求:有些任务可能它就需要在主线程运行,那就需要考虑该任务放弃异步加载或者考虑任务优先级程度选择在主线程延迟加载

需要在某阶段完成:比如任务的数据要及时在闪屏页展示给用户,那么就考虑使用CountDownLatch(后续会用启动器的方式实现)

线程数量控制:比如设备是8核的CPU,计算的设置的线程池核心数量是4,要根据设备的CPU数量动态计算核心线程数量。并且设置了4个核心线程,如果将任务全都放在一个Runnable运行也是不合理的,因为CPU线程没有得到有效的利用

4.1.2 异步启动器

不改变现有启动任务执行逻辑的前提下,启动优化本质上就是解决任务的依赖以及合理的、有序的调度问题。而依赖问题本质就是数据结构问题,合理调度解决的是并发问题

上面讲解使用子线程/线程池的方式也能实现异步初始化,如果需要等待加载完成再使用,还可以使用CountDownLatch锁存器解决。

实际项目一般可能会有多个库,如果还是按照上面的写法一个个去写锁存器等待加载就会比较麻烦,而任务与任务之间如果存在依赖就很难处理。

任务间的依赖关系适合的数据结构是有向无环图,即一个有向图无法从某个顶点出发经过若干条边重新回到该顶点,就是有向无环图,简称 DAG 图。

DAG 常被用来表示事件之间的驱动依赖关系,管理任务之间的调度。并且处理任务之间的顺序执行,还需要完成拓扑排序,拓扑排序是对一个有向图构造拓扑序列的过程。

有向无环图的比喻:学习 OkHttp,需要先学 Java,然后再学 Socket 或设计模式,学完 Socket 后再学 Http 协议,最后才学 OkHttp。

图的概念:

顶点:每个任务 Task 代表的就是顶点

边:任务之间连接的线就是边

出度:从顶点发出的边的数量。例如 Java 有两条边分别连接 Socket 和设计模式,Java 的出度就是 2

入度:连接到顶点的边的数量。例如 Socket 有一条边从 Java 连接的边,Socket 的入度是 1,Java 入度是 0

将图经过拓扑排序为有向无环图的步骤(实际上排序是为了准备两张表:入度表和任务依赖表):

先找出入度为 0 的顶点,然后从图中删除入度为 0 的顶点【第一个入度为 0 的顶点是 Java】

继续找入度为 0 的顶点【Java 被删除后,另外入度为 0 的顶点就是 Socket 和设计模式】

继续找入度为 0 的顶点【Socket 和设计模式被删除后,另外入度为 0 的顶点是 Http 协议,OkHttp 的入度为 1】

继续找入度为 0 的顶点【Http 协议被删除,另外入度为 0 的顶点是 OkHttp】

经过拓扑排序后入度表和依赖任务表如下:

具体的代码实现如下:

object TopologySort {// StartUp 就是任务 Taskfun sort(startupList: List<Startup<*>>): StartupSortStore {// 入度表val inDegreeMap = mutableMapOf<Class<out Startup<*>>, Int>()// 入度为0的任务队列val zeroDeque = ArrayDeque<Class<out Startup<*>>>()val startupMap = mutableMapOf<Class<out Startup<*>>, Startup<*>>()// 任务依赖表val startupChildMap = mutableMapOf<Class<out Startup<*>>, MutableList<Class<out Startup<*>>>>()// 找出图中入度为0的顶点startupList.forEach {startup ->startupMap[startup.javaClass] = startup// 构建入度表// 记录每个任务的入度数(依赖的任务数)val dependenciesCount = startup.getDependenciesCount()inDegreeMap[startup.javaClass] = dependenciesCount// 记录入度数(依赖的任务数)为0的任务if (dependenciesCount == 0) {zeroDeque.offer(startup.javaClass)} else {// 构建任务依赖表// 遍历本任务的依赖(父)任务列表startup.dependencies().forEach {parent ->var child = startupChildMap[parent]if (child == null) {child = mutableListOf()startupChildMap[parent] = child}child.add(startup.javaClass)}}}// 依次在图中删除顶点val result = mutableListOf<Startup<*>>()val mainStartupList = mutableListOf<Startup<*>>()val threadStartupList = mutableListOf<Startup<*>>()while (!zeroDeque.isEmpty()) {val parentCls = zeroDeque.poll()val startup = startupMap[parentCls]!!if (startup.callCreateOnMainThread()) {mainStartupList.add(startup)} else {threadStartupList.add(startup)}// 删除后再找出现在入度为0的顶点if (startupChildMap.containsKey(parentCls)) {val childClsList = startupChildMap[parentCls]childClsList?.forEach {childCls ->val num = inDegreeMap[childCls]!!inDegreeMap[childCls] = num - 1if (num - 1 == 0) {zeroDeque.offer(childCls)}}}}result.apply {// 先添加子线程的任务,再添加主线程任务// 避免要运行在主线程的同步任务先执行导致阻塞下一个要被执行的异步任务启动addAll(threadStartupList)addAll(mainStartupList)}return StartupSortStore(result, startupMap, startupChildMap)}}

具体代码可以参考 demo 项目 startup。

4.2 延迟加载:IdleHandler

延迟加载主要针对的是一些优先级不是很高的任务在某个适当的时机再初始化。

在Android中一般我们需要做延迟处理都会使用Handler.sendEmptyMessageDelay()或者postDelay()

Handler.postDelay({// initialized}, 3000)

但是使用这种方式会有比较明显的问题:

时机不容易控制。任务一般都会比较耗时,UI更新是在主线程,handler.postDelay()需要延迟多久不好控制

导致界面UI卡顿。延时时机不准确,UI在更新绘制过程如果执行了耗时任务就会导致UI卡顿

Android提供了IdleHandler,它在CPU空闲的时候会回调,我们可以在回调时分批执行任务初始化:

Looper.myQueue().addIdleHanddler {// initializedfalse}

在异步加载介绍的异步启动器TaskDispatcher也支持IdleHandler

public class DelayInitDispatcher {private Queue<Task> mDelayTasks = new LinkedList<>();private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {@Overridepublic boolean queueIdle() {if (mDelayTasks.size() > 0) {Task task = mDelayTasks.poll();new DispatchRunnable(task).run();}return !mDelayTasks.isEmpty();}};public DelayInitDispatcher addTask(Task task) {mDelayTasks.add(task);return this;}public void start() {Looper.myQueue().addIdleHandler(mIdleHandler);}public void stop() {Looper.myQueue().removeIdleHandler(mIdleHandler);}}

在合适的时机调用延迟初始化:

DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();delayInitDispatcher.addTask(new InitOtherTask()).start();

使用IdleHandler的优势:

执行时机明确,指定在CPU空闲时才执行回调

缓解界面UI卡顿,不会干扰UI更新绘制布局等过程

4.3 其他启动优化方案

提前加载SharedPreferences

Multidex之前加载,利用此阶段的CPU

覆写getApplicationContext()返回this

启动阶段不启动子进程

子进程会共享CPU资源,导致主进程CPU紧张

注意启动顺序:Application的onCreate()之前是ContentProvider启动调用onCreate()

类加载优化:提前异步类加载

Class.forName()只加载类本身及其静态变量的引用类

如果是new类实例,可以额外加载类成员变量的引用类

启动阶段抑制GC

CPU锁频

5 UI的启动优化途径

按照上面的解决方案对组件做了区队对待处理为异步加载和延迟加载后,启动应用进程让UI更快的显示出来展示给用户也是启动优化其中一个优化途径。

提升Activity的创建速度是优化App启动速度的首要关注目标。从桌面点击App图标启动应用开始,程序会显示一个启动窗口等待Activity的创建加载完毕再进行显示。在Activity的创建加载过程中,会执行很多操作,例如设置页面主题、初始化页面的布局、加载图片、获取网络数据等等。

上述操作的任何一个环节出现性能问题都可能导致画面不能及时显示,影响了程序的启动速度。

那在UI层面怎么提升启动速度呢?

5.1 修改启动主题背景

上图是启动的过程,绝大多数步骤都是由系统控制的,一般不会出现什么问题我们也不需要干预。对于启动速度,我们能够控制优化的主要有三个地方:

Application:在Application.onCreate()通常会在这里做大量的通用组件初始化操作

Activity:在Activity.onCreate()通常会做界面初始化相关的操作,特别是UI布局和渲染操作,如果布局过于复杂很可能导致启动性能降低

闪屏页:部分App会提供自定义的启动窗口,在这个界面展示一些图片宣传等给用户提供一种程序已经启动的视觉效果

系统启动App前会加载显示一个空白的Window窗口,直到页面渲染加载完毕;如果应用程序启动速度够快,空白窗口停留显示时间则会很短,但是当程序启动速度偏慢时,等待时间过长就会降低用户体验甚至让用户放弃使用App。

所以目前大多数的App都会设置闪屏页,通过修改启动窗口主题替换系统默认的启动窗口,让用户在视觉效果上降低启动App等待的时间。但本质上并没有对启动速度做什么优化。

一般闪屏页Activity都是展示一张图片,修改主题非常简单,在res/drawable/提供一个主题背景图片,把这张图片通过设置主题的方式显示为启动闪屏,然后在入口Activity的super.onCreate()之前调用setTheme()替换回来:

res/drawable/splash_bg.xml

<?xml version="1.0" encoding="utf-8"?><layer-list xmlns:android="/apk/res/android"><item android:drawable="@android:color/white" /><item><bitmapandroid:gravity="center"android:src="@drawable/ic_launcher" /></item></layer-list>

res/values/styles.xml

<style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar"><item name="android:windowBackground">@drawable/splash_bg</item></style>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="/apk/res/android"package="com.example.demo"><applicationandroid:name=".MyApplication"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/AppTheme"><activity android:name=".SplashActivity"android:theme="@style/SplashTheme"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application></manifest>

SplashActivity.java

class SplashActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {setTheme(R.style.AppTheme)super.onCreate(savedInstanceState)}}

5.2 UI渲染布局优化

布局优化主要有两个优化方向:

减少过度绘制

减少布局层级

控件延迟加载

具体的优化方式可以参考文章:

Android性能优化系列:VSync、Choreographer和Render Thread

Android性能优化系列:渲染及布局优化

Android一些你需要知道的布局优化技巧

5.3 异步inflate

在实际的项目代码中,你可能已经处理了上面的所有操作,但是启动速度还是没能达到要求,比如启动进入的首页是 ViewPager 需要加载多个 Fragment,而某个或多个 Fragment 在执行onCreateView()使用 LayoutInflater 创建的 View,即使已经做到布局扁平化和某些控件懒加载,但因为控件数量过多导致了耗时过长,onCreateView()的时机我们是不可干预的。

首先需要明白 inflate 它本质上是IO操作,当我们调用layoutInflater.inflate(),该方法主要会有两个操作:

使用递归的方式解析 xml

根据 xml 解析使用反射的方式创建 View

控件越多,递归 xml 解析的IO过程就越久,反射创建 View 越多也越耗时。既然是IO操作,那能否将它放在子线程处理呢?答案是可以的。

Google 已经为我们提供了一个异步 inflate 的API:AsyncLayoutInflater,使用 AsyncLayoutInflater 可以将 inflate 操作放到子线程处理,如果子线程 inflate 失败,就会在主线程执行 inflate,当 inflate 结束后会通过 callback 回调到主线程。

使用方式非常简单:

AsyncLayoutInflater asyncLayoutInflater = new AsyncLayoutInflater(this);asyncLayoutInflater.inflate(R.layout.test_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {@Overridepublic void onInflateFinished(@NonNull View view, int resid, @androidx.annotation.Nullable ViewGroup parent) {// ...}});

在我负责的项目中也是使用了异步 inflate,但是我没有使用 AsyncLayoutInflater,原因主要有两点:

AsyncLayoutInflater 是不能向下兼容的:高版本向下兼容是通过 AppCompat 进行的,而 AsyncLayoutInflater 的 BasicInflater 没有向下兼容:

private static class BasicInflater extends LayoutInflater {private static final String[] sClassPrefixList = {"android.widget.","android.webkit.","android.app."};BasicInflater(Context context) {super(context);}}

不能控制子线程的优先级:在冷启动时如果子线程还没有执行完 inflate 操作,还是会在主线程按正常流程 inflate,可能会有失效的问题

有上述两种问题,在了解了实现思路的情况下,我们完全可以自己写一个类似的功能。下面提供了一个自己写的工具类,仅作为参考:

public class AsyncInflateManager {private static final String TAG = "AsyncInflateManager";private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();// We want at least 2 threads and at most 4 threads in the core pool,// preferring to have 1 less than the CPU count to avoid saturating// the CPU with background workprivate static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));private ThreadPoolExecutor mExecutors;public static final int TAG_TEST1 = 1;public static final int TAG_TEST2 = 2;public static final int TAG_TEST3 = 3;private SparseArray<View> mInflateFinishedViews = new SparseArray<>();private SparseBooleanArray mMainInflatedTags = new SparseBooleanArray();private static final class AsyncInflateManagerHolder {private static final AsyncInflateManager sInstance = new AsyncInflateManager();}private AsyncInflateManager() {mExecutors = new ThreadPoolExecutor(CORE_POOL_SIZE, CORE_POOL_SIZE, 8, TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(), new ThreadFactory() {private AtomicInteger mCount = new AtomicInteger();@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r, "AsyncInflate-Thread-" + mCount.incrementAndGet());thread.setPriority(Thread.MAX_PRIORITY); // 设置线程优先级,根据实际需求场景需要设置,仅参考return thread;}});mExecutors.allowCoreThreadTimeOut(true); // 如果仅用于冷启动,可以使用完后也将核心线程池回收}public static AsyncInflateManager getInstance() {return AsyncInflateManagerHolder.sInstance;}/*** 批量异步inflate布局*/public void batchAsyncInflate(Context context, List<AsyncInflateInfo> asyncInflateInfoList) {if (CollectionUtils.isEmpty(asyncInflateInfoList)) return;// 每个inflate都在一个单独线程异步并行执行for (AsyncInflateInfo asyncInflateInfo : asyncInflateInfoList) {if (isTagInvalid(asyncInflateInfo.tag)) continue;asyncInflate(context, asyncInflateInfo);}}/*** 将耗时的布局inflate切换到子线程执行并临时存储*/public void asyncInflate(Context context, AsyncInflateInfo asyncInflateInfo) {mExecutors.submit(new AsyncInflateRunnable(LayoutInflater.from(context), asyncInflateInfo));}private class AsyncInflateRunnable implements Runnable {private LayoutInflater inflater;private AsyncInflateInfo asyncInflateInfo;AsyncInflateRunnable(LayoutInflater inflater, AsyncInflateInfo asyncInflateInfo) {this.inflater = inflater;this.asyncInflateInfo = asyncInflateInfo;}@Overridepublic void run() {doAsyncInflate(inflater, asyncInflateInfo);}}private void doAsyncInflate(@NonNull LayoutInflater inflater, AsyncInflateInfo asyncInflateInfo) {View inflatedView = = inflater.inflate(asyncInflateInfo.layoutResId, null, false);// 如果主线程已经创建了,就不需要再存储进列表了if (!mMainInflatedTags.get(asyncInflateInfo.tag)) {mInflateFinishedViews.put(asyncInflateInfo.tag, inflatedView);}}public View getInflatedView(int tag) {if (isTagInvalid(tag)) return null;View inflatedView = mInflateFinishedViews.get(tag);// 异步inflate预加载的view获取后就移除if (inflatedView != null) {removeInflatedView(tag);} else {putTagIfMainInflated(tag);}return inflatedView;}private void putTagIfMainInflated(int tag) {if (isTagInvalid(tag)) return;mMainInflatedTags.put(tag, true);}public void removeInflatedView(int tag) {if (isTagInvalid(tag)) return;mInflateFinishedViews.remove(tag);}private boolean isTagInvalid(int tag) {return tag <= 0;}public static class AsyncInflateInfo {@LayoutRes int layoutResId;int tag;public AsyncInflateInfo(@LayoutRes int layoutResId, int tag) {this(layoutResId, tag);}}}public class MyApplication extends Application {@Overridepublic void onCreate() {super.onCreate();List<AsyncInflateManager.AsyncInflateInfo> asyncInflateInfoList = new ArrayList<>();asyncInflateInfoList.add(new AsyncInflateManager.AsyncInflateInfo(R.layout.test_layout1, AsyncInflateManager.TAG_TEST1));asyncInflateInfoList.add(new AsyncInflateManager.AsyncInflateInfo(R.layout.test_layout2, AsyncInflateManager.TAG_TEST2));asyncInflateInfoList.add(new AsyncInflateManager.AsyncInflateInfo(R.layout.test_layout3, AsyncInflateManager.TAG_TEST3));AsyncInflateManager.getInstance().batchAsyncInflate(this, asyncInflateInfoList);}}public abstract class BaseAsyncInflateFragment extends Fragment {@Nullable@Overridepublic View onCreateView(@NonNull final LayoutInflate inflater, @Nullable final ViewGroup container, @Nullable Bundle savedInstanceState) {View inflatedView = AsyncInflateManager.getInstance().getInflatedView(getAsyncInflatedTag());// 子线程拿不到,就从主线程创建if (inflatedVIew == null) {inflatedView = inflater.inflate(getLayoutId(), container, false);}return inflatedView;}protected abstract int getAsyncInflatedTag();}

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。