1 说在前面

卡顿的场景可以分为 UI,应用启动,跳转,响应。

这又可以分为两大类界面绘制,数据处理。

  • 页面绘制

    主要原因是绘制的层级深、⻚⾯复杂、刷新不合理, 由于这些原因导致卡顿的场景更多出现在 UI 和启动后的初始界⾯以及 跳转到⻚⾯的绘制上。

  • 数据处理

    数据处理量太⼤,⼀般分 为三种情况,⼀是数据处理在 UI 线程(这种应该避免),⼆是数据处 理占⽤ CPU ⾼,导致主线程拿不到时间⽚,三是内存增加导致 GC 频 繁,从⽽引起卡顿。

本章是通过优化UI界面编程来减少卡顿。

2 安卓系统显示原理

Android 应⽤程序把经过测量、布局、绘制后的 surface 缓存数据,通过 SurfaceFlinger 把数据渲染到显⽰屏幕上,通过 Android 的刷新机制来刷新数据。也就是说应⽤层负责绘制,系统层负责渲染,通过进程间通信把应⽤层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕。

Android 的图形显⽰系统采⽤的是 Client/Server 架构。 SurfaceFlinger ( Server )由 C++ 代码编写。 Client 端代码分为两部分,⼀部分是由 Java 提供给应⽤ 使⽤的 API ,另⼀部分则是由 C++ 写成的底层具体实现。

2.1 应用层

一个 Activity 中有很多不同层级的基本元素 View ,有不同的层句与嵌套关系,并且,当我们进行渲染的时候,对每一个 View 都要进行一次绘制,每个 View 的操作都是递归的。

在绘制一个 View 的时候有三个步骤:

通过 Measure 和 Lyout 来确定当前 View 的大小与位置,通过绘制(Draw)到 surface,由于是递归,所以层级越深,元素越多,耗时就越长。

  1. Measure

    用深度递归原色递归得到所有的视图的宽高,获取当前 View 的宽高之后,就可以调佣成员函数 Measure 来是设置他的大小。如果当前正在测的 View 的子视图也是一个容器,那就重复测量,直到测量完毕。

  2. Layout

    也是深度递归优先,可以获取到所有 View 的位置,当一个 View 的位置确定之后,我们就可以结合前面的 Measure 来确定在程序窗口中的位置。

  3. Draw

    目前 Android 有两种绘制方式,软件绘制(CPU),硬件加速(GPU),后者的效率远远的超过前者,但是后者也有缺点:

    • 比 CPU 耗电

    • 存在兼容性问题,有的接口不支持硬件加速

    • 内存使用量大,使用 Opengl 的接口至少要8MB内存

    所以我们可以结合实际来看,比如电视,不在乎耗电之类的,就可以使用过硬件加速。

2.2 系统层

把需要显示的数据渲染到屏幕,通过系统级进程 SurfaceFlinger 服务来实现的。这个服务做的工作如下所示:

  • 响应客户端事件,创建 Layout 与客户端的 Suface(可以理解为画布上的具体内容) 连接

  • 接收客户端属性与数据,修改Layer(管理与显示图形内容的画布)属性,比如尺寸,颜色,透明度。

  • 将创建的 Layer 内容刷新到屏幕上

  • 维持 Layer 的序列,并对 Layer 的最终输出做裁剪计算。

我们之前将了是 CS 架构,不同进程。所以使用了 Android匿名共享内存:SharedClient,在每个应用与 SurfaceFlinger 之间都有一个 SharedClient ,每个 SharedClient 中最多可以创建31个 SharedBufferStack ,每个 Surface 都对应一个 SharedBufferStack ,也就是一个window。

⼀个 SharedClient 对应⼀个 Android 应⽤程序,⽽⼀个 Android 应⽤ 程序可能包含多个窗⼝,即 Surface 。也就是说 SharedClient 包含的是 SharedBufferStack 的集合。因为最多可以创建 31 个 SharedBufferStack , 这也意味着⼀个 Android 应⽤程序最多可以包含 31 个窗⼝,同时每个 SharedBufferStack 中⼜包含了两个(低于 4.1 版本)或者三个( 4.1 及以 上版本)缓冲区,即在后⾯的显⽰刷新机制中会提到的双缓冲和三重 缓冲技术。

在上图可以看出,绘制过程⾸先是 CPU 准备数据,通过 Driver 层把数据交给 CPU 渲染,其中 CPU 主要负责 Measure 、 Layout 、 Record 、 Execute 的数据计算⼯作, GPU 负责 Rasterization (栅格化)、 渲染。由于图形 API 不允许 CPU 直接与 GPU 通信,⽽是通过中间的⼀个 图形驱动层( Graphics Driver )来连接这两部分。图形驱动维护了⼀个 队列, CPU 把 display list 添加到队列中, GPU 从这个队列取出数据进⾏ 绘制,最终才在显⽰屏上显⽰出来。

知道了绘制的原理后,那么到底绘制⼀个单元多⻓时间才是合理 的,⾸先需要了解⼀个名词: FPS 。 FPS ( Frames Per Second )表⽰每 秒传递的帧数。在理想情况下, 60 FPS 就感觉不到卡,这意味着每个 绘制时⻓应该在 16ms 以内,如下图所示:

但是 Android 系统很有可能⽆法及时完成那些复杂的界⾯渲染操 作。 Android 系统每隔 16ms 发出 VSYNC 信号,触发对 UI 进⾏渲染,如 果每次渲染都成功,这样就能够达到流畅的画⾯所需的 60FPS 。即为了 实现 60FPS ,就意味着程序的⼤多数绘制操作都必须在 16ms 内完成。

如果某个操作花费的时间是 24ms ,系统在得到 VSYNC 信号时就⽆ 法进⾏正常渲染,这样就发⽣了丢帧现象。那么⽤户在 32ms 内看到的 会是同⼀帧画⾯。主要场景在执⾏动画或者滑动 ListView 时更容易感知 到卡顿不流畅,是因为这⾥的操作相对复杂,容易发⽣丢帧的现象, 从⽽感觉卡顿。有很多原因可以导致 CPU 或者 GPU 负载过重从⽽出现 丢帧现象:可能是你的 Layout 太过复杂,⽆法在 16ms 内完成渲染;可 能是 UI 上有层叠太多的绘制单元;还有可能是动画执⾏的次数过多。

3 刷新机制

在 4.1 版本推出的 Project Butter 。 Project Butter 对 Android Display 系统进⾏了重构,引⼊三个核⼼元素: VSYNC 、 Triple Buffer 和 Choreographer 。其中, VSYNC 是理解 Project Buffer 的核⼼。 VSYNC 是 Vertical Synchronization (垂直同步)的缩写,是⼀种在 PC 上已经很早 就⼴泛使⽤的技术,读者可简单地把它认为是⼀种定时中断。 Choreographer 起调度的作⽤,将绘制⼯作统⼀到 VSYNC 的某个时间点 上,使应⽤的绘制⼯作有序。接下来,本⽂将围绕 VSYNC 来介绍 Android Display 系统的⼯作⽅式。

下面介绍几个名词:

  • 双缓冲

    首先我们要知道为什么使用双缓冲,是因为,如果不使用双缓冲,上一个数据显示完,下一个数据就更新并渲染上去了,那就会造成残影现象。双缓冲就是有两个区,其中⼀个称为 Front Buffer ,另外⼀个称为 Back Buffer 。先在B区中绘制,绘制成功之后给信号,与F区进行交换,这样子就解决了问题。

  • VSYNC

    在双缓冲中我们知道,一个区准备好之后才主动发信号,这个效率就很低,所以我们现在用了这个垂直中断技术,可以理解为一种定时中断,一但收到中断,CPU 就开始处理任务。

  • Choreographer

    收到 VSYNC 信号之后,调用用户设置的回调函数,一共有三种回调函数:

    • CALLBACK_INPUT :优先级最⾼,与输⼊事件有关。

    • CALLBACK_ANIMATION :第⼆优先级,与动画有关。

    • CALLBACK_TRAVERSAL :最低优先级,与 UI 控件绘制有关。

接下来东西很关键,我们通过时序图去理解:

  1. 没有垂直同步

    1. 从第⼀个 16ms 开始看, Display 显⽰第 0 帧, CPU 处理完第⼀帧 后, GPU 紧接其后处理继续第⼀帧。三者都在正常⼯作。

    2. 时间进⼊第⼆个 16ms :因为在上⼀个 16ms 时间内,第 1 帧已经 由 CPU 、 GPU 处理完毕。所以 Display 可以正常显⽰第 1 帧。显⽰没有问 题,但在本 16ms 期间, CPU 和 GPU 并未及时绘制第 2 帧数据(前⾯的空 ⽩区在忙别事情去了),⽽是在本周期快结束时, CPU/GPU 才去处理 第 2 帧数据。

    3. 时间进⼊第 3 个 16ms ,此时 Display 应该显⽰第 2 帧数据,但由于 CPU 和 GPU 还没有处理完第 2 帧数据,故 Display 只能继续显⽰第⼀帧的 数据,结果使得第 1 帧多画了⼀次(对应时间段上标注了⼀个 Jank ), 这就导致错过了显⽰第⼆帧。

    通过上述分析可知,在第⼆个 16ms 时,发⽣ Jank 的关键问题在 于,为何在第 1 个 16ms 段内, CPU/GPU 没有及时处理第 2 帧数据?从第 ⼆个 16ms 开始有⼀段空⽩的时间,可以说明原因所在,那就是 CPU 可 能是在忙别的事情,不知道该到处理 UI 绘制的时间了。可 CPU ⼀旦想 起来要去处理第 2 帧数据,时间⼜错过了。

  2. 有垂直同步

    ⼀旦收到 VSync 中断, CPU 就开 始处理各帧的数据。⼤部分的 Android 显⽰设备刷新率是 60Hz (图 2-7 的 时间轴也是 60ms ),这也就意味着每⼀帧最多只能有 1/60=16ms 左右的 准备时间。假如 CPU/GPU 的 FPS ⾼于这个值,显⽰效果将更好。但 是,这时⼜出现了⼀个新问题: CPU 和 GPU 处理数据的速度都能在 16ms 内完成,⽽且还有时间空余,但必须等到 VSYNC 信号到来后,才 处理下⼀帧数据,因此 CPU/GPU 的 FPS 被拉低到与 Display 的 FPS 相同。

采⽤双缓冲区的显⽰效果来看:在双缓冲下, CPU/GPU FPS ⼤于刷新频率同时采⽤了双缓冲技术以及 VSync ,可以看到整个过 程还是相当不错的,虽然 CPU/GPU 处理所⽤的时间时短时⻓,但总体 来说都在 16ms 以内,因⽽不影响显⽰效果。 A 和 B 分别代表两个缓冲 区,它们不断交换来正确显⽰画⾯。

上图可以看到,当 CPU/GPU 的处理时间超过 16ms 时,第⼀个 VSync 就已经到来,但缓冲区 B 中的数据却还没有准备好,这样就只能 继续显⽰之前 A 缓冲区中的内容。⽽后⾯ B 完成后,⼜因为还没有 VSync 信号, CPU/GPU 这个时候只能等待下⼀个 VSync 的来临才开始处 理下⼀帧数据。因此在整个过程中,有⼀⼤段时间被浪费。很关键的结论如下:

在第⼆个 16ms 时间段内, CPU ⽆所事事,因为 A Buffer 由 Display 在使⽤。 B Buffer 由 GPU 使⽤。注意,⼀旦过了 VSYNC 时间 点, CPU 就不能被触发以及处理绘制⼯作了。为什么 CPU 不能在第⼆个 16ms 处即 VSync 到来就开始⼯作呢?很明 显,原因就是只有两个 Buffer 。如果有第三个 Buffer 存在, CPU 就可以 开始⼯作,⽽不⾄于空闲。于是在 Andoird 4.1 以后,引出了第三个缓冲 区

这里我们要注意一点,一般情况下我们只使用双缓冲区,只用当出现了上述情况的时候,才会开始使用三级缓存。如下所示。

4 卡顿的根本原因

主要有两个原因:

  1. 绘制任务重

    这样会导致绘制一帧内容耗时太长。

  2. 主线程忙碌

    主线程太忙,导致垂直同步中断信号来的时候还没有准备好数据。

我们这里重点讨论第二种 主线程忙碌

绘制工作都是由主线程,也就是UI线程来负责,主线程的职责是处理用户交互,在屏幕上绘制像素,并进行加载相关的数据,所以我们一定要注意,不要做一些阻碍主线程的事情,这样就能保证对用户的及时响应。

所以我们要先知道,主线程做哪些操作,总结如下:

  1. UI生命周期控制

  2. 系统事件控制

  3. 消息处理

  4. 界面布局

  5. 界面绘制

  6. 界面刷新

除了这些以外,剩下的任务就尽量避免放在主线程中,尤其是复杂的数据分析计算以及网络请求。

5 性能分析工具

首先我们要知道卡顿的本质在于,当中断信号来的时候,不能及时的处理绘制时间导致的卡顿,所以有两个问题需要去考虑:

  1. 应用层干了什么导致了不能及时处理

  2. 卡顿可以监控吗?

分析问题和确认问题是否解决,都借助了相应的调试⼯具,⽐如 查看 Layout 层次的 Hierarchy View 、 Android 系统上带的 GPU Profile ⼯具 和静态代码检查⼯具 Lint 等。这些⼯具对性能优化都起到⾮常重要的 作⽤。本节将介绍这些⼯具和另外两个性能优化⾮常重要的⼯具: TraceView 和 Systrace 。

尤其是后两个,不止UI用到,后面的其他优化也非常的关键。

5.1 卡顿检测工具

我们已经知道,从应⽤层绘制⼀个⻚⾯( View ),主要有三个过 程:

CPU 准备数据 →

GPU从数据缓存列表获取数据 →

Display 设备绘制

这三个过程的耗时可以通过⼀个⼿机开发辅助⼯具查看: Profile GPU Rendering 。

Profile GPU Rendering 是 Android 4.1 系统开始提供的 ⼀个开发辅助功能,在手机开发者功能中打开。

  • 是一个图形检测工具,能够试试反应当前绘制的耗时。

  • 横轴表示时间,纵轴表示没一帧的耗时。

  • 随着时间推移,从左到右的刷新呈现。

  • 提供了一个标准的耗时,如果高于标准耗时,表示当前这一帧丢失。

每一根竖线就代表了一帧,有多个颜色组成。不同颜色解释如下:

  • 每一条柱状图都由4种颜色组成:红黄蓝紫,这些线条对应了每一帧在不同阶段的实际耗时

  • 蓝色代表测量绘制时间,代表需要多长时间去创建和更新DisPlayList,在 Android 中,⼀个视图在进⾏渲染之前,它必须被转换 成 GPU 熟悉的格式,简单来说就是⼏条绘图命令,蓝⾊就是记录了在 屏幕上更新视图需要花费的时间,也可以理解为执⾏每⼀个 View 的 onDraw ⽅法,创建或者更新每⼀个 View 的 Display List 对象。在蓝⾊的 线很⾼时,有可能是因为需要重新绘制,或者⾃定义视图的 onDraw 函 数处理事情太多

  • 红⾊代表执⾏的时间,这部分是 Android 进⾏ 2D 渲染 Display List 的时间,为了绘制到屏幕上, Android 需要使⽤ OpenGl ES 的 API 接⼝来 绘制 Display List ,这些 API 有效地将数据发送到 GPU ,最终在屏幕上显 ⽰出来。当红⾊的线⾮常⾼时,可能是由重新提交了视图⽽导致的。

  • 橙⾊部分表⽰处理时间,或者是 CPU 告诉 GPU 渲染⼀帧的地⽅, 这是⼀个阻塞调⽤,因为 CPU 会⼀直等待 GPU 发出接到命令的回复, 如果柱状图很⾼,就意味着 GPU 太繁忙了。

  • 紫⾊段表⽰将资源转移到渲染线程的时间,只有 Android 4.0 及以 上版本才会提供

这⾥可以通过: adb shell dumpsys gfxinfo com.**.** (包名)把具体的耗时输出到⽇志中来分析。

任何时候超过绿线(警戒线,对应时⻓ 16ms ),就有可能丢失⼀ 帧的内容,虽然对于⼤部分应⽤来说,丢失⼏帧确实感觉不出卡顿, 但保持 UI 流畅的关键就在于让这些垂直的柱状条尽可能地保持在绿线 下⾯。

在 GPU Profile Render 发现 有问题的⻚⾯后,可以通过另外⼀个⼯具 Hierarchy Viewer 来查看⻚⾯ 的布局层次和每个 View 所花的时间,这个工具我们后续再来讲解。

5.2 TraceView

TraceView 是 AndroidSDK ⾃带的⼯具,⽤来分析函数调⽤过程,可 以对 Android 的应⽤程序以及 Framework 层的代码进⾏性能分析。它是 ⼀个图形化的⼯具,最终会产⽣⼀个图表,⽤于对性能分析进⾏说 明,可以分析到应⽤具体每⼀个⽅法的执⾏时间,使⽤可以⾮常直观 简单,分析性能问题很⽅便。

5.2.1 使用方法

在使⽤ TraceVeiw 分析问题之前需要得到⼀个 *.trace 的⽂件,然后 通过 TraceView 来分析 trace ⽂件的信息, trace ⽂件的获取有两种⽅式:

  1. 在DDMS中使用

    1. 连接设备

    2. 打开应用

    3. 打开DDMS(若在Android Studio中则先打开 Android Device Monitor)

    4. 点击 Start Method Profiling 按钮

    5. 在应用中操作需要操作的点,比如进入一个 Activity 或者进行一些滑动,之后点击 Stop Method Profiling。

    6. 结束后会自动跳转Trace View

  2. 代码中加入调试语句保存 trace 文件

    有时在开发过程中不好复现的问题,需要在关键的路径上获取 TraceView 数据,在测试时复现此问题后直接拿到 Trace ⽂件查看对应的 数据。这时可以在代码中使⽤ TraceView ⼯具并⽣成对应的 trace ⽂件。 在 android.os.Debug 类中提供了相应的⽅法

    1. 在需要开始监控的地⽅调⽤ startMethodTracing ()

    2. 在需要结束监控的地⽅调⽤ stopMethodTracing ()

    3. 系统会在 SD 卡中创建 .trace ⽂件。

    4. 使⽤ traceveiw 打开该⽂件进⾏分析。

之后我们看看怎么去分析,下面是面板参数意义:

列名

意义

Name

所有的调用项目,parent和children,指调用和被调用

Inclusive

统计函数本身运行时间 + 调用子函数运行时间

incl

inclusive 时间占总时间的百分比

Exclusive

统计函数本身运行的时间

Excl

执行占时间的百分比

Calls + Recur Calls/Total

该方法调用次数 + 递归次数

Cpu Time / Call

该方法耗时

Real Time / Call

实际时常

RealTime 与 cputime 区别为:因为 RealTime 包括了 CPU 的上下⽂切换、阻塞、 GC 等,所以 RealTime ⽅法的实际执⾏时间要⽐ CPU Time 稍微⻓⼀点

5.3 Systrace

参数讲解:

  1. Alerts

    Alerts ⼀栏标记了性能有问题的点,单击该点 可以查看详细信息,在右边侧边栏还有⼀个 Alerts 框,单击可以查看每 个类型的 Alerts 的数量,单击某⼀个 Alert 可以看到问题的详细描述。

  2. Frame

    每个应⽤都有⼀⾏专门显⽰ frame ,每⼀帧就显⽰为⼀个绿⾊的圆 圈。当显⽰为⻩⾊或者红⾊时,它的渲染时间超过了 16.6ms (即达不 到 60fps 的⽔准)。使⽤ W 键放⼤,看看这⼀帧的渲染过程中系统到底 做了什么,同时它会将任何它认为性能有问题的东⻄都⾼亮警告,并 提⽰要怎么优化

6 布局优化

布局是否合理主要影响的是⻚⾯测量时间的多少,我们知道⼀个 ⻚⾯的显⽰测量和绘制过程都是通过递归来完成的,多叉树遍历的时 间与树的⾼度 h 相关,其时间复杂度为 O ( h ),如果层级太深,每增 加⼀层则会增加更多的⻚⾯显⽰时间。

任何时候 View 中的绘制内容发⽣变化时,都需要重新创建 DisplayList 、渲染 DisplayList ,更新到屏幕上等⼀系列操作。这个流程 的表现性能取决于 View 的复杂程度、 View 的状态变化以及渲染管道的 执⾏性能。例如,假设某个 Button 的⼤⼩需要增⼤到⽬前的两倍,在 增⼤ Button ⼤⼩之前,需要通过⽗ View 重新计算并摆放其他⼦ View 的 位置。修改 View 的⼤⼩会触发整个 HierarcyView 的重新计算⼤⼩的操 作。如果是修改 View 的位置,则会触发 HierarchView 重新计算其他 View 的位置。如果布局很复杂,就很容易导致严重的性能问题。

6.1 常用布局优化工具

6.1.1 Hierarchy Viewer

Hierarchy Viewer 是 Android SDK ⾃带的⼀款可视化调试⼯具,⽤来 检查 Layout 嵌套及绘制时间,以可视化的布局⾓度直观获取 Layout 布局 设计和各种属性信息,开发者在调试和布局 UI 界⾯时可以很⽅便地使 ⽤,提⾼⽤户的开发效率。

注意:

  • 出于安全考虑, Hierarchy Viewer 只能连接 Android 开发版⼿机或模 拟器。

  • 在应⽤程序 DEBUG 模式中,⽆法启动 Hierarchy Viewer 。

如何使用:

  1. 构造一个页面,测试使用

  2. 打开 Hierarchy Viewer

    android studion 中在工具栏可以找见。

    可以看见4个窗口,各功能如下:

    • Windows:显⽰当前设备信息,以及当前设备的所有⻚⾯列表。

    • View Properties:从当前选中View的属性。

    • Tree View:把 Activity 中所有控件( View )的层次结构从左到右显 ⽰出来,其中最右边部分是最底层的控件( View )。

    • Tree Overview:整体 Layout 布局图,以⼿机屏幕上真实位置呈现出 来,在 TreeView 中选中某⼀个控件时,会在 Layout View ⽤红⾊的框标 注。

6.2 如何优化

  1. 减少层级

  2. 使用Merge优化布局

  3. 布局复用

  4. 避免过度绘制(某个像素在同⼀帧的时间 内被绘制了多次)

  5. 尽量使用属性动画

  6. 考虑使用硬件加速。