Android 绘制优化

绘制原理

绘制优化:指 View 的 onDraw() 要避免执行大量的操作。

  • onDraw() 中不要创建新的局部对象
  • onDraw() 中不要做耗时的任务,也不能执行成千上万次的循环操作

View 的绘制流程有 3 个步骤:measure、layout 和 draw,它们主要运行在系统的应用框架层,而真正将数据渲染到屏幕上的则是系统 Native 层的 SurfaceFlinger 服务来完成的,

绘制过程主要由 CPU 来进行 Measure、Layout、Record、Execute 的数据计算工作,GPU 负责栅格化、渲染。CPU 和 GPU 是通过图形驱动层来进行连接的,图形驱动层维护了一个队列,CPU 将 display list 添加到该队列中,这样 GPU 就可以从这个队列中取出数据进行绘制。

帧数:一秒内传输的图片的量,也可以理解为图形处理器每秒钟能够刷新几次,通常用 FPS 表示。每一帧其实就是静止的图像,通过快速连续地显示帧便形成了运动的假象。因为人类的大脑会不断接收并处理眼球看到的信息,单位时间内越多的帧被处理,越能有效地被大脑识别。最简单的举例就是玩游戏时,如果画面在 60fps 则不会感觉到卡顿,当帧数太低时(10fps ~ 12fps),大脑就分不清这个图像是静止的还是变化的。

要想画面保持在 60fps,需要屏幕在 1 秒内刷新 60 次,也就是每 16.6667ms 刷新一次(绘制时长在 16ms 以内,每 16ms DRAW 一次)。

Android 系统每隔 16ms 发出 VSYNC 信号,触发对 UI 进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的 60 fps。(VSYNC 是 Vertical Synchronization(垂直同步)的缩写,是一种定时中断,一旦收到 VSYNC 信号,CPU 就开始处理各帧数据。如果某个操作要花费 24ms,这样系统在得到 VSYNC 信号时无法进行正常的渲染,会发生丢帧。用户会在 32ms 中看到同一帧的画面(发生了丢帧,在第一个 16 ms 时没有能够正常的 DRAW,只能在第二个 16 ms 时 DRAW)。

产生卡顿原因有很多,主要有以下几点:

  • 布局 Layout 过于复杂,无法在 16ms 内完成渲染。
  • 同一时间动画执行的次数过多,导致 CPU 或 GPU 负载过重。
  • VIew 过渡绘制,导致某些像素在同一帧时间内被绘制多次。
  • 在 UI 线程中做了稍微耗时的操作。
  • GC 回收时暂停时间过长或者频繁 GC 产生大量的暂停时间。

辅助工具

Profile GPU Rendering

Android 4.1 系统提供的开发辅助功能,用来找到渲染有问题的界面,可以在手机的开发者选项中打开这个功能。(不同厂商的叫法可能会不一样,一般在开发者选项中的监控分类里面,比如 XX 呈现模式分析,然后在选项中选择在屏幕上显示为条形图,接着屏幕上会显示出彩色的柱状图。)

我的设备为 IQOO:

图中横轴代表时间,纵轴表示某一帧的耗时。绿色的横线为警戒线,超过这条线则意味着时长超过了 16ms,尽量要保证垂直的彩色柱状图保持在绿线下面。

这些垂直的彩色柱状图代表着一帧,不同颜色的彩色柱状图代表不同的含义。

  • 橙色:处理的时间,是 CPU 告诉 GPU 渲染一帧的地方,这是一个阻塞调用,因为 CPU 会一直等待 GPU 发出接到命令的回复,如果橙色柱状图很高,则表明 GPU 很繁忙。
  • 红色:执行的时间,这部分是 Android 进行 2D 渲染 Display List 的时间。如果红色柱状图很高,可能由于重新提交了视图而导致的。还有复杂的自定义 View 也会导致红色柱状图变高。
  • 蓝色:测量绘制的时间,也就是需要多长时间去创建和更新 Display List 。如果蓝色柱状图很高,可能需要重新绘制,或者 View 的 onDraw 方法处理事情太多。

在 Android 6.0 中,有更多的颜色被加了进来:

  • Swap Buffers(交换缓冲区):表示处理的时间,和上面的橙色一样。
  • Command Issue(发出命令):表示执行的时间,和上面的红色一样。
  • Sync & Upload(同步和上传):表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,可以减少屏幕上的图片数量或者缩小图片的大小。
  • Draw(绘制):表示测量和绘制视图列表所需要的时间,和上面的蓝色一样。
  • Measure/Layout(测量/布局):表示布局的 onMeasure 与 onLayout 所花费的时间,一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题。
  • Animation(动画):表示计算执行动画所需要花费的时间,包含的动画有 ObjectAnimator、ViewPropertyAnimator、Transition 等。一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者检查动画执行的过程中是不是触发了读/写操作等。
  • Input Handling(输入处理):表示系统处理输入事件所耗费的时间,粗略等于对事件处理方法所执行的时间。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作。
  • Misc Time/VSync Delay(其他时间/VSync 延迟):表示在主线程中执行了太多的任务,导致 UI 渲染跟不上 VSYNC 的信号而出现掉帧的情况。

检查 GPU 渲染速度和过度绘制

Systrace

Android 4.1 中新增的性能数据采样和分析工具,它可以帮助开发者收集 Android 关键子系统(SurfaceFlinger、WMS 等 Framework 部分关键模块、服务,View 体系系统等)的运行信息。Systrace 的功能包括跟踪系统的 I/O 操作、内核工作队列、CPU 负载以及 Android 各个子系统的运行状况等。对于 UI 显示性能,比如动画播放不流畅、渲染卡顿等问题提供了分析数据。

在 Android 4.3 以后版本中,它可以在 DDMS 上使用,可以通过命令行使用,可以在代码中进行跟踪。

通过 DDMS:启动 Android Device Monitor 工具,因为Android studio 3.1后认为monitor用的很少,便去掉了菜单栏启动按钮,所以只能通过命令运行该工具了。工具位于android-sdk目录中,monitor.bat即为启动脚本,双击运行即可。

TraceView

Android SDK 中自带的数据采集和分析工具。一般来说,通过它可以得到以下两种数据:

  • 单次执行耗时的方法
  • 执行次数多的方法

布局优化

布局优化:就是尽量减少布局文件的层级。一个界面的测量和绘制是通过递归来完成的,减少布局的层数就会减少测量和绘制的时间,从而性能就会得到提升。当然这只是布局优化的一方面,可通过一些工具来对布局进行分析和优化。

优化工具

Tools -> Layout Inspector

Hierarchy Viewer

Android SDK 自带的可视化的调试工具,用来检查布局嵌套和绘制的时间。

Android Lint

ADT 16 中提供的新工具,它是一个代码扫描工具,通过代码静态检查来发现代码出现的潜在问题,并给出优化建议。检查的范围主要有几点:Correctness(正确性)、Security(安全性)、Performance(性能)、Usability(可用性)、Accessibility(可达性)、Internationalization(国际化)

可通过 Android Studio 的 Analyze -> Inspect Code 来配置检查的范围,单击 OK 按钮来进行代码检查。

可通过 File -> Settings -> Editor -> Inspections 来自定义 Android Lint 的检查提示。

优化方法

优化方式比如:

  • 删除布局中无用的控件和层级,其次有选择地使用性能较低的 ViewGroup。
  • 采用 include 标签,merge 标签和 ViewStub。
  • include 标签:主要用于布局重用
  • merge 标签:一般和 include 标签配合使用,它可降低减少布局的层级
  • ViewStub:提供了按需加载的功能

使用 Include 标签来进行布局复用

<include layout="@layout/titlebar" />

使用 Merge 标签去除多余层级

一般和 Include 标签搭配使用,用来替换 Include 标签引用的布局的根布局。为了避免布局错乱,最好是替代 FrameLayout 或者布局方向一致的LinearLayout,比如当前父布局的 LinearLayout 的布局方向是垂直的,包含的子布局 LinearLayout 的布局方向也是垂直的,则可以用 merge 标签来替代子布局的 LinearLayout。如果方向不一致时执意要使用 merge 标签,可以用继承自 LinearLayout 的自定义 View。

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="40dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</merge>

使用 ViewStub 来提高加载速度

当加载一个布局时,有时并不需要显示全部的控件,一般采用的方法是通过 View 的 GONE 和 INVISIBLE 属性,但这种方法效率不高,虽然达到了隐藏的目的,但是仍在布局当中,系统仍会解析它们。这时可以用 ViewStub 来解决,它是一个轻量级的 View,当调用 inflate 方法或者设置可见时,系统会加载 ViewStub 指定的布局,然后将这个布局添加到 ViewStub 中,此前它是不占布局空间和系统资源的,它主要的目的就是为目标视图占用一个位置。因此,使用 ViewStub 可以提高界面初始化的性能,从而提高界面的加载速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<ViewStub
android:id="@+id/viewstub"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout="@layout/titlebar"/>

</LinearLayout>
1
2
3
4
5
6
7
8
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// 将 ViewStub 引用的布局加载到 ViewStub 中。
viewstub.inflate()
viewstub.visibility = View.VISIBLE
}

使用 ViewStub 时需要注意的问题:

  • ViewStub 只能加载一次,加载后 ViewStub 对象会被置为空,这样引用的布局被加载后,就不能用 ViewStub 来控制引用的布局了。因此,如果一个控件需要不断地显示和隐藏,还是要使用 View 的 Visibility 属性。
  • ViewStub 不能嵌套 Merge 标签。
  • ViewStub 操作的是布局文件,如果只是想操作具体的 View,还是要使用 View 的 Visibility 属性。

避免 GPU 过度绘制

过度绘制是指在屏幕上某个像素在同一帧的时间内被绘制多次,从而浪费了 GPU 和 CPU 的资源。产生这一情况主要有两个原因:

  • 在 XML 布局中,控件有重叠且都有设置背景。
  • View 的 onDraw 在同一区域绘制多次。

可以使用 Android 系统中自带的工具来检测过度绘制。首先保证系统版本在 4.1 以上,然后 -> 开发者选项 -> 调式 GPU 过渡绘制(一般在硬件加速渲染中)。这时屏幕会出现各种颜色,含义为:

  • 白色:没有过渡绘制 - 每个像素点在屏幕上绘制了一次。
  • 蓝色:一次过渡绘制 - 每个像素点在屏幕上绘制了两次。
  • 绿色:二次过渡绘制 - 每个像素点在屏幕上绘制了三次。
  • 粉色:三次过渡绘制 - 每个像素点在屏幕上绘制了四次。
  • 红色:四次或四次以上过渡绘制 - 每个像素点在屏幕上绘制了五次或五次以上。

颜色越浅越好,避免过度绘制主要有以下两种方案:

  • 移除不需要的 background
  • 在自定义 View 的 onDraw 方法中,用 canvas.clipRect 来指定绘制的区域,防止重叠的组件发生过度绘制。

备注

参考资料:
Android 进阶解密

单词音标: