Android-安卓应用的过度渲染优化
什么是过度渲染
GPU过渡绘制的概念:GPU过度绘制指的是在屏幕一个像素上绘制多次(超过一次),比如一个TextView后有背景,那么显示文本的像素至少绘了两次,一次是背景,一次是文本
如果你粉刷过一个房间或一所房子,就会知道给墙壁涂上颜色需要做大量的工作。假如你还要重新粉刷一次的话,第二次粉刷的颜色会覆盖住第一次的颜色,第一次的颜色就永远不可见了,等于你第一次粉刷做的大量工作就完全被浪费掉。这太可怕了。
同样的道理,如果在你的应用程序中浪费精力去绘制一些东西同样会产生性能问题。过度绘制这个名词就是用来描述屏幕上一个像素在单个帧中被重绘了多少次。
过度绘制分析
- view tree的绘制过程:
View本身大小多少,这由onMeasure()决定,在viewtree 上递归调用该函数来计算每个view大小。
View在ViewGroup中的位置如何,这由onLayout()决定,在viewtree 上递归调用该函数来计算每个view在父view中的位置。
绘制View,onDraw()定义了如何绘制这个View。在viewtree 上递归调用该函数来绘制每个view。
显然view tree 层级越深,过程越长,越可能产生卡顿
不同的颜色代表什么?
image.png- 原始色(即没有发生改变):代表屏幕上该像素块被绘制了一次。
- 蓝色(过度绘制1x):代表屏幕上该像素块被绘制了2次。
- 绿色(过度绘制2x):代表屏幕上该像素块被绘制了3次。
- 粉色(过度绘制3x):代表屏幕上该像素块被绘制了4次。
- 红色(过度绘制4x):代表屏幕上该像素块被绘制了5次。
过度绘制的检测和检验
-
手机检测工具
开发者选项开启GPU过度绘制调试:按照以下步骤打开Show GPU Overrdraw的选项:设置 -> 开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制
image.png
image.png -
Android Studio检测工具
image.png
Tools-> Android -> Android Device Monitor
HierarchyViewer
进行UI布局复杂程度及冗余等分析,要使用这个工具需要最好在虚拟机上才能work,不然会报
Unable to get view server version from device
如果需要真机上使用这个功能Android SDK开发团队提供了VIewServer开源库,项目地址https://github.com/romainguy/ViewServer
需要在Activity的onCreate、onDestroy、onResume三个生命周期方法中调用ViewServer的对应方法即可在真机环境下使用HierarchyView工具.
由于布局过于复杂,这里截取其中一部分
image.png
一个Activity的View树,通过这个树可以分析出View嵌套的冗余层级,左下角可以输入View的id直接自动跳转到中间显示;Save as PNG用来把左侧树保存为一张图片;Capture Layers用来保存psd的PhotoShop分层素材;右侧剧中显示选中View的当前属性状态;右下角显示当前View在Activity中的位置等;左下角三个进行切换;Load View Hierarchy用来手动刷新变化(不会自动刷新的)。当我们选择一个View后会如下图所示:
image.png
类似上图可以很方便的查看到当前View的许多信息;上图最底那三个彩色原点代表了当前View的性能指标,从左到右依次代表测量、布局、绘制的渲染时间,红色和黄色的点代表速度渲染较慢的View(当然了,有些时候较慢不代表有问题,譬如ViewGroup子节点越多、结构越复杂,性能就越差)。
当然了,在自定义View的性能调试时,HierarchyViewer上面的invalidate Layout和requestLayout按钮的功能更加强大,它可以帮助我们debug自定义View执行invalidate()和requestLayout()过程,我们只需要在代码的相关地方打上断点就行了,接下来通过它观察绘制即可。
不过有时候布局嵌套过深通过这个来看其实有点不现实,这时候需要结合另外一个工具
布局分析器
image.png
解决方案
-
太多重叠的背景
这个问题其实最容易解决,建议就是检查你在布局和代码中设置的背景,有些背景是被隐藏在底下的,它永远不可能显示出来,这种没必要的背景一定要移除,因为它很可能会严重影响到app的性能。
去掉window的默认背景:
当我们使用了Android自带的一些主题时,window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。当我们的自定义布局时又添加了一张背景图或者设置背景色,那么DecorView的background此时对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。
查看Android源码里的Theme主题,如下:
<style name="Theme">
...
<!-- Window attributes -->
<item name="windowBackground">@drawable/screen_background_selector_dark</item>
...
</style>
去掉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区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。
3. 太多重叠的view
套路一: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标签
标签能够重用布局文件
还有一些其他避免过度绘制的方法建议总结:
- 尽量多使用RelativeLayout和LinearLayout,不要使用绝对布局AbsoluteLayout。
在布局层次一样的情况下,建议使用LinearLayout代替RelativeLayout,因为LinearLayout性能要稍高一些。
在完成相对较复杂的布局时,建议使用RelativeLayout,RelativeLayout可以简单实现LinearLayout嵌套才能实现的布局。 - 将可复用的组件抽取出来并通过include标签使用
- 使用ViewStub标签来加载一些不常用的布局
- 动态地inflation view性能要比SetVisiblity性能要好,当然ViewStub是最好的选择。
- 使用merge标签减少布局的嵌套层次。
- 去掉多余的背景颜色。
- 对于有多层背景颜色的Layout来说,留最上面的一层的颜色即可,其他底层的颜色都可以去掉。
- 对于使用Selector当背景的Layout(比如ListView的Item,会使用Selector来标记点击,选择等不同的状态),可以将normal状态的color设置成"@android:color/transparent",来解决对应的问题。
内嵌使用包含layout_weight属性的LinearLayout会在绘制时花费昂贵的系统资源,因为每一个子组件都需要被测量两次。在使用ListView与GridView的时候,这个问题显得尤为重要,因为子组件会重复被创建,所以要尽量避免使用layout_weight。 - 使得Layout宽而浅,而不是窄而深(在Hierarchy Viewer的Tree视图里面体现)。