安卓应用 绘制优化
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,由于是递归,所以层级越深,元素越多,耗时就越长。
Measure
用深度递归原色递归得到所有的视图的宽高,获取当前 View 的宽高之后,就可以调佣成员函数 Measure 来是设置他的大小。如果当前正在测的 View 的子视图也是一个容器,那就重复测量,直到测量完毕。
Layout
也是深度递归优先,可以获取到所有 View 的位置,当一个 View 的位置确定之后,我们就可以结合前面的 Measure 来确定在程序窗口中的位置。
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 控件绘制有关。
接下来东西很关键,我们通过时序图去理解:
没有垂直同步
从第⼀个 16ms 开始看, Display 显⽰第 0 帧, CPU 处理完第⼀帧 后, GPU 紧接其后处理继续第⼀帧。三者都在正常⼯作。
时间进⼊第⼆个 16ms :因为在上⼀个 16ms 时间内,第 1 帧已经 由 CPU 、 GPU 处理完毕。所以 Display 可以正常显⽰第 1 帧。显⽰没有问 题,但在本 16ms 期间, CPU 和 GPU 并未及时绘制第 2 帧数据(前⾯的空 ⽩区在忙别事情去了),⽽是在本周期快结束时, CPU/GPU 才去处理 第 2 帧数据。
时间进⼊第 3 个 16ms ,此时 Display 应该显⽰第 2 帧数据,但由于 CPU 和 GPU 还没有处理完第 2 帧数据,故 Display 只能继续显⽰第⼀帧的 数据,结果使得第 1 帧多画了⼀次(对应时间段上标注了⼀个 Jank ), 这就导致错过了显⽰第⼆帧。
通过上述分析可知,在第⼆个 16ms 时,发⽣ Jank 的关键问题在 于,为何在第 1 个 16ms 段内, CPU/GPU 没有及时处理第 2 帧数据?从第 ⼆个 16ms 开始有⼀段空⽩的时间,可以说明原因所在,那就是 CPU 可 能是在忙别的事情,不知道该到处理 UI 绘制的时间了。可 CPU ⼀旦想 起来要去处理第 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 卡顿的根本原因
主要有两个原因:
绘制任务重
这样会导致绘制一帧内容耗时太长。
主线程忙碌
主线程太忙,导致垂直同步中断信号来的时候还没有准备好数据。
我们这里重点讨论第二种 主线程忙碌
绘制工作都是由主线程,也就是UI线程来负责,主线程的职责是处理用户交互,在屏幕上绘制像素,并进行加载相关的数据,所以我们一定要注意,不要做一些阻碍主线程的事情,这样就能保证对用户的及时响应。
所以我们要先知道,主线程做哪些操作,总结如下:
UI生命周期控制
系统事件控制
消息处理
界面布局
界面绘制
界面刷新
除了这些以外,剩下的任务就尽量避免放在主线程中,尤其是复杂的数据分析计算以及网络请求。
5 性能分析工具
首先我们要知道卡顿的本质在于,当中断信号来的时候,不能及时的处理绘制时间导致的卡顿,所以有两个问题需要去考虑:
应用层干了什么导致了不能及时处理
卡顿可以监控吗?
分析问题和确认问题是否解决,都借助了相应的调试⼯具,⽐如 查看 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 ⽂件的获取有两种⽅式:
在DDMS中使用
连接设备
打开应用
打开DDMS(若在Android Studio中则先打开 Android Device Monitor)
点击 Start Method Profiling 按钮
在应用中操作需要操作的点,比如进入一个 Activity 或者进行一些滑动,之后点击 Stop Method Profiling。
结束后会自动跳转Trace View
代码中加入调试语句保存 trace 文件
有时在开发过程中不好复现的问题,需要在关键的路径上获取 TraceView 数据,在测试时复现此问题后直接拿到 Trace ⽂件查看对应的 数据。这时可以在代码中使⽤ TraceView ⼯具并⽣成对应的 trace ⽂件。 在 android.os.Debug 类中提供了相应的⽅法
在需要开始监控的地⽅调⽤ startMethodTracing ()
在需要结束监控的地⽅调⽤ stopMethodTracing ()
系统会在 SD 卡中创建 .trace ⽂件。
使⽤ traceveiw 打开该⽂件进⾏分析。
之后我们看看怎么去分析,下面是面板参数意义:
RealTime 与 cputime 区别为:因为 RealTime 包括了 CPU 的上下⽂切换、阻塞、 GC 等,所以 RealTime ⽅法的实际执⾏时间要⽐ CPU Time 稍微⻓⼀点
5.3 Systrace
参数讲解:
Alerts
Alerts ⼀栏标记了性能有问题的点,单击该点 可以查看详细信息,在右边侧边栏还有⼀个 Alerts 框,单击可以查看每 个类型的 Alerts 的数量,单击某⼀个 Alert 可以看到问题的详细描述。
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 。
如何使用:
构造一个页面,测试使用
打开 Hierarchy Viewer
android studion 中在工具栏可以找见。
可以看见4个窗口,各功能如下:
Windows:显⽰当前设备信息,以及当前设备的所有⻚⾯列表。
View Properties:从当前选中View的属性。
Tree View:把 Activity 中所有控件( View )的层次结构从左到右显 ⽰出来,其中最右边部分是最底层的控件( View )。
Tree Overview:整体 Layout 布局图,以⼿机屏幕上真实位置呈现出 来,在 TreeView 中选中某⼀个控件时,会在 Layout View ⽤红⾊的框标 注。
6.2 如何优化
减少层级
使用Merge优化布局
布局复用
避免过度绘制(某个像素在同⼀帧的时间 内被绘制了多次)
尽量使用属性动画
考虑使用硬件加速。