在上一篇博客当中具体讲解了为什么内存抖动和耗时的复杂计算会导致UI卡顿.
其中还穿插了一些UI的渲染机制的知识。
这篇博客将介绍对于渲染最重要的CPU与GPU如何去优化。
具体的GPU与CPU的知识上篇博客中有很详细的讲解。
首先我们来看从GPU角度去优化:
GPU主要是用来栅格化的,GPU通常出现的问题是过渡绘制
那什么是过度绘制呢?
如果我们曾经粉刷过房子,我们应该知道,给墙壁粉刷工作量非常大,如果我们需要重新粉刷,第一次的粉刷就白干了。同样的道理,我们的应用程序会因为过度绘制,从而导致性能问题,如果我们想兼顾高性能和完美的设计,往往会碰到一种性能问题,即过度绘制。过度绘制是一个术语,指的是屏幕上的某个像素点在同一帧的时间内被绘制了多次。假如我们有一堆重叠的UI卡片,最接近用户的卡片在最上面,其余卡片都藏在下面,也就是说我们花大力气绘制的那些下面的卡片基本都是不可见的。
问题就在于此,因为每次像素经过渲染后,并不是用户最后看到的部分,这就是在浪费GPU的时间。目前流行的一些布局是一把双刃剑,带给我们漂亮视觉感受的同时,也造成过度绘制的问题,为了最大限度地提高应用程序的性能,我们必须尽量减少过度绘制。幸运的是,Android手机提供了查看过度绘制情况的工具,在开发者选项中打开“Show GPU overdraw”选项,手机屏幕显示会出现一些异常不用过于惊慌,Android在屏幕上使用不同颜色,标记过度绘制的区域,如果某个像素点只渲染了一次,我们看到的是它原来的颜色,随着过度绘制的增多,标记颜色也会逐渐加深,例如1倍过度绘制会被标记为蓝色,2倍、3倍、4倍过度绘制遵循同样的模式。所以当我们调试应用程序的用户界面时,目标就是尽可能的减少过度绘制,将红色区块转变成蓝色区块,为了完成目标有两种清楚过度绘制的方法,首先要从视图中清楚那些,不必要的背景和图片,他们不会在最终渲染图像中显示,记住,这些都会影响性能。其次,对视图中重叠的屏幕区域进行定义,从而降低CPU和GPU的消耗,接下来我们深入了解过度绘制如何去优化。
一般在咱们开发应用的时候都是去继承AppCompatActivity(再去继承activity就只能说明你out了),但是这存在弊端,因为Android默认的Theme是带有灰色背景的,但是我们很多人平常开发的时候,喜欢在顶级布局给他添加纯白色背景,这样子就与Android的材料主题默认设置相冲突,特别是窗口背景图片,这这些都导致了不必要的过度绘制,作为一个开发者我们必须做一个决定,我们希望保留白色背景,材料主题其实没有任何意义,我们能做的一个优化就是把Activity的背景图片设置为null,展示一下在代码中如何实现:
除了上述的之外,还有很多人在写一些布局文件的时候,不注意background的使用,
产生了很多不必要的背景,很重要的一个清理原则就是背景不能叠加,如果你发现你的布局文件某个控件的背景与子view的背景重叠,例如现在有一个子view的宽高都是设置的match_parent,背景是绿色(实际需求就是要显示绿色),这个时候你的父布局就不能再有背景颜色了,因为需要只需要显示一种颜色,我只需要设置子view的背景颜色,没有必要去设置父容器的背景颜色,很多人可能听了感觉这不是废话嘛,但是事实上开发中有很多人是这样做的,可能是由于他们的疏忽或者其他原因,总之,要去除掉重复的背景,避免过度绘制。下面这张图原先存在着很多重复的背景,现在经过优化后,明显改善了很多。
除了背景的重叠会导致过度绘制之外,还有一种情况会导致过度绘制,通常存在于自定义控件中,比如下面这张图:
这叠牌只有最上面的牌是完全可见的,其他牌都被挡住了,这就意味着绘制那些重叠的像素就是浪费时间。
如果我们能确定某个对象会被完全阻挡,那就完全没有必要绘制它,事实上,这是最重要的性能优化方法之一,而且是由Android系统执行的,但是不幸的是,这一技术无法应对复杂的自定义的View,系统无法检测onDraw具体会执行什么操作。这些情况下,底层系统无法识别如何去绘制对象,系统很难将覆盖的View,从渲染管道中清除。
为了解决这个问题,我们可以使用Canvas类的一些特别方法去帮助Android系统识别被遮挡的不需要绘制的部分,最有用的办法是Canvas.clipRect,它可以帮助我们识别给定View的图片边界,边界之外区域的任何绘制操作会被忽视,如果碰到此类重叠的View,这个方法特别好用,就像例子中的纸牌。如果我们知道自定义View可见部分的范围,或者知道遮挡部分的范围,我们就可以定义ClipRect边界,可以避免遮挡区域的任何绘制操作,ClipRect API帮助系统识别出无需绘制的区域,对自定义View进行剪辑时,这个方法也很有用处。比如说,如果我们知道绘制对象在剪辑矩形之外,这个方法就非常好用,幸运的是,我们不必亲自搞清楚重叠逻辑,我们可以使用Canvas.quickReject方法,判定给定区域是否完全在剪辑矩形之外,这种情况下可以忽略全部绘制工作。
上面讲了从GPU的角度去优化渲染,
接下来就从CPU的角度去优化吧:
为了在屏幕上绘制某个东西,Android通常将高级XML文件转换为GPU能够识别的对象,然后显示在屏幕上,这个操作是在DisplayList的帮助下完成的,DisplayList持有所有要交给GPU绘制到屏幕上的数据信息,包含GPU要绘制的全部对象的信息列表,还有执行绘制操作的OpenGL命令列表,在某个View第一次需要被渲染时,DisplayList会因此被创建,当这个View要显示到屏幕上时,我们将绘制指令提交给GPU来执行DisplayList,我们下次渲染这个View时,比如说位置发生了变化,我们仅仅需要执行DisplayList就够了,但是如果我们修改了View的某些可见组件的内容,那么之前的DisplayList就无法继续使用了,这时我们要重新创建一个DisplayList,重新执行渲染指令并更新到屏幕上,请注意,任何时候View的绘制内容发生变化,都需要重新创建DisplayList并重新执行指令更新到屏幕,这个流程的表现性能,取决于我们的View的复杂程度,取决于视觉变化的类型,同时对渲染管道也会产生一些影响。举例说,假如某个文本框尺寸突然变成当前的两倍,在改变尺寸前,需要通过父View重新计算,并摆放其他子View的位置,在这种情况下我们改变了某个View,后面就会有很多工作要做,这些类型的视觉变化需要渲染管道的额外工作,当我们的View的尺寸变化时,触发了测量操作,会经过整个View Hierarchy,询问各个View的新尺寸,我们一旦改变了View的大小就会触发上述过程,无论是填充或者图片尺寸、设置文本大小、宽度、高度等等,如果我们是改变对象位置或者询问布局,或者某个View重新摆放子View都会触发布局操作,会触发整个Hierarchy重新计算对象在屏幕上的新位置,现在Android运行系统已经非常善于处理记录并执行渲染管道,除非我们要处理自定义View或者同时需要绘制太多View,其他情况下一般不会耗费太多时间,测量和布局操作性能也很好,但是当我们的View Hierarchy失控时也更容易出现问题,执行这些功能的时间是和我们的View Hierarchy中需要处理的节点数成正比的,系统需要处理的View越多处理时间就越长。某些View可能比其他View要耗费更多时间,造成这种浪费的首要原因是,View Hierarchy中包含太多的无用View,这些View根本不会显示在屏幕上,一旦触发测量操作和布局操作只会拖累应用程序的性能表现,幸好有一款叫做Hierarchy Viewer工具,它可以帮助我们查找并修复这些流氓View,我们来看看。这里就不介绍如何使用Hierarchy Viewer工具了(下篇博客补充介绍)。
下面这是某布局的的viewTree:
我们可能注意到每个视图有三个独特的圆点,它们可能具有不同的颜色,绿色、黄色或红色。但是,这些圆点的顺序也具有特定的含义。最左边的圆点表示渲染管道的测量阶段,中间的圆点表示布局阶段,最右边的圆点表示渲染管道的绘制阶段。现在,我来介绍这些颜色的含义,这些圆点的颜色表示这个节点相对于所有其他已经概要显示的节点的性能。那么,相对性能是什么意思呢?让我们来看这个绿色表示这个管道阶段,这个视图的渲染速度快于至少一半以上的其他视图。这个黄色表示它的渲染速度属于比较慢的50%,如果我们看到红色,意味着这是视图层级中最慢的节点,我们还应该知道红色节点可能是存在问题。我们可能不希望它的渲染速度如此之慢。我们要做的就是要尽可能的去简化布局层次,从渲染过程的角度来说线性布局设计比相对布局更慢一些,与相对布局比较,线性布局需要更多的资源开销,这里全部是绿色。如果我们有机会采取扁平化布局,我们应该想办法尽可能使用它。
除此之外还有一些优化的建议:
1):当我们的布局是用的FrameLayout的时候,我们可以把它改成merge
可以避免自己的帧布局和系统的ContentFrameLayout帧布局重叠造成重复计算(measure和layout)
2):ViewStub:当加载的时候才会占用。不加载的时候就是隐藏的,仅仅占用位置。
总体优化思想:查看自己的布局,层次是否很深以及渲染比较耗时,然后想办法能否减少层级以及优化每一个View的渲染时间。