探索 Android 启动优化方法

首图

目录.png

前言

1. 启动优化的意义

假如咱们去到一家餐厅,叫了半天都没有人过来点菜,那等不了多久就没耐心想走了。html

对于 App 也是同样的,若是咱们打开一个应用半天都打不开,那很快的咱们也会失去耐心。python

启动速度是用户对咱们应用的第一体验,用户只有启动咱们的应用才能使用咱们应用中的功能。android

就算咱们应用内部设计得再精美,其余性能优化地再好,若是打开速度很慢的话,用户对咱们应用的第一印象仍是不好。git

你能够追求完美,要作到应用在 1 毫秒内启动。github

可是通常状况下, 咱们只要作到超越竞品或者远超竞品,就能在启动速度这一个点上让用户满意。浏览器

用户选择 App 的时候会考虑各类因素,而咱们 App 开发者能作的就是在争取经过各类技术让咱们的 App 从众多竞品中脱颖而出。性能优化

2. 文章内容

这篇文章分为三部分。服务器

  • 第一部分:启动优化基础多线程

    第一部分是第 1 大节,讲的是应用启动流程的相关知识。并发

  • 第二部分:启动优化方法

    第二部分是第 2~4 大节,讲的是经常使用的优化启动速度的工具和方法。

  • 第三部分:优化方法改进

    第三部分是第 5~7 大节,讲的是常规优化启动方法的改进型解决方案。

1. 三种启动状态

启动速度对 App 的总体性能很是重要,因此谷歌官方给出了一篇启动速度优化的文章。

在这篇文章中,把启动分为了三种状态:热启动、暖启动和冷启动。

下面咱们来看下三种启动状态的特色。

1.1 热启动

热启动是三种启动状态中是最快的一种,由于热启动是从后台切到了前台,不须要再建立 Applicaiton,也不须要再进行渲染布局等操做。

1.2 暖启动

暖启动的启动速度介于冷启动和热启动之间,暖启动只会重走 Activity 的生命周期,不会重走进程建立和 Application 的建立和生命周期等。

1.3 冷启动

冷启动经历了一系列流程,耗时也是最多的,理解冷启动总体流程的理解,能够帮助咱们寻找以后的一个优化方向。

冷启动也是优化的衡量标准,通常在线上进行的启动优化都是以冷启动速度为指标的。

启动速度的优化方向是 Application 和 Activity 生命周期阶段,这是咱们开发者能控制的时间,其余阶段都是系统作的。

冷启动流程能够分为三步:建立进程、启动应用和绘制界面。

  1. 建立进程

    建立进程阶段主要作了下面三件事,这三件事都是系统作的。

    • 启动 App
    • 加载空白 Window
    • 建立进程
  2. 启动应用

    启动应用阶段主要作了下面三件事,从这些开始,随后的任务和咱们本身写的代码有必定的关系。

    • 建立 Application
    • 启动主线程
    • 建立 MainActivity
  3. 绘制界面

    绘制界面阶段主要作了下面三件事。

    • 加载布局
    • 布置屏幕
    • 首帧绘制

2. 两种测量方法

上一节介绍了三种启动状态,这一节咱们来看一下经常使用的两种测量启动时间的方法:命令测量和埋点测量。

2.1 命令测量

命令测量指的是用 adb 命令测量启动时间,经过下面两步就能实现 adb 命令测量应用启动时间

  1. 输入测量命令
  2. 分析测量结果

2.2.1 输入测量命令

咱们在终端中输入一条 adb 命令打开咱们要测量的应用,打开后系统会输出应用的启动时间。

下面就是测量启动时间的 adb 命令。

adb 命令1.png

首屏 Activity 也要加上包名,好比下面这样的。

adb 命令2.png

2.2.2 分析测量结果

adb 输出结果.png

上面是命令执行完成后显示的内容,在输出中能够看到三个值:ThisTime、TotalTime 和 WaitTime。

下面咱们来看下这三个值分别表明什么。

  • ThisTime

    ThisTime 表明最后一个 Activity 启动所须要的时间,也就是最后一个 Activity 的启动耗时。

  • TotalTime

    TotalTime 表明全部 Activity 启动耗时,在上面的输出中,TotalTime 和 ThisTime 是同样的,由于这个 Demo 没有写 Splash 界面。

    也就是这个 App 打开了 Application 后就直接打开了 MainActivity 界面,没有启动其余页面。

  • WaitTime

    WaitTime 是 AMS 启动 Activity 的总耗时。

这三者之间的关系以下。

ThisTime <= TotalToime < WaitTime

2.2 埋点测量

埋点测量指的是咱们在应用启动阶段埋一个点,在启动结束时再埋一个点,二者之间的差值就是 App 的启动耗时。

经过下面三步能够实现埋点测量。

  1. 定义埋点工具类
  2. 记录启动时间
  3. 计算启动耗时

2.2.1 定义埋点工具类

使用埋点测量的第一步是定义一个记录埋点工具类。

在这里要注意的是,除了 System.currentTimeMillis() 之外,咱们还能够用 SystemClock.currentThreadTimeMillis() 记录时间。

经过 SystemClock 拿到的是 CPU 真正执行的时间,这个时间与下一大节要讲的 Systrace 上记录的时间点是同样的。

LaunchTimer.png

2.2.2 记录启动时间

使用埋点测量的第二步是记录启动时间。

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

记录启动时间.png

2.2.3 计算启动耗时

计算启动耗时的一个误区就是在 onWindowFocusChanged 方法中计算启动耗时。

onWindowFocusChanged 方法只是 Activity 的首帧时间,是 Activity 首次进行绘制的时间,首帧时间和界面完整展现出来还有一段时间差,不能真正表明界面已经展示出来了。

按首帧时间计算启动耗时并不许确,咱们要的是用户真正看到咱们界面的时间。

正确的计算启动耗时的时机是要等真实的数据展现出来,好比在列表第一项的展现时再计算启动耗时。

在 Adapter 中记录启动耗时要加一个布尔值变量进行判断,避免 onBindViewHolder 方法被屡次调用致使没必要要的计算。

计算启动耗时.png

2.3 小结

2.3.1 命令测量优缺点

  • 命令测量优势

    • 线下使用方便

      adb 命令测量启动速度的方式在线下使用比较方便,并且这种方式还能用于测量竞品。

  • 命令测量缺点

    • 不能带到线上

      若是一条 adb 命令带到线上去,没有 app 也没有系统帮咱们执行这一条 adb 命令,咱们就拿不到这些数据,因此不能带到线上。

    • 不严谨和精确

      不能精确控制启动时间的开始和结束。

2.3.2 埋点测量的特色

  • 精确

    手动打点的方式比较精确,由于咱们能够精确控制开始和结束的位置。

  • 可带到线上

    使用埋点测量进行用户数据的采集,能够很方便地带到线上,把数据上报给服务器。

    服务器能够针对全部用户上报的启动数据,天天作一个整合,计算出一个平均值,而后对比不一样版本的启动速度。

3. 两个分析工具

经常使用的分析方法耗时的工具备 Systrace 和 Traceview,它们两个是相互补充的关系,咱们要在不一样的场景下使用不一样的工具,这样才能发挥工具的最大做用。

本节内容以下。

  • Traceview
  • Systrace
  • 小结

3.1 Traceview

Traceview 能以图形的形式展现代码的执行时间和调用栈信息,并且 Traceview 提供的信息很是全面,由于它包含了全部线程。

Traceview 的使用能够分为两步:开始跟踪、分析结果。

下面咱们来看看这两步的具体操做。

3.1.1 开始跟踪

咱们能够经过 Debug.startMethodTracing("输出文件") 就能够开始跟踪方法,记录一段时间内的 CPU 使用状况。

当咱们调用了 Debug.stopMethodTracing() 中止跟踪方法后,系统就会为咱们生成一个文件,咱们能够经过 Traceview 查看这个文件记录的内容。

文件生成的位置在 Android/data/包名/files 下,下面咱们来看一个示例。

咱们在 Application 的 onCreate 方法的开头开始追踪方法,而后在结尾结束追踪,在这里只是对 BlockCanary 卡顿监测框架进行初始化。

startMethodTracing.png

startMethodTracing 方法真正调用的实际上是另外一个重载方法,在这个重载方法能够传入 bufferSize。

bufferSize 就是分析结果文件的大小,默认是 8 兆。

咱们能够进行扩充,好比扩充为 16 兆、32 兆等。

这个重载方法的第三个参数是标志位,这个标志位只有一个选项,就是 TRACE_COUNT_ALLOCS。

startMethodTracing2.png

3.1.2 分析结果

运行了程序后,有两种方式能够获取到跟踪结果文件。

第一种方式是经过下面的命令把文件拉到项目根目录。

pull 跟踪结果.png

第二种方式是在 AS 右下方的文件资源管理器中定位到 /sdcard/android/data/包名/files/ 目录下,而后本身找个地方保存。

文件资源管理器.png

咱们在 AS 中打开跟踪文件 mytrace.trace 后,就能够用 Profiler 查看跟踪的分析结果。

查看跟踪文件.png

在分析结果上比较重要的是 5 种信息。

  • 代码指定的时间范围

    这个时间范围是咱们经过 Debug 类精确指定的

  • 选中的时间范围

    咱们能够拖动时间线,选择查看一段时间内某条线程的调用堆栈

  • 进程中存在的线程

    在这里能够看到在指定时间范围内进程中只有主线程和 BlockCanary 的线程,一共有 4 条线程。

  • 调用堆栈

    在上面的跟踪信息中,我选中了 main,也就是主线程。

    还把时间范围缩小到了特定时间区域内,放大了这个时间范围内主线程的调用堆栈信息

  • 方法耗时

    当咱们把鼠标放到某一个方法上的时候,咱们能够看到这个方法的耗时,好比上面的 initBlockCanary 的耗时是 19 毫秒。

3.2 Systrace

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

Systrace 提供的 Trace 工具类默认只能 API 18 以上的项目中才能使用,若是咱们的兼容版本低于 API 18,咱们可使用 TraceCompat。

Systrace 的使用步骤和 Traceview 差很少,分为下面两步。

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

3.2.2 调用跟踪方法

首先在 Application 中调用 Systrace 的跟踪方法。

beginSection.png

而后链接设备,在终端中定位到 Android SDK 目录下,好比个人 Android SDK 目录在 /users/oushaoze/library/Android/sdk 。

这时候我打开 SDK 目录下的 platform-tools/systrace 就能看到 systrace.py 的一个 python 文件。

Systrace 是一个 Python 脚本,输入下面命令,运行 systrace ,开始追踪系统信息。

systrace命令.png

这行命令附加了下面一些选项。

  • -t ...

    -t 后面表示的是跟踪的时间,好比上面设定的是 10 秒就结束。

  • -o ...

    -o 后面表示把文件输出到指定目录下。

  • -a ...

    -a 后面表示的是要启动的应用包名

输入完这行命令后,能够看到开始跟踪的提示。看到 Starting tracing 后能够打开打开咱们的应用。

10 秒后,会看到 Wrote trace HTML file: ....。

systrace输出.png

上面这段输出就是说追踪完毕,追踪到的信息都写到 trace.html 文件中了,接下来咱们打开这个文件。

3.2.3 查看跟踪结果

Systrace.png

打开文件后咱们能够看到上面这样的一个视图,在这里有几个须要特别关注的地方。

  • 8 核

    我运行 Systrace 的设备是 8 核的,因此这里的 Kernel 下面是 8 个 CPU。

  • 缩放

    当咱们选中缩放后,缩放的方式是上下移动,不是左右移动。

  • 移动

    选择移动后,咱们能够拖动咱们往下查看其它进程的分析信息。

  • 时间片使用状况

    时间片使用状况指的是各个 CPU 在特定时间内的时间片使用状况,当咱们用缩放把特定时间段内的时间片信息放大,咱们就能够看到时间片是被哪一个线程占用了。

  • 运行中的进程

    左侧一栏除了各个内核外,还会显示运行中的进程。

咱们往下移动,能够看到 MyAppplication 进程的线程活动状况。

Systrace_myapp.png

在这个视图上咱们主要关注三个点。

  • 主线程

    在这里咱们主要关注主线程的运行了哪些方法

  • 跟踪的时间段

    刚才在代码中设置的标签是 AppOnCreate,在这里就显示了这个跟踪时间段的标签

  • 耗时

    咱们选中 AppOnCreate 标签后,就能够看到这个方法的耗时。

    在 Slice 标签下的耗时信息包括 Wall Duration 和 CPU Duration,下面是它们的区别。

    • Wall Duration

      Wall Time 是执行这段代码耗费的时间,不能做为优化指标。

      假如咱们的代码要进入锁的临界区,若是锁被其余线程持有,当前线程就进入了阻塞状态,而等待的时间是会被计算到 Wall Time 中的。

    • CPU Duration

      CPU Duration 是 CPU 真正花在这段代码上的时间,是咱们关心的优化指标。

      在上面的例子中 Wall Duration 是 84 毫秒,CPU Duration 是 34 毫秒,也就是在这段时间内一共有 50 毫秒 CPU 是处于休息状态的,真正执行代码的时间只花了 34 毫秒。

3.3 小结

3.3.1 Traceview 的两个特色

Traceview 有两个特色:可埋点、开销大。

  • 可埋点

    Traceview 的好处之一是能够在代码中埋点,埋点后能够用 CPU Profiler 进行分析。

    由于咱们如今优化的是启动阶段的代码,若是咱们打开 App 后直接经过 CPU Profiler 进行记录的话,就要求你有单身三十年的手速,点击开始记录的时间要和应用的启动时间彻底一致。

    有了 Traceview,哪怕你是老年人手速也能够记录启动过程涉及的调用栈信息。

  • 开销大

    Traceview 的运行时开销很是大,它会致使咱们程序的运行变慢。

    之因此会变慢,是由于它会经过虚拟机的 Profiler 抓取咱们当前全部线程的全部调用堆栈。

    由于这个问题,Traceview 也可能会带偏咱们的优化方向。

    好比咱们有一个方法,这个方法在正常状况下的耗时不大,可是加上了 Traceview 以后可能会发现它的耗时变成了原来的十倍甚至更多。

3.3.2 Systrace 的两个特色

Systrace 的两个特色:开销小、直观。

  • 开销小

    Systrace 开销很是小,不像 Traceview,由于它只会在咱们埋点区间进行记录。

    而 Traceview 是会把全部的线程的堆栈调用状况都记录下来。

  • 直观

    在 Systrace 中咱们能够很直观地看到 CPU 利用率的状况。

    当咱们发现 CPU 利用率低的时候,咱们能够考虑让更多代码以异步的方式执行,以提升 CPU 利用率。

3.3.3 Traceview 与 Systrace 的两个区别

  • 查看工具

    Traceview 分析结果要使用 Profiler 查看。

    Systrace 分析结果是在浏览器查看 HTML 文件。

  • 埋点工具类

    Traceview 使用的是 Debug.startMethodTracing()。

    Systrace 用的是 Trace.beginSection() 和 TraceCompat.beginSection()。

4. 两种优化方法

经常使用的两种优化方法有两种,这两种是能够结合使用的。

第一种是闪屏页,在视觉上让用户感受启动速度快,第二种是异步初始化。

4.1 闪屏页

闪屏页是优化启动速度的一个小技巧,虽然对实际的启动速度没有任何帮助,可是能让用户感受比启动的速度要快一些。

闪屏页就是在 App 打开首屏 Activity 前,首先显示一张图片,这张图片能够是 Logo 页,等 Activity 展现出来后,再把 Theme 变回来。

冷启动的其中一步是建立一个空白 Window,闪屏页就是利用这个空白 Window 显示占位图。

经过下面四个步骤能够实现闪屏页。

  1. 定义闪屏图
  2. 定义闪屏主题
  3. 设置主题
  4. 换回主题

4.1.1 定义闪屏图

第一步是在 drawable 目录下建立一个 splash.xml 文件。

splash.xml.png

4.1.2 定义闪屏主题

第二步是在 values/styles.xml 中定义一个 Splash 主题。

SplashTheme.png

4.1.3 设置主题

第三步是在清单文件中设置 Theme。

ActivityTheme.png

4.1.4 换回主题

第四步是在调用 super.onCreate 方法前切换回来

换回主题.png

4.2 异步初始化

咱们这一节来看一下怎么用线程池进行异步初始化。

本节内容包括以下部分,

  • 异步初始化简介
  • 线程池大小
  • 线程池基本用法

4.2.1 异步初始化简介

异步优化就是把初始化的工做分细分红几个子任务,而后让子线程分别执行这些子任务,加快初始化过程。

若是你对怎么在 Android 中实现多线程不了解,能够看一下个人上一篇文章:探索 Android 多线程优化,在这篇文章中我对在 Android 使用多线程的方法作了一个简单的介绍。

有些初始化代码在子线程执行的时候可能会出现问题,好比要求在 onCreate 结束前执行完成。

这种状况咱们能够考虑使用 CountDownLatch 实现,实在不行的时候就保留这段初始化代码在主线程中执行。

4.2.2 线程池大小

咱们可使用线程池来实现异步初始化,使用线程池须要注意的是线程池大小的设置。

线程池大小要根据不一样的设备设置不一样的大小,有的手机是四核的,有的是八核的,若是把线程池大小设为固定数值的话是不合理的。

咱们能够参考 AsyncTask 中设置的线程池大小,在 AsyncTask 中有 CPU_COUNT 和 CORE_POOL_SIZE。

  • CPU_COUNT

    CPU_COUNT 的值是设备的 CPU 核数。

  • CORE_POOL_SIZE

    CORE_POOL_SIZE 是线程池核心大小,这个值的最小值是 2,最大值是 Math.min(CPU_COUNT - 1, 4)。

    当设备的核数为 8 时,CORE_POOL_SIZE 的值为 4,当设备核数为 4 时,这个值是 3,也就是 CORE_POOL_SIZE 的最大值是 4。

4.2.3 线程池基本用法

在这里咱们能够参考 AsyncTask 的作法来设置线程池的大小,并把初始化的工做提交到线程池中。

线程池.png

6. 改进优化方案

上一节介绍了怎么经过线程池处理初始化任务,这一节咱们看一下改进的异步初始化工具:启动器(LaunchStarter)。

这一节的内容包括以下部分。

  • 线程池实现的不足
  • 启动器简介
  • 启动器工做流程
  • 实现任务等待执行
  • 实现任务依赖关系

6.1 线程池实现的不足

经过线程池处理初始化任务的方式存在三个问题。

  • 代码不够优雅

    假如咱们有 100 个初始化任务,那像上面这样的代码就要写 100 遍,提交 100 次任务。

  • 没法限制在 onCreate 中完成

    有的第三方库的初始化任务须要在 Application 的 onCreate 方法中执行完成,虽然能够用 CountDownLatch 实现等待,可是仍是有点繁琐。

  • 没法实现存在依赖关系

    有的初始化任务之间存在依赖关系,好比极光推送须要设备 ID,而 initDeviceId() 这个方法也是一个初始化任务。

6.2 启动器简介

启动器的核心思想是充分利用多核 CPU ,自动梳理任务顺序。

第一步是咱们要对代码进行任务化,任务化是一个简称,好比把启动逻辑抽象成一个任务。

第二步是根据全部任务的依赖关系排序生成一个有向无环图,这个图是自动生成的,也就是对全部任务进行排序。

好比咱们有个任务 A 和任务 B,任务 B 执行前须要任务 A 执行完,这样才能拿到特定的数据,好比上面提到的 initDeviceId。

第三步是多线程根据排序后的优先级依次执行,好比咱们如今有三个任务 A、B、C。

假如任务 B 依赖于任务 A,这时候生成的有向无环图就是 ACB,A 和 C 能够提早执行,B 必定要排在 A 以后执行。

6.3 启动器工做流程

启动器流程图.png

  • Head Task

    Head Task 就是全部任务执行前要作的事情,在这里初始化一些其余任务依赖的资源,也能够只是打个 Log。

  • Tail Task

    Tail Task 可用于执行全部任务结束后打印一个 Log,或者是上报数据等任务。

  • Idle Task

    Idle Task 是在程序空闲时执行的任务。

若是咱们不使用异步的方案,全部的任务都会在主线程执行。

为了让其余线程分担主线程的工做,咱们能够把初始化的工做拆分红一个个的子任务,采用并发的方式,使用多个线程同时执行这些子任务。

6.4 实现任务等待执行

启动器(LaunchStarter)使用了有向无环图实现任务之间的依赖关系,具体的代码能够在本文最下方找到。

使用启动器须要完成 3 个步骤。

  • 添加依赖
  • 定义任务
  • 开始任务

下面咱们来看下这 3 个步骤的具体操做。

6.4.1 添加依赖

首先在项目根目录的 build.gradle 中添加 jitpack 仓库。

allprojects {
  repositories {
    // ...
    maven { url 'https://jitpack.io' }
  }
}
复制代码

而后在 app 模块的 build.gradle 中添加依赖

dependencies {
  // 启动器
  implementation 'com.github.zeshaoaaa:LaunchStarter:0.0.1'
}  
复制代码

6.4.2 定义任务

定义任务这个步骤涉及了几个概念:MainTask、Task、needWait 和 run。

  • MainTask

    MainTask 是须要在主线程执行的任务

  • Task

    Task 就是在工做线程执行的任务。

  • needWait

    InitWeexTask 中重写了 needWait 方法,这个方法返回 true 表示 onCreate 的执行须要等待这个任务完成。

  • run

    run() 方法中的代码就是须要作的初始化工做

InitWeexTask.png

6.4.3 开始任务

定义好了任务后,咱们就能够开始任务了。

这里须要注意的是,若是咱们的任务中有须要等待完成的任务,咱们能够调用 TaskDispatcher 的 await() 方法等待这个任务完成,好比 InitWeexTask。

使用 await() 方法要注意的是这个方法要在 start() 方法调用后才能使用。

startDispatcher.png

6.5 实现任务依赖关系

除了上面提到的等待功能之外,启动器还支持任务之间存在依赖关系,下面咱们来看一个极光推送初始化任务的例子。

在这一节会讲实现任务依赖关系的两个步骤。

  • 定义任务
  • 开始任务

6.5.1 定义任务

在这里咱们定义两个存在依赖关系的任务:GetDeviceIdTask 和 InitJPush Task。

首先定义 GetDeviceIdTask ,这个任务负责初始化设备 ID 。

GetDeviceIdTask.png

而后定义InitJPushTask,这个任务负责初始化极光推送 SDK,InitJPushTask 在启动器中是尾部任务 Tail Task。

InitJPushTask 依赖于 GetDeviceIdTask,因此须要重写 dependsOn 方法,在 dependsOn 方法中建立一个 Class 列表,把想依赖的任务的 Class 添加到列表中并返回。

InitJPushTask.png

6.5.2 开始任务

GetDeviceIdTask 和 InitJPushTask 这两个任务都不须要等待 Application 的 onCreate 方法执行完成,因此咱们这里不须要调用 TaskDispatcher 的 await 方法。

startTask.png

6.5.3 小结

上面这两个步骤就能实现经过启动器实现任务之间的依赖关系。

7. 延迟执行任务

在咱们应用的 Application 和 Activity 中可能存在部分优先级不高的初始化任务,咱们能够考虑把这些任务进行延迟初始化,好比放在列表的第一项显示出来后再进行初始化。

常规的延迟初始化方法有两种:onPreDraw 和 postDelayed。

除了常规方法外,还有一种改进的延迟初始化方案:延迟启动器。

本节包括以下内容。

  • onPreDraw

    onPreDraw 指的是在列表第一项显示后,在 onPreDraw 回调中执行初始化任务

  • postDelayed

    经过 Handler 的 postDelayed 方法延迟执行初始化任务

  • 延迟启动器

7.1 onPreDraw

这一节咱们来看下怎么经过 OnPreDrawListener 把任务延迟到列表显示后再执行。

下面是 onPreDraw 方式实现延迟初始化的 3 个步骤。

  • 声明回调接口
  • 调用接口方法
  • 在 Activity 中监听
  • 小结

7.1.1 声明回调接口

第一步先声明一个 OnFeedShowCallback。

OnFeedShowCallback.png

7.1.2 调用接口方法

第二步是在 Adapter 中的第一条显示的时候调用 onFeedShow() 方法。

AdapterOnFeedShow.png

7.1.3 在 Activity 中监听

第三步是在 Activity 中调用 setOnFeedCallback 方法。

ActivityOnFeedShow.png

7.1.4 小结

直接在 onFeedShow 中执行初始化任务的弊端是有可能致使滑动卡顿。

若是咱们 onPreDraw 的方式延迟执行初始化任务,假如这个任务耗时是 2 秒,那就意味着在列表显示第一条后的 2 秒内,列表是没法滑动的,用户体验不好。

7.2 postDelayed

还有一种方式就是经过 Handler.postDelayed 方法发送一个延迟消息,好比延迟到 100 毫秒后执行。

假如在 Activity 中有 1 个 100 行的初始化方法,咱们把前 10 行代码放在 postDelayed 中延迟 100 毫秒执行,把前 20 行代码放在 postDelayed 中延迟 200 毫秒执行。

这种实现的确缓解了卡顿的状况,可是这种实现存在两个问题

  • 不够优雅

    假如按上面的例子,能够分出 10 个初始化任务,每个都放在 不一样的 postDelayed 中执行,这样写出来的代码不够优雅。

  • 依旧卡顿

    假如把任务延迟 200 毫秒后执行,而 200 后用户还在滑动列表,那仍是会发生卡顿。

7.3 延迟启动器

7.3.1 延迟启动器基本用法

除了上面说到的方式外,如今咱们来讲一个更好的解决方案:延迟启动器。

延迟启动器利用了 IdleHandler 实现主线程空闲时才执行任务,IdleHandler 是 Android 提供的一个类,IdleHandler 会在当前消息队列空闲时才执行任务,这样就不会影响用户的操做了。

假如如今 MessageQueue 中有两条消息,在这两条消息处理完成后,MessageQueue 会通知 IdleHandler 如今是空闲状态,而后 IdleHandler 就会开始处理它接收到的任务。

DelayInitDispatcher 配合 onFeedShow 回调来使用效果更好。

下面是一段使用延迟启动器 DelayInitDispatcher 执行初始化任务的示例代码。

DelayInitDispatcher.png

结语

看完了上面提到的一些启动优化技巧,你有没有获得一些启发呢?

又或者是你有没有本身的一些启动优化技巧,不妨在评论区给你们说说。

可能你以为不值一提的技巧,能解决了其余同窗的一个大麻烦。

参考文献

1. 视频

  1. 国内Top团队大牛带你玩转Android性能分析与优化

2. 文章

  1. App startup time
  2. Android App 冷启动优化方案
  3. 使用 CPU Profiler 检查 CPU Activity 和函数跟踪
  4. Overview of Systrace

其余

启动器源码

相关文章
相关标签/搜索