你真的了解过度绘制吗?

7,535 阅读12分钟
原文链接: blog.csdn.net

本文主要谈谈Android过度绘制相关的知识点,并结合云课堂企业版首页优化过度绘制的实践总结一些与之相关的问题,与君共享,互相进步~

本文将按照如下结构展开

1、 表面问题产生

2、 问题背后的探索

    ---- 什么是过度绘制
    ---- Android渲染机制

3、 优化过度常用工具和套路

4、 小结

0x00 表面问题的产生

在我的小米2s测试机,打开云课堂企业版,顺手打开了过度绘制发现了惊人的一幕

目前企业版线上版本才1.8.2版本,这么年轻的app,但是作为门面的三个tab竟然过度绘制如此严重(中间那张灰色的图是测试数据),没有道理不顺手解决一下

搬砖中~此处略过几个小时

优化后的效果

0x01 问题背后的探索

如果对这背后的相关原理不是太感兴趣的话可以直接参看下一小节。

什么是过度绘制

过度绘制(Overdraw)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构里面,如果不可见的 UI 也在做绘制的操作,会导致某些像素区域被绘制了多次,同时也会浪费大量的 CPU 以及 GPU 资源,绘制示意图如下图所示

在可以看出是一层层绘制,搞清楚过度绘制之前,还是得先了解一下Android渲染的相关机制

Android渲染机制

1、Android如何将代码绘制到手机屏幕上

我们知道手机是有许多像素点构成的通过显示每个像素点不同的颜色,然后组成各种图像;Activity的界面之所以可以被绘制到屏幕上其中有一个很重要的过程就是栅格化(Resterization),栅格化简单来说就是将向量图转化为机器可以识别的位图的一个过程。其中很复杂也比较很耗时

GPU就是用来加快栅格化的速度。

下面用一张图来介绍一下渲染的大致过程

从上面的图可以看出,CPU会先把UI组件计算成polygons(多边形)和textures(纹理),然后再交给GPU进行栅格化渲染,最后GPU再将数据传送给屏幕,由屏幕进行绘制显示。当然,从CPU到GPU还需要经过OpenGL ES的处理,这也是一个很复杂的过程。

因此我们可以看出Android的渲染依赖于两个核心组件

  • CPU:负责包括Measure,Layout,Record,Execute的计算操作 (容易产生不必要的重绘)
  • GPU:负责Rasterization(栅格化)操作 (容易产生过度绘制的问题)

因此我们也可以看出二者各种背负者的的问题

在GPU方面,最常见的问题是我们所说的过度绘制(overdraw),通常是在像素着色过程中,通过其他工具进行后期着色时浪费了GPU处理时间。

2、 手机像素点数据哪里来? Frame Buffer和Back Buffer

手机屏幕是由许多的像素点组成的,通过让每一个像素点显示不同的颜色,可以组合成各种各样的图像。这些像素点的颜色数据从哪里来?

GPU控制的一块缓冲区中,这块缓冲区叫做Frame Buffer(也就是帧缓冲区)。你可以把它简单理解成一个二维数组,数组中的每一个元素对应着手机屏幕上的一个像素点,元素的值代表着屏幕上对应的像素点要显示的颜色。优化屏幕画面不断变化,需要这个buffer不断地更新数据,一个FrameBuffer肯定是应接不暇的,因此GPU除了Frame Buffer,用以交给手机屏幕进行绘制外,还有一个缓冲区,叫Back Buffer,这个Back Buffer 用以交给你的应用,让你往里面填充数据。GPU会定期交换Back Buffer和Frame Buffer,也就是让Back Buffer 变成Frame Buffer交给屏幕进行绘制,让原先的Frame Buffer变成Back Buffer交给你的应用进行绘制。交换的频率也是60次/秒,这就与屏幕硬件电路的刷新频率保持了同步。如下图所示:

关于缓冲机制如双缓冲、三缓冲这里就不展开介绍了,有兴趣的可以自行查阅相关资料~

3、 关于 VSync

刷新屏幕的时机就由VSync信号来控制,由于人眼与大脑之间的协作无法感知超过60fps的画面更新,(1000/60hz = 16ms),因此需要在16ms内完成屏幕刷新的全部逻辑操作,否则就会出现画面丢失造成卡顿。

VSync(Vertical Synchronization),就是所谓的“垂直同步”。在Android中这也沿用了这个概念,我们也可以把它理解为“帧同步”。这个用保证CPU、GPU生成帧的速度和display刷新的速度保持一致,Android系统每16ms就会发出一次VSYNC信号触发UI渲染更新。上面提到屏幕一秒刷新60次,这就要求CPU和GPU每秒要有处理60帧的能力,一帧花费的时间在16ms内。那么在Android系统中,是如何利用VSYNC工作的呢,如下图

设计预期是这样的一个过程:在VSYNC信号刚开始发出时,Android系统就立刻开始了下一帧数据的处理了,这样就不会浪费时间了。图中先显示第0帧,在这16ms显示时间里,CPU和GPU已经开始准备下一帧的数据了,赶在下个VSYNC信号到来时,GPU渲染完成,及时交换数据,display绘制显示完成,每一帧如果可以井然有序的进行,那么用户就不会感到卡顿了,看起来很美妙,难道不是么?

0x02 优化过度绘制常见工具和套路

通过上面的一些理论学习,我们更好了了解了关于过度绘制相关的一些细节,下面将来介绍一下如何解决过度绘制的常见工具和套路

常见工具

  • 手机检测工具

按照以下步骤打开Show GPU Overrdraw的选项:设置 -> 开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制

每个颜色的说明如下:

原色:没有过度绘制
蓝色:1 次过度绘制
绿色:2 次过度绘制
粉色:3 次过度绘制
红色:4 次及以上过度绘制

过度绘制的存在会导致界面显示时浪费不必要的资源去渲染看不见的背景,或者对某些像素区域多次绘制,就会导致界面加载或者滑动时的不流畅、掉帧,对于用户体验来说就是 App 特别的卡顿。为了提升用户体验,提升应用的流畅性,优化过度绘制的工作还是很有必要做的。

  • Android Studio检测工具

Tools-> Android -> Android Device Monitor

HierarchyViewer

进行UI布局复杂程度及冗余等分析,要使用这个工具需要最好在虚拟机上才能work,不然会报

Unable to get view server version from device

如果需要真机上使用这个功能Android SDK开发团队提供了VIewServer开源库,项目地址github.com/romainguy/V…,需要在Activity的onCreate、onDestroy、onResume三个生命周期方法中调用ViewServer的对应方法即可在真机环境下使用HierarchyView工具

由于布局过于复杂,这里截取其中一部分

一个Activity的View树,通过这个树可以分析出View嵌套的冗余层级,左下角可以输入View的id直接自动跳转到中间显示;Save as PNG用来把左侧树保存为一张图片;Capture Layers用来保存psd的PhotoShop分层素材;右侧剧中显示选中View的当前属性状态;右下角显示当前View在Activity中的位置等;左下角三个进行切换;Load View Hierarchy用来手动刷新变化(不会自动刷新的)。当我们选择一个View后会如下图所示:

类似上图可以很方便的查看到当前View的许多信息;上图最底那三个彩色原点代表了当前View的性能指标,从左到右依次代表测量、布局、绘制的渲染时间,红色和黄色的点代表速度渲染较慢的View(当然了,有些时候较慢不代表有问题,譬如ViewGroup子节点越多、结构越复杂,性能就越差)。

当然了,在自定义View的性能调试时,HierarchyViewer上面的invalidate Layout和requestLayout按钮的功能更加强大,它可以帮助我们debug自定义View执行invalidate()和requestLayout()过程,我们只需要在代码的相关地方打上断点就行了,接下来通过它观察绘制即可。

不过有时候布局嵌套过深通过这个来看其实有点不现实,这时候需要结合另外一个工具

布局分析器

这个工具通常也可以用来分析别人app UI的实现,非常强大

你可以查看你当前UI截图任意控件,比如觉得图片那块过度绘制这么严重,点击看一下,可以看到对应的布局,资源id,全局查找一下,就可以到对应的位置去看看代码,这样就可以通过可视化快速定位到问题代码,进行修复

常用套路

通过以上的工具找到问题之后就是如何去处理了,下面总结了一些常用套路

  • 套路一:去掉window的默认背景

当我们使用了Android自带的一些主题时,window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。当我们的自定义布局时又添加了一张背景图或者设置背景色,那么DecorView的background此时对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。去掉window的背景可以在onCreate()中setContentView()之后调用

getWindow().setBackgroundDrawable(null);

或者在theme中添加

android:windowbackground="null";
  • 套路二:去掉其他不必要的背景

有时候为了方便会先给Layout设置一个整体的背景,再给子View设置背景,这里也会造成重叠,如果子View宽度mach_parent,可以看到完全覆盖了Layout的一部分,这里就可以通过分别设置背景来减少重绘。再比如如果采用的是selector的背景,将normal状态的color设置为“@android:color/transparent”,也同样可以解决问题。这里只简单举两个例子,我们在开发过程中的一些习惯性思维定式会带来不经意的Overdraw,所以开发过程中我们为某个View或者ViewGroup设置背景的时候,先思考下是否真的有必要,或者思考下这个背景能不能分段设置在子View上,而不是图方便直接设置在根View上。

  • 套路三:clipRect的使用

我们可以通过canvas.clipRect()来 帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠 组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。

  • 套路四:ViewStub ViewStub称之为“延迟化加载”,在很多数情况下,程序无需显示ViewStub所指向的布局文件,只有在特定的某些较少条件下,此时ViewStub所指向的布局文件才需要被inflate,且此布局文件直接将当前ViewStub替换掉,具体是通过viewStub.infalte()或viewStub.setVisibility(View.VISIBLE)来完成;常见的比如网络加载布局

  • 套路五:Merge标签

MMerge标签可以干掉一个view层级。Merge的作用很明显,但是也有一些使用条件的限制。有两种情况下我们可以使用Merge标签来做容器控件。第一种子视图不需要指定任何针对父视图的布局属性,就是说父容器仅仅是个容器,子视图只需要直接添加到父视图上用于显示就行。另外一种是假如需要在LinearLayout里面嵌入一个布局(或者视图),而恰恰这个布局(或者视图)的根节点也是LinearLayout,这样就多了一层没有用的嵌套,无疑这样只会拖慢程序速度。而这个时候如果我们使用merge根标签就可以避免那样的问题。另外Merge只能作为XML布局的根标签使用,当Inflate以开头的布局文件时,必须指定一个父ViewGroup,并且必须设定attachToRoot为true。不常用的UI被设置成GONE,比如异常的错误页面,如果有这类问题,我们需要用标签,代替GONE提高UI性能。,毕竟visible/gone是会引起布局重绘的

  • 套路六:inlcude标签

标签能够重用布局文件

还有可以尝试使用ConstraintLayout也可以大大减少布局的嵌套,提高UI性能等。

小结

事情虽小,也不是什么大的难点,但是这个背后的牵涉的面还是很广的,还是值得去总结总结,有助于以点到面的构建自己的知识体系。

参考链接