美文网首页
UI 优化(下):如何优化 UI 渲染?

UI 优化(下):如何优化 UI 渲染?

作者: 凯玲之恋 | 来源:发表于2020-04-27 09:50 被阅读0次

UI 渲染也会造成卡顿,
在 Android 系统的 VSYNC 信号到达时,如果 UI 线程被某个耗时任务堵塞,长时间无法对 UI 进行渲染,这时就会出现卡顿。

UI 优化要解决的核心是由于渲染性能本身造成用户感知的卡顿,它可以认为是卡顿优化的一个子集。

从设计师和产品的角度,他们希望应用可以用丰富的图形元素、更炫酷的动画来实现流畅的用户体验。但是 Android 系统很有可能无法及时完成这些复杂的界面渲染操作,这个时候就会出现掉帧。

1、UI 渲染测量

在 Android Studio 3.1 之后,Android 推荐使用Graphics API Debugger(GAPID)来替代 Tracer for OpenGL ES 工具。

5c390e9148664f338fb61781e650a138.png

通过上面的几个工具,我们可以初步判断应用 UI 渲染的性能是否达标,例如是否经常出现掉帧、掉帧主要发生在渲染的哪一个阶段、是否存在 Overdraw 等。

虽然这些图形化界面工具非常好用,但是它们难以用在自动化测试场景中,那有哪些测量方法可以用于自动化测量 UI 渲染性能呢?

1.1 gfxinfo

gfxinfo可以输出包含各阶段发生的动画以及帧相关的性能信息,具体命令如下:

adb shell dumpsys gfxinfo 包名

除了渲染的性能之外,gfxinfo 还可以拿到渲染相关的内存和 View hierarchy 信息。

在 Android 6.0 之后,gxfinfo 命令新增了 framestats 参数,可以拿到最近 120 帧每个绘制阶段的耗时信息。

adb shell dumpsys gfxinfo 包名 framestats

通过这个命令我们可以实现自动化统计应用的帧率,更进一步还可以实现自定义的“Profile GPU Rendering”工具,在出现掉帧的时候,自动统计分析是哪个阶段的耗时增长最快,同时给出相应的建议

4f74599b0b3eca4fc3cde7901fcbe2b2.png

1.2 SurfaceFlinger

除了耗时,我们还比较关心渲染使用的内存。

在 Android 4.1 以后每个 Surface 都会有三个 Graphic Buffer,那如何查看 Graphic Buffer 占用的内存,系统是怎么样管理这部分的内存的呢?

你可以通过下面的命令拿到系统 SurfaceFlinger 相关的信息:

adb shell dumpsys SurfaceFlinger

下面以今日头条为例,应用使用了三个 Graphic Buffer 缓冲区,当前用在显示的第二个 Graphic Buffer,大小是 1080 x 1920。现在我们也可以更好地理解三缓冲机制,你可以看到这三个 Graphic Buffer 的确是在交替使用。


+ Layer 0x793c9d0c00 (com.ss.***。news/com.**.MainActivity)
   //序号            //状态           //对象        //大小
  >[02:0x794080f600] state=ACQUIRED, 0x794081bba0 [1080x1920:1088,  1]
   [00:0x793e76ca00] state=FREE    , 0x793c8a2640 [1080x1920:1088,  1]
   [01:0x793e76c800] state=FREE    , 0x793c9ebf60 [1080x1920:1088,  1]

继续往下看,你可以看到这三个 Buffer 分别占用的内存:


Allocated buffers:
0x793c8a2640: 8160.00 KiB | 1080 (1088) x 1920 | 1 | 0x20000900 
0x793c9ebf60: 8160.00 KiB | 1080 (1088) x 1920 | 1 | 0x20000900 
0x794081bba0: 8160.00 KiB | 1080 (1088) x 1920 | 1 | 0x20000900

这部分的内存其实真的不小,特别是现在手机的分辨率越来越大,而且还很多情况应用会有其他的 Surface 存在,例如使用了SurfaceView或者TextureView等。
那系统是怎么样管理这部分内存的呢?当应用退到后台的时候,系统会将这些内存回收,也就不会再把它们计算到应用的内存占用中。


+ Layer 0x793c9d0c00 (com.ss.***。news/com.**.MainActivity)
   [00:0x0] state=FREE    
   [01:0x0] state=FREE    
   [02:0x0] state=FREE

2、UI 优化的常用手段

让我们再重温一下 UI 渲染的阶段流程图,我们的目标是实现 60 fps,这意味着渲染的所有操作都必须在 16 ms(= 1000 ms/60 fps)内完成。


bcbf90aa1c684c261d009c04f489810d.png

谓的 UI 优化,就是拆解渲染的各个阶段的耗时,找到瓶颈的地方,再加以优化。接下来我们一起来看看 UI 优化的一些常用的手段。

2.1 尽量使用硬件加速

相信你也发自内心地认同硬件加速绘制的性能是远远高于软件绘制的。所以说 UI 优化的第一个手段就是保证渲染尽量使用硬件加速。

有哪些情况我们不能使用硬件加速呢?之所以不能使用硬件加速,是因为硬件加速不能支持所有的 Canvas API,具体 API 兼容列表可以见drawing-support文档。

如果使用了不支持的 API,系统就需要通过 CPU 软件模拟绘制,这也是渐变、磨砂、圆角等效果渲染性能比较低的原因。

SVG 也是一个非常典型的例子,SVG 有很多指令硬件加速都不支持。

但我们可以用一个取巧的方法,提前将这些 SVG 转换成 Bitmap 缓存起来,这样系统就可以更好地使用硬件加速绘制。同理,对于其他圆角、渐变等场景,我们也可以改为 Bitmap 实现

这种取巧方法实现的关键在于如何提前生成 Bitmap,以及 Bitmap 的内存需要如何管理。

2.2 Create View 优化

View 创建的耗时。
请不要忘记,View 的创建也是在 UI 线程里,对于一些非常复杂的界面,这部分的耗时不容忽视。

在优化之前我们先来分解一下 View 创建的耗时,可能会包括各种 XML 的随机读的 I/O 时间、解析 XML 的时间、生成对象的时间(Framework 会大量使用到反射)

2.2.1 使用代码创建

使用 XML 进行 UI 编写可以说是十分方便,可以在 Android Studio 中实时预览到界面。

如果我们要对一个界面进行极致优化,就可以使用代码进行编写界面。

所以我们需要兼容性能与开发效率,我建议只在对性能要求非常高,但修改又不非常频繁的场景才使用这个方式。

2.2.2 异步创建

那我们能不能在线程提前创建 View,实现 UI 的预加载吗?
尝试过的同学都会发现系统会抛出下面这个异常:


java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()      
  at android.os.Handler.<init>(Handler.java:121)

事实上,我们可以通过又一个非常取巧的方式来实现。

在使用线程创建 UI 的时候,先把线程的 Looper 的 MessageQueue 替换成 UI 线程 Looper 的 Queue。

不过需要注意的是,在创建完 View 后我们需要把线程的 Looper 恢复成原来的。

54ab7385263b71ded795a5001df24a55.png

当然也可以提前 异步inflate

2.2.3 View 重用

正常来说,View 会随着 Activity 的销毁而同时销毁。
ListView、RecycleView 通过 View 的缓存与重用大大地提升渲染性能。

因此我们可以参考它们的思想,实现一套可以在不同 Activity 或者 Fragment 使用的 View 缓存机制。

但是这里需要保证所有进入缓存池的 View 都已经“净身出户”,不会保留之前的状态。微信曾经就因为这个缓存,导致出现不同的用户聊天记录错乱。


d21f2febd742c91cbeca9a14755b71fa.png

2.2.3 measure/layout 优化

渲染流程中 measure 和 layout 也是需要 CPU 在主线程执行的,对于这块内容网上有很多优化的文章,一般的常规方法有:

  • 减少 UI 布局层次。例如尽量扁平化,使用 等优化。
  • 优化 layout 的开销。尽量不使用 RelativeLayout 或者基于 weighted LinearLayout,它们 layout 的开销非常巨大。这里我推荐使用 ConstraintLayout 替代 RelativeLayout 或者 weighted LinearLayout。
  • 背景优化。尽量不要重复去设置背景,这里需要注意的是主题背景(theme), theme 默认会是一个纯色背景,如果我们自定义了界面的背景,那么主题的背景我们来说是无用的。但是由于主题背景是设置在 DecorView 中,所以这里会带来重复绘制,也会带来绘制性能损耗。

对于 measure 和 layout,我们能不能像 Create View 一样实现线程的预布局呢?这样可以大大地提升首次显示的性能。
Textview 是系统控件中非常强大也非常重要的一个控件,强大的背后就代表着需要做很多计算。
在 2018 年的 Google I/O 大会,发布了PrecomputedText并已经集成在 Jetpack 中,它给我们提供了接口,可以异步进行 measure 和 layout,不必在主线程中执行。

2.3 UI 优化的进阶手段

我来介绍一下 Facebook 的一个开源库 Litho

2.3.1 Litho:异步布局

Litho是 Facebook 开源的声明式 Android UI 渲染框架,它是基于另外一个 Facebook 开源的布局引擎Yoga开发的。
Litho 本身非常强大,内部做了很多非常不错的优化。

一般来说的 Android 所有的控件绘制都要遵守 measure -> layout -> draw 的流水线,并且这些都发生在主线程中。

b8bd2cb5ad88a64f301381b0cf45b15c.png

Litho 如我前面提到的 PrecomputedText 一样,把 measure 和 layout 都放到了后台线程,只留下了必须要在主线程完成的 draw,这大大降低了 UI 线程的负载。它的渲染流水线如下:

2.3.2 界面扁平化

降低 UI 的层级是一个非常通用的优化方法。

Litho 就给了我们一种方案,由于 Litho 使用了自有的布局引擎(Yoga),在布局阶段就可以检测不必要的层级、减少 ViewGroups,来实现 UI 扁平化。
上半部分是我们一般编写这个界面的方法,下半部分是 Litho 编写的界面,可以看到只有一层层级。


1758d00240d0eda842570038caf92090.png

2.3.3 优化 RecyclerView

Litho 还优化了 RecyclerView 中 UI 组件的缓存和回收方法。
原生的 RecyclerView 或者 ListView 是按照 viewType 来进行缓存和回收,但如果一个 RecyclerView/ListView 中出现 viewType 过多,会使缓存形同虚设。
但 Litho 是按照 text、image 和 video 独立回收的,这可以提高缓存命中率、降低内存使用率、提高滚动帧率。


9d8a2830ef39dd84ca8165a08a38098d.png

Litho 虽然强大,但也有自己的缺点。它为了实现 measure/layout 异步化,使用了类似 react 单向数据流设计,这一定程度上加大了 UI 开发的复杂性。

并且 Litho 的 UI 代码是使用 Java/Kotlin 来进行编写,无法做到在 AS 中预览。

2.3.2 RenderThread 与 RenderScript

在 Android 5.0,系统增加了 RenderThread,对于 ViewPropertyAnimator 和 CircularReveal 动画,我们可以使用RenderThead 实现动画的异步渲染
当主线程阻塞的时候,普通动画会出现明显的丢帧卡顿,而使用 RenderThread 渲染的动画即使阻塞了主线程仍不受影响。

现在越来越多的应用会使用一些高级图片或者视频编辑功能,例如图片的高斯模糊、放大、锐化等。拿日常我们使用最多的“扫一扫”这个场景来看,这里涉及大量的图片变换操作,例如缩放、裁剪、二值化以及降噪等。

图片的变换涉及大量的计算任务,这个时候使用 GPU 是更好的选择。那如何进一步压榨系统 GPU 的性能呢?
我们可以通过RenderScript,它是 Android 操作系统上的一套 API。它基于异构计算思想,专门用于密集型计算。

RenderScript 提供了三个基本工具:一个硬件无关的通用计算 API;一个类似于 CUDA、OpenCL 和 GLSL 的计算 API;一个类C99的脚本语言。

RenderScript渲染利器
RenderScript :简单而快速的图像处理
使用RenderScript实现高斯模糊(毛玻璃/磨砂)效果

参考

UI 优化(下):如何优化 UI 渲染?

相关文章

网友评论

      本文标题:UI 优化(下):如何优化 UI 渲染?

      本文链接:https://www.haomeiwen.com/subject/cgptwhtx.html