UI 绘制优化

wladislaw-sokolowskij-584523-unsplash_meitu_1

Google 在15年初发布了 Android性能优化典范,里面详细谈及了Android 渲染机制。

渲染机制

大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,那么整个过程如果保证在16ms以内就能达到一个流畅的画面。

如果你的某个操作花费时间超时,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象,产生卡顿。

我们通常都会提到60fps与16ms,并且将60fps作为App性能的衡量标准,是因为人眼与大脑之间的协作无法感知超过60fps的画面更新。

根据Google官方出品的 Android性能优化典范,60帧每秒是目前最合适的图像显示速度,事实上绝大多数的Android 设备也是按照每秒 60 帧来刷新的。为了让屏幕的刷新帧率达到 60fps,我们需要确保在时间16ms(1000/60Hz)内完成单次刷新的操作(包括measure、layout 以及 draw),这也是Android系统每隔16ms就会发出一次 VSYNC 信号触发对 UI 进行渲染的原因。

作为开发人员,为了保证画面的流畅性,就必须保证稳定的帧率,即保证在16ms 内完成该完成界面渲染。

有很多原因可以导致丢帧,也许是因为你的 layout 太过复杂,无法在16ms内完成渲染,有可能是因为你的 UI 上有层叠太多的绘制单元,还有可能是因为动画执行的次数过多。

接下来,针对这些卡顿的原因,我们慢慢来优化。


1.布局嵌套

Android 的布局文件的加载是 LayoutInflater 利用 pull 解析方式来解析,然后根据节点名通过反射的方式创建出View 对象实例;

子View的位置受父View的影响,如RelativeLayout 需要measure两次才能完成,而嵌套、相互嵌套、深层嵌套等的发生会使measure次数呈指数级增长,所费时间呈线性增长;

因此,随着控件数量越多、布局嵌套层次越深,测量和布局花费的时间几乎是线性增长。

所有,我们要尽可能的减少布局嵌套以及控件个数,保持 view 的树形结构尽量扁平,同时移除所有不需要渲染的view。

这就推荐使用 Hierarchy Viewer 这个可视化工具,在 Android studio 中以此打开 Tools > Android > Android Device Monitor ,然后再打开 Window > Open Perspective > Hierarchy View,就可以显示布局的层次结构。

hierarch view

详细的使用方法,请 Google。

借助工具,我们可以方便的查看页面的布局情况,同时配合使用布局标签 merge、viewstub (Merge 减少嵌套层次、ViewStub 延迟初始化)来尽可能减少布局嵌套,从而达到优化布局的目的。


2.过渡绘制

一个像素点,仅仅绘制一次是最优的方式,而由于布局的重叠,有可能会导致一块区域被多次绘制,这就是过度绘制(Overdraw)。

Android 系统提供了检测 Overdraw 的调试工具,在手机中打开设置——开发者选项——调试GPU过度绘制——显示过度绘制区域。

overdraw

打开工具后,会发现界面多了很多色块,不必担心,这些颜色都有不同的含义:

原色 – 没有过度绘制 – 这部分的像素点只在屏幕上绘制了一次。

蓝色 – 1次过度绘制– 这部分的像素点只在屏幕上绘制了两次。

绿色 – 2次过度绘制 – 这部分的像素点只在屏幕上绘制了三次。

粉色 – 3次过度绘制 – 这部分的像素点只在屏幕上绘制了四次。

红色 – 4次过度绘制 – 这部分的像素点只在屏幕上绘制了五次。

这样通过工具就可以查看哪些地方出现了过度绘制。

解决的方法也很简单,就是移除 View 中不必要的Background 。

可以借助工具,再对照着自身的布局文件,很容易找到并移除不必要的背景。

特别注意还有 window 也是有Background ,这个背景很容易被忽视。移除方式如下:

1
getWindow().setBackgroundDrawable(null);

除了布局文件,在自定义控件中也容易产生 Overdraw,这时候就需要 使用clipRect() 裁剪画布,从而避免 Overdraw。

具体实例,请查看鸿洋的这篇博客 Android UI性能优化实战 识别绘制中的性能问题


3.UI 线程复杂运算

Android 为了提升画面的流畅性,特意使用一个线程来绘制图像,这个线程就是所谓的主线程,也可以称为 UI线程,而且因为刷新 UI 时有做线程校验,所有也只能是 UI 线程来刷新 UI(SurfaceView、TextureView除外)。
当我们在 UI 线程里做一些复杂的耗时操作时,占用了 UI 线程的资源,很容易使得 UI 线程来不及去绘制画面,从而导致丢帧,产生卡顿。

所有解决的方法是:

首先检测出耗时的地方,然后优化代码提升执行效率,如果无法优化就将其放过在子线程中执行。

检测耗时的工具,有前文 App 启动优化 中已经介绍过的TraceView、Hugo ,现在介绍另一种 StrictMode(严苛模式)

StrictMode 就是用来指定一系列策略(policy),对相应规则(rule)进行检查并且做出反应。这些策略大致包括Android 的编码规范,例如监控在主线程(UI线程)中的操作等等。StrictMode 有不同的策略 (ThreadPolicy 和 VmPolicy ),每种策略又用不同的规则(rule),每种规则又对应不同的方法,一旦规则被违反,这些对应的方法就会被用来做出反应。

先来一段实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void onCreate() {
if (BuildConfig.DEBUG) {
// 针对线程的相关策略
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads() //侦测磁盘读
.detectDiskWrites() //侦测磁盘写
.detectNetwork() //侦测网络操作
.detectCustomSlowCalls() //侦测自定义的耗时操作
// .detectAll() //侦测一切潜在违规
.penaltyLog() //违规时,打印日志
.build());
// 针对VM的相关策略
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog() //违规时,打印日志
.penaltyDeath() //违规时,直接使应用崩溃
.build());
}
super.onCreate();
}

注意,StrictMode 不要在线上版本中使用。如果想了解更多使用方式,请 Google。


4.频繁的 GC

这里直接引用 Google 发布 Android 性能优化典范 - 开源中国社区 中的原文解释:

虽然 Android 有自动管理内存的机制,但是对内存的不恰当使用仍然容易引起严重的性能问题。在同一帧里面创建过多的对象是件需要特别引起注意的事情。

Android系统里面有一个Generational Heap Memory的模型,系统会根据内存中不同 的内存数据类型分别执行不同的GC操作。例如,最近刚分配的对象会放在Young Generation区域,这个区域的对象通常都是会快速被创建并且很快被销毁回收的,同时这个区域的GC操作速度也是比Old Generation区域的GC操作速度更快的。

img

除了速度差异之外,执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行。

通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。

导致GC频繁执行有两个原因:

  • Memory Churn内存抖动,内存抖动是因为大量的对象被创建又在短时间内马上被释放。
  • 瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加 Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。

解决上面的问题有简洁直观方法,如果你在Memory Monitor 里面查看到短时间发生了多次内存的涨跌,这意味着很有可能发生了内存抖动。

img

同时我们还可以通过Allocation Tracker来查看在短时间内,同一个栈中不断进出的相同对象。这是内存抖动的典型信号之一。

当你大致定位问题之后,接下去的问题修复也就显得相对直接简单了。例如,你需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循 环体之外,自定义View中的onDraw方法也需要引起注意,每次屏幕发生绘制以及动画执行过程中,onDraw方法都会被调用到,避免在onDraw 方法里面执行复杂的操作,避免创建对象。对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是这里 需要注意结束使用之后,需要手动释放对象池中的对象。


参考

Android性能优化(二)之布局优化面面观

UI性能优化实战 识别绘制中的性能问题

性能优化之布局优化

Android App优化之消除卡顿

Google 发布 Android 性能优化典范 - 开源中国社区


坚持分享技术,但行好事,莫问前程 ~^o^~