Android 渲染优化

参考文章

  • 目标:保证稳定的帧率来避免卡顿
  • 调试GPU过度绘制,将Overdraw降低到合理范围内;
  • 减少嵌套层次及控件个数,保持view的树形结构尽量扁平(使用Hierarchy Viewer可以方便的查看),同时移除所有不需要渲染的view;
  • 使用GPU配置渲染工具,定位出问题发生在具体哪个步骤,使用TraceView精准定位代码;
  • 使用标签,Merge减少嵌套层次、ViewStub延迟初始化。
  • 在不影响布局层级的时候LinearLayout更好,否则RelativeLayout更好

渲染性能

目标:保证稳定的帧率来避免卡顿

这里写图片描述
60fps 16ms 保证稳定的帧率来避免卡顿。(根据Google官方出品的Android性能优化典范)60帧每秒是目前最合适的图像显示速度,绝大多数的Android设备也是按照每秒60帧来刷新的。为了让屏幕的刷新帧率达到60fps,我们需要确保在时间16ms(1000/60Hz)内完成单次刷新的操作(包括measure、layout以及draw),这也是Android系统每隔16ms就会发出一次VSYNC信号触发对UI进行渲染的原因。如果整个过程在16ms内顺利完成则可以展示出流畅的画面,无法完成本次刷新操作,就会产生掉帧的现象,如果出现一次掉帧,用户就会感觉画面不是很顺畅,如果出现多次掉帧,用户就会明显感觉到卡顿,当多次掉帧时,用户正在与界面交互,卡顿会更加明明显。

Android渲染管道分为两个关键的部分。
1. CPU
2. GPU

Android渲染机制(View绘制原理)
Android渲染机制(View绘制原理) :
CPU进行 measure(测量对应View的onMeasure) layout(布局对应 onLayout ) record(记录) execute(执行),GPU 进行rasterization(栅格化,主要看综述)

CPU方面最常见的性能问题是: 多余的布局失效的布局
CPU方面最常见的性能问题
unnecessay 多余
invalidation失效
这些布局必须在视图层次结构中进行 测量,清除并重新创建。
引发这个问题主要有两个原因
1. 重建显示列表的次数太多(估计和ViewGroup的嵌套层级多少有关)
2. 花费了太多时间作废视图(view)层次并进行不必要的重绘
这两个原因导致更新视图列表(displayList)以及相关GPU资源时导致CPU过度运行

GPU的性能问题overdraw(过度绘制)
这里写图片描述
浪费GPU处理时间的原因:通常是在像素着色的过程中,像素着色后又改变的颜色(一个像素渲染多次颜色)

CPU和GPU渲染性能问题的整体解决流程
CPU和GPU渲染性能问题的整体解决流程
Activity是如何绘制到屏幕上的? 复杂的XML布局文件和标记语言是如何转换成用户能看懂的图像的?
实际上这些是由格栅化操作来完成,格栅化将诸如字符串、按钮、路径或者形状的一些高级对象在屏幕上进行显示时,拆分到不同的像素上。
这里写图片描述
格栅化是一个非常耗时的操作,手机有一个硬件(GPU)是专门进行格栅化操作。
这里写图片描述
现在的GPU使用一些指定的基础指令集,主要是处理多边形(polygons)和纹理(textsure)也就是图片

CPU在屏幕上绘制图像前会像GPU输入这些指令,这一过程通常使用的API就是Android OpenGL ES,这就是说,在屏幕上绘制UI对象时,无论是按钮 、路径(paths)、或复选框,都需要先在CPU中转换为多边形或纹理,然后传递给GPU进行格栅化。
这里写图片描述
你可以想象 一个UI对象转换为一系列多边形和纹理的时候肯定相当耗时,从CPU上传数据到GPU同样也很耗时
这里写图片描述
所以很明显需要尽量减少对象的转换次数以及上传数据的次数

openGL ES API允许数据上传到GPU后可以对数据进行保存,当下次绘制一个按钮时,只需要在GPU的存储器里引用它,然后告诉openGL 如何绘制。
这里写图片描述
渲染性能优化就是尽可能快的上传数据到GPU,然后尽可能长的在不修改的条件下保存数据。
这里写图片描述
应为每次上传资源到GPU时,都会浪费浪费宝贵的处理时间。

Android系统Honeycomb(3.0)版本发布之后,整个UI渲染系统就在GPU中运行,之后的各个版本都在渲染系统性能方面有更多改进,Android系统在降低、重用GPU资源方面做了很多工作,这方面不用担心。比如:任何主题提供的资源例如Bitmaps、Drawables等都是一起打包到统一的纹理中,然后使用统一的网格工具上传到GPU,例如Line、patches,这样在每次需要绘制这些资源时,就不需要做任何的转换,他们已经存储在GPU中了,大大加快了这些视图类型的显示。
这里写图片描述
然而随着UI对象的不断升级,渲染流程也变得越来越复杂。例如说:绘制图像,就是把图片上传到CPU存储器,然后传递到GPU中去进行渲染。但使用路径就是另外一回事,你需要在CPU中创建一系列的多边形,甚至在GPU中创建遮掩纹理来定义路径(path)形状(shape)。
绘制字符更加复杂一些,首先需要在CPU中把字符绘制成图像,然后把图像上传的GPU进行渲染,再返回到CPU在屏幕上为每个字符串绘制一个正方形。
这里写图片描述
Android系统已经解决了大多数的性能问题,除非你要更高的要求,否则基本不会发现与GPU相关的问题。然而还有一个GPU性能瓶颈——过度绘制

过度绘制:指屏幕上的某个像素点在同一帧的时间被绘制了多少次。上面绘制的颜色把下面的颜色盖住了,只能看到上面的颜色,但实际上下面的颜色也绘制了(占用GPU资源),所以必须尽量减少过度绘制。
Android手机提供了查看过度绘制的工具。开发者选项->调试GPU过度绘制(我的是一加5)

首先要去掉不必要的背景和图片,他们不会在最终渲染图像中显示,但是会占用GPU资源。
其次是对重叠的屏幕区域进行定义,从而降低CPU和GPU的消耗

Avoid Overdraw调试GPU过度绘制,将Overdraw降低到合理范围内;参考(YouTube video文章
去掉window的默认背景
getWindow().setBackgroundDrawable(null);
使用了Android自带的一些主题时,window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。当我们的自定义布局时又添加了一张背景图或者设置背景色,那么DecorView的background此时对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。
去掉其他不必要的背景。设置背景时,尽量分别在子view上设置背景,减少重绘,而不是图方便直接设置在根View上。

Android系统会设法避免过度绘制,去掉那些最终不显示的UI组件、图片。这种优化类型叫做剪辑,它对UI性能非常重要,如果你能确定某个对象会被完全阻挡,那就完全没有必要绘制它,这是最重要的性能优化方法之一,而且由Android系统执行。不过这一技术无法应对复杂的自定义View,系统无法监测onDraw具体会执行怎么操作,这样底层系统就无法识别如何去绘制图像,系统很难将覆盖的View重渲染管道中清除。例如下面的牌只有最上面完全可见,其它的都被遮挡了,绘制重叠部分的像素就是浪费时间。
这里写图片描述
为了解决这个问题,可以使用使用canvas API帮助Android系统识别被遮挡,不需要绘制的部分

这里写图片描述
最有用的办法是适用Canvas.clipRect 可以帮助你识别给定View的图片边界,边界区域之外的任何绘制操作都会被忽视,如果碰到了此类重叠的View,这个方法特别好用,比如例子中的纸牌
这里写图片描述

如果你知道自定义View可见部分的范围,或者知道遮挡部分的范围,你就可以定义clipRect边界,可以避免遮挡区域的任何绘制操作。
这里写图片描述

clipRect API帮助帮助系统识别无需绘制的区域对自定义View进行剪辑的时候这个方法很有用。
比如说:你知道绘制的图像在剪辑矩形之外,这个方法就非常好用。幸运的是你不必去搞清楚重叠逻辑,可以使用Canvas.quickReject方法判定给定区域是否完全在剪辑矩形之外,这种情况下可以忽略全部绘制工作

渲染管道中CPU的部分
为了在屏幕上绘制某个东西,Android通常将高级的XML文件转换称为GPU能够识别的对象然后显示在屏幕上。这个操作是在DisplayList的帮助下完成。
这里写图片描述
DisplayList持有所有要交给GPU绘制到屏幕上的数据信息,包含GPU要绘制的全部对象的信息列表,还有执行绘制操作的OpenGL命令列表,在某个View第一次需要被渲染时Display会因此被创建,当这个View要显示到屏幕上时,我们会将绘制指令提交给GPU来执行DisplayList,我们下次渲染这个View时 比如说位置发生了变化,我们仅仅需要执行DisplayList就够了
这里写图片描述

但是如果你修改了View的某些可见组件的内容,那么之前的DisPlayList就无法继续使用了,这时需要重新创建一个DisplayList,重新执行渲染指令并更新到屏幕上。
这里写图片描述
任何时候View的绘制内容发生变化,都需要重新创建DisplayList并重新执行指令更新到屏幕上
这里写图片描述
这个流程的性能表现取决于View的复杂程度,取决于视觉变化的类型,同时对渲染管道也会产生一些影响。
例如某个文本框的尺寸突然变成当前的两倍,在改变尺寸前,需要通过父View重新计算,并摆放其他子View的位置
这里写图片描述
在这种情况下 我们改变了某个View,后面就会有很多工作要做,这些类型的视觉变化需要渲染管道的额外工作,让你的View的尺寸变化时,触发了测量操作,会经过整个View Hierarchy询问各个View新的尺寸。一旦改变View的大小就会触发上述过程,无论是填充或者图片尺寸,设置文本大小宽高等,如果你是改变对象位置、布局、某个View重新摆放子View都会触发布局操作(requestLayout(…)),会触发整个Hierarchy重新计算对象在屏幕上的新位置
这里写图片描述

现在Android系统已经非常善于处理记录并执行渲染管道,除非你要处理自定义View,或者同时需要绘制太多View,其他情况下一般不会耗费太多时间,测量和布局的性能也很好,但是当View Hierarchy(指XMLView层级)失控时,也容易出现问题,执行这些功能的时间是和View Hierarchy中需要处理的节点数成正比的,系统需要处理的View越多,处理的时间就越长,某些View可能比其他View要耗费更多时间,造成这种浪费的首要原因是,View Hierarchy中包含太多的无用View,这些View根本不会显示在屏幕上,一旦触发测量和布局操作,只会拖累应用程序的性能表现,可以通过Hierarchy Viewer工具找出并解决无用的View
AndroidStudio tool->android->android device monitor 打开 View Hierarchy工具

庞大的布局十分浪费资源,每个附加嵌套布局和内置视图都会直接影响你的应用程序的性能和响应灵敏度,因此你应该了解应用程序的行为模式

减少嵌套层次及控件个数,保持view的树形结构尽量扁平(使用Hierarchy Viewer可以方便的查看),同时移除所有不需要渲染的view;

  • 减少嵌套层次及控件个数:随着控件数量越多、布局嵌套层次越深,展开布局花费的时间几乎是线性增长,性能也就越差。原因如下
    • Android的布局文件的加载是LayoutInflater利用pull解析方式来解析,然后根据节点名通过反射的方式创建出View对象实例;
    • 同时嵌套子View的位置受父View的影响,类如RelativeLayout、LinearLayout等经常需要measure两次才能完成,而嵌套、相互嵌套、深层嵌套等的发生会使measure次数呈指数级增长,所费时间呈线性增长;
    • 使用Hierarchy Viewer这个方便可视化的工具,可以得到:树形结构总览、布局view、每一个View(包含子View)绘制所花费的时间及View总个数。

使用GPU配置渲染工具,定位出问题发生在具体哪个步骤,使用TraceView精准定位代码;

根据Android性能优化典范,打开设备的GPU配置渲染工具(我的手机是 GPU呈现模式分析)——》在屏幕上显示为条形图,可以协助我们定位UI渲染问题。
GPU配置渲染工具虽然可以定位出问题发生在某个步骤,但是并不能定位到具体的某一行;当我们定位到某个步骤之后可以使用工具TraceView进行更加详细的定位。TraceView的使用可以参照《Android性能优化(一)之启动加速35%》。

4.使用标签,Merge减少嵌套层次、ViewStub延迟初始化。

  • 使用Merge和ViewSub
    • Merge 消除一个view层级。
    • ViewStub :可以为ViewStub指定一个布局,在Inflate布局的时候,只有ViewStub会被初始化,然后当ViewStub被设置为可见的时候,或是调用了ViewStub.inflate()的时候,ViewStub所向的布局就会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局。
    • include标签
      include标签和布局性能关系不大,主要用于布局重用,一般和merge标签配合使用

5.在不影响布局层级的时候LinearLayout更好,否则RelativeLayout更好

在不影响布局层级的时候LinearLayout更好,否则RelativeLayout更好。LinearLayout易用,效率高,表达能力有限。RelativeLayout复杂,表达能力强,效率稍逊。LinearLayout只能用来描述一个方向上连续排列的控件,而RelativeLayout几乎可以用于描述任意复杂度的界面。表达能力越强的容器控件,性能往往略低一些,因为系统需要将更多的时间花在计算子控件的位置上

猜你喜欢

转载自blog.csdn.net/csdn1125550225/article/details/80401365