android硬件加速

来源:互联网 发布:炒股软件开发公司 编辑:程序博客网 时间:2024/06/15 12:33
Android应用的开发过程中,我们经常会接触到“硬件加速”这个词。由于操作系统对底层软硬件封装非常完善,上层软件开发者往往对硬件加速的底层原理了解很少,也不清楚了解底层原理的意义,因此常会有一些误解,如硬件加速是不是通过特殊算法实现页面渲染加速,或是通过硬件提高CPU/GPU运算速率实现渲染加速?都不是的,其实硬件加速就是运用GPU优秀的运算能力来加快渲染的速度,而通常的基于软件的绘制渲染模式是完全利用CPU来完成渲染。
本文尝试从底层硬件原理,一直到上层代码实现,对硬件加速技术进行简单介绍,其中上层实现基于android 6.0分析。
安卓的硬件加速是自安卓3.0(API 11)之后才有的,安卓2D绘制支持硬件加速,但因为启用硬件加速需要增加资源,所以将会消耗更多的RAM。

在API14之后(包括14),硬件加速默认是开启的,侧面也说明,从11-13默认是关闭的,不过这个现在我们已经不考虑了,毕竟现在开发都不考虑那么低版本的手机了。启用硬件加速的最简单方式就是整个application都启用,如果只是使用标准的view或者drawable,那么应该不会产生不好的绘制效果。但是,因为硬件加速并不支持所有的2D绘制操作,如果你的应用有自定义的view或者调用某些绘制函数,启用硬件加速可能会对你的应用有一些影响,比如看不见一些元素或者呈现错误的像素,所以为了解决这个不兼容的问题,google大佬们的建议是,通过真机上启用硬件加速测试,遇到问题的话对以下四个级别进行控制,我们可以选择启用或者不启用硬件加速:
Application级别:
在安卓manifest文件,添加下列属性到<application>标签就可以给整个应用添加硬件加速:

Activity级别:
如果不想全局都启用硬件加速,那么个别Activity可以不指定硬件加速,在Activity级别启动或者不启用硬件加速,你可以<activity>标签指定android:hardwareAccelerated属性

window级别:
如果需要更细粒度的控制,可以用以下代码为一个window启用硬件加速

PS:目前还不允许在window级别关闭硬件加速的
view级别:
你可以在运行时对指定的View关闭硬件加速

第二个参数是Paint
或者在xml中的控件属性中,使用android:layerType=”software”来关闭硬件加速:比如

PS:因为硬件加速需要加载系统资源,目前还不允许运行时启用硬件加速 ,View layers还有一些函数是不支持硬件加速的。
判断一个View是否开启硬件加速
有时候我们想知道应用是否开启了硬件加速,尤其是对于自定义View的情景,如果你的应用有很多的自定义绘制并且不是所有的操作都支持新的绘制管线时(硬件加速)。那么这两个不同的方法就变得特别的有用,可以检测应用是否是启用硬件加速:

View.isHardwareAcclerated() 如果View是依附到启动了硬件加速的window返回true

Canvas.isHardwareAccelerated如果canvas是硬件加速的 返回true

但是,强烈建议使用Canvas.isHardwareAcclerated代替View.isHardwareAcclerated,因为View的这个方法真的非常不靠谱,即使这个View是依附在一个硬件加速的Window上,但是仍然可以使用一个不启用硬件加速的Canvas进行绘制,比如当我们把View绘制到bitmap上的时候。


不支持的绘制操作

当启用了硬件加速后,2D渲染管线支持大部分的canvas的常用绘制操作和一些不常用的操作,分别列举一下,其实我们也只有在自定义View的时候才担心canvas调用的函数会不会不支持硬件加速,所以我们只需要真正用到的时候查阅一下即可。

View Layers
在android的所有版本中views可以离屏渲染,使用view的绘制缓存或者通过使用Canvas.saveLayer()。离屏缓存,或者图层,有几种用途。你可以使用它们获得更好的显示效果,当对一些复杂的views使用动画效果或者一些合成效果,比如,你可以使用Canvas.saveLayer()实现渐变效果来临时渲染一个view到一个layer,然后把不透明的元素合成到屏幕上。

android3.0开始,使用layers的时候有更多的控制方法了,通过View.setLayerType()函数,这个API有两个参数:layer的类型,和一个可选的描述layer如何合成的Paint对象,你可以使用Paint参数来给layer添加Color Filter,指定混合模式,或者不透明度,可以选择以下三种layer type:
LAYER_TYPE_NONE:view只会普通地进行渲染,并且不会使用离屏缓存回退,这是默认的行为。
LAYER_TYPE_HARDWARE:如果应用启动了硬件加速,那么这个view就会使用硬件里面的texture渲染,利用GPU来加速渲染,如果应用不能够硬件加速,那么它的效果就和LAYER_TYPE_SOFTWARE一样。
LAYER_TYPE_SOFTWARE:这个view将会使用CPU来渲染到一个bitmap里。如果对某个View设置View .setLayerType(View.LAYER_TYPE_SOFTWARE,null);关闭硬件加速以后,这个view将会使用CPU来渲染到一个bitmap里,并且系统会把当前view转换后的bitmap保存,导致内存使用非常大,所以在离开页面后最好清空View的内容并且调用View .destroyDrawingCache();释放bitmap占用的缓存,否则多开几个页面就内存溢出了,特别是对WebView和ImageView关闭硬件加速尤为需要注意这个问题。
texture:纹理,在3D游戏开发里面叫做贴图,存放图片到texture里面,运行时会读取,而在这里当我们需要重绘的时候也是一样的原理,从硬件中读取纹理然后进行绘制。
这几种类型的layer取决于你的用途:
Performance:使用一个硬件图层类型来渲染view到硬件中的texture,自从view被渲染到一个图层,它的绘制代码就不再被执行,直到view调用invalidate方法,一些动画,例如alpha动画,使用GPU来实现就可以高效地直接作用于图层上。
Visual effects:使用一个硬件或者软件图层类型,还有一个Paint对view进行一些特殊的处理,比如,你可以使用黑色和白色通过ColorMatrixColorFilter来绘制一个view。
Compatibility:使用一个软件图层类型来强迫一些view使用软件来渲染,如果一个view是硬件加速的(比如,如果你整个应用都是硬件加速的),那么就会出现渲染问题,最简单的解决方式就是限制硬件渲染管道(关闭View的硬件加速)。
View layer和animations

当你的应用是硬件加速的,硬件图层类型可以传达更快和更加顺滑的动画,当你在处理的是一个复杂的又很多绘制操作的view的时候,运行一个动画不总是60帧每秒的。可以通过使用硬件层来渲染view到一个硬件的texture中来优化这个问题,硬件texture可以用来对view进行动画,排除开始动画的时候需要重绘自己的View,view不会重新重绘除非你改变它的属性,然后调用invalidate()。如果你运行一个动画在你的应用上,但是得不到一个你想要的顺滑结果,考虑启用硬件加速在你的动画view上。当view从硬件图层回退的时候,它的一些属性会通过图层合成到屏幕上的方式进行处理,设置这些属性将会更加高效,因为它不需要view重绘或者无效化,下面的一些属性可以通过这种方式来合成到屏幕上,调用这些属性的setter方法就可以在目标view上实现而不需要重绘。这个过程利用GPU优秀的运算能力完成,不消耗CPU。
alpha:改变图层的不透明度
x,y,translationX,translationY:改变图层的位置
scaleX,scaleY:改变图层的大小
rotation,rotaionX,rotationY:改变图层在三维空间的排列方向
pivotX,pivotY:改变图层的转移点

这些属性都是同样的用法,当一个view使用ObjectAnimator,如果你想要访问这些属性,调用这些属性的getter和setter,比如,为了修改alpha属性,调用setAlpha,接下来的代码片段展示了最有效的方式来围绕Y轴旋转一个view:
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator.ofFloat(view, "rotationY", 180).start();

因为硬件层消耗video存储,所以强烈建议启用它们只有在动画开始时,并且当动画完成的时候关闭它们,你可以完成这个通过使用动画监听器,这一个比较细节,但能够对View进行优化,毕竟手机内存这么少
View.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180);
animator.addListener(new AnimatorListenerAdapter() {

@Override
public void onAnimationEnd(Animator animation) {
view.setLayerType(View.LAYER_TYPE_NONE, null);
}
});
animator.start();

了解硬件加速对App开发的意义
对于App开发者,简单了解硬件加速原理及上层API实现,开发时就可以充分利用硬件加速提高页面的性能。以Android举例,实现一个圆角矩形按钮通常有两种方案:使用PNG图片;使用代码(XML/Java)实现。简单对比两种方案如下。
方案原理特点使用PNG图片(BitmapDrawable)解码PNG图片生成Bitmap,传到底层,由GPU渲染图片解码消耗CPU运算资源,Bitmap占用内存大,绘制慢使用XML或Java代码实现(ShapeDrawable)直接将Shape信息传到底层,由GPU渲染消耗CPU资源少,占用内存小,绘制快

页面渲染背景知识

  • 页面渲染时,被绘制的元素最终要转换成矩阵像素点(即多维数组形式,类似安卓中的Bitmap),才能被显示器显示。
  • 页面由各种基本元素组成,例如圆形、圆角矩形、线段、文字、矢量图(常用贝塞尔曲线组成)、Bitmap等。
  • 元素绘制时尤其是动画绘制过程中,经常涉及插值、缩放、旋转、透明度变化、动画过渡、毛玻璃模糊,甚至包括3D变换、物理运动(例如游戏中常见的抛物线运动)、多媒体文件解码(主要在桌面机中有应用,移动设备一般不用GPU做解码)等运算。
  • 绘制过程经常需要进行逻辑较简单、但数据量庞大的浮点运算。

CPU与GPU结构对比

CPU(Central Processing Unit,中央处理器)是计算机设备核心器件,用于执行程序代码,软件开发者对此都很熟悉;GPU(Graphics Processing Unit,图形处理器)主要用于处理图形运算,通常所说“显卡”的核心部件就是GPU。
下面是CPU和GPU的结构对比图。其中:
  • 黄色的Control为控制器,用于协调控制整个CPU的运行,包括取出指令、控制其他模块的运行等;
  • 绿色的ALU(Arithmetic Logic Unit)是算术逻辑单元,用于进行数学、逻辑运算;
  • 橙色的Cache和DRAM分别为缓存和RAM,用于存储信息。

  • 从结构图可以看出,CPU的控制器较为复杂,而ALU数量较少。因此CPU擅长各种复杂的逻辑运算,但不擅长数学尤其是浮点运算。
    • 以8086为例,一百多条汇编指令大部分都是逻辑指令,数学计算相关的主要是16位加减乘除和移位运算。一次整型和逻辑运算一般需要1~3个机器周期,而浮点运算要转换成整数计算,一次运算可能消耗上百个机器周期。
    • 更简单的CPU甚至只有加法指令,减法用补码加法实现,乘法用累加实现,除法用减法循环实现。
    • 现代CPU一般都带有硬件浮点运算器(FPU),但主要适用于数据量不大的情况。
  • CPU是串行结构。以计算100个数字为例,对于CPU的一个核,每次只能计算两个数的和,结果逐步累加。
  • 和CPU不同的是,GPU就是为实现大量数学运算设计的。从结构图中可以看到,GPU的控制器比较简单,但包含了大量ALU。GPU中的ALU使用了并行设计,且具有较多浮点运算单元。
  • 硬件加速的主要原理,就是通过底层软件代码,将CPU不擅长的图形计算转换成GPU专用指令,由GPU完成。
扩展:很多计算机中的GPU有自己独立的显存;没有独立显存则使用共享内存的形式,从内存中划分一块区域作为显存。显存可以保存GPU指令等信息。

并行结构举例:级联加法器

为了方便理解,这里先从底层电路结构的角度举一个例子。如下图为一个加法器,对应实际的数字电路结构。
  • A、B为输入,C为输出,且A、B、C均为总线,以32位CPU为例,则每根总线实际由32根导线组成,每根导线用不同的电压表示一个二进制的0或1。
  • Clock为时钟信号线,每个固定的时钟周期可向其输入一个特定的电压信号,每当一个时钟信号到来时,A和B的和就会输出到C。

现在我们要计算8个整数的和。
对于CPU这种串行结构,代码编写很简单,用for循环把所有数字逐个相加即可。串行结构只有一个加法器,需要7次求和运算;每次计算完部分和,还要将其再转移到加法器的输入端,做下一次计算。整个过程至少要消耗十几个机器周期。
而对于并行结构,一种常见的设计是级联加法器,如下图,其中所有的clock连在一起。当需要相加的8个数据在输入端A1~B4准备好后,经过三个时钟周期,求和操作就完成了。如果数据量更大、级联的层级更大,则并行结构的优势更明显。
由于电路的限制,不容易通过提高时钟频率、减小时钟周期的方式提高运算速度。并行结构通过增加电路规模、并行处理,来实现更快的运算。但并行结构不容易实现复杂逻辑,因为同时考虑多个支路的输出结果,并协调同步处理的过程很复杂(有点像多线程编程)。

GPU并行计算举例

假设我们有如下图像处理任务,给每个像素值加1。GPU并行计算的方式简单粗暴,在资源允许的情况下,可以为每个像素开一个GPU线程,由其进行加1操作。数学运算量越大,这种并行方式性能优势越明显。

Android中的硬件加速

在Android中,大多数应用的界面都是利用常规的View来构建的(除了游戏、视频、图像等应用可能直接使用OpenGL ES)。下面根据Android 6.0原生系统的Java层代码,对View的软件和硬件加速渲染做一些分析和对比。

DisplayList

DisplayList是一个基本绘制元素,包含元素原始属性(位置、尺寸、角度、透明度等),对应Canvas的drawXxx()方法(如下图)。
信息传递流程:Canvas(Java API) —> OpenGL(C/C++ Lib) —> 驱动程序 —> GPU。
在Android 4.1及以上版本,DisplayList支持属性,如果View的一些属性发生变化(比如Scale、Alpha、Translate),只需把属性更新给GPU,不需要生成新的DisplayList。

RenderNode

一个RenderNode包含若干个DisplayList,通常一个RenderNode对应一个View,包含View自身及其子View的所有DisplayList。

Android绘制流程(Android 6.0)

下面是安卓View完整的绘制流程图,主要通过阅读源码和调试得出,虚线箭头表示递归调用。
  • ViewRootImpl.performTraversalsPhoneWindow.DecroView.drawChild是每次遍历View树的固定流程,首先根据标志位判断是否需要重新布局并执行布局;然后进行Canvas的创建等操作开始绘制。
    • 如果硬件加速不支持或者被关闭,则使用软件绘制,生成的Canvas即Canvas.class的对象;
    • 如果支持硬件加速,则生成的是DisplayListCanvas.class的对象;
    • 两者的isHardwareAccelerated()方法返回的值分别为false、true,View根据这个值判断是否使用硬件加速。
  • View中的draw(canvas,parent,drawingTime) - draw(canvas) - onDraw - dispachDraw - drawChild这条递归路径(下文简称Draw路径),调用了Canvas.drawXxx()方法,在软件渲染时用于实际绘制;在硬件加速时,用于构建DisplayList。
  • View中的updateDisplayListIfDirty - dispatchGetDisplayList - recreateChildDisplayList这条递归路径(下文简称DisplayList路径),仅在硬件加速时会经过,用于在遍历View树绘制的过程中更新DisplayList属性,并快速跳过不需要重建DisplayList的View。
    Android 6.0中,和DisplayList相关的API目前仍被标记为“@hide”不可访问,表示还不成熟,后续版本可能开放。
  • 硬件加速情况下,draw流程执行结束后DisplayList构建完成,然后通过ThreadedRenderer.nSyncAndDrawFrame()利用GPU绘制DisplayList到屏幕上。

纯软件绘制 VS 硬件加速(Android 6.0)

下面根据具体的几种场景,具体分析一下硬件加速前后的流程与加速效果。
渲染场景纯软件绘制硬件加速加速效果分析页面初始化绘制所有View创建所有DisplayListGPU分担了复杂计算任务在一个复杂页面调用背景透明TextView的setText(),且调用后其尺寸位置不变重绘脏区所有ViewTextView及每一级父View重建DisplayList重叠的兄弟节点不需CPU重绘,GPU会自行处理TextView逐帧播放Alpha / Translation / Scale动画每帧都要重绘脏区所有View除第一帧同场景2,之后每帧只更新TextView对应RenderNode的属性刷新一帧性能极大提高,动画流畅度提高修改TextView透明度重绘脏区所有View直接调用RenderNode.setAlpha()更新加速前需全页面遍历,并重绘很多View;加速后只触发DecorView.updateDisplayListIfDirty,不再往下遍历,CPU执行时间可忽略不计
  • 场景1中,无论是否加速,遍历View树并都会走Draw路径。硬件加速后Draw路径不做实际绘制工作,只是构建DisplayList,复杂的绘制计算任务被GPU分担,已经有了较大的加速效果。
  • 场景2中,TextView设置前后尺寸位置不变,不会触发重新Layout。
    • 软件绘制时,TextView所在区域即为脏区。由于TextView有透明区域,遍历View树的过程中,和脏区重叠的多数View都要重绘,包括与之重叠的兄弟节点和他们的父节点(详见后面的介绍),不需要绘制的View在draw(canvas,parent,drawingTime)方法中判断直接返回。
    • 硬件加速后,也需要遍历View树,但只有TextView及其每一层父节点需要重建DisplayList,走的是Draw路径,其他View直接走了DisplayList路径,剩下的工作都交给GPU处理。页面越复杂,两者性能差距越明显。
  • 场景3中,软件绘制每一帧都要做大量绘制工作,很容易导致动画卡顿。硬件加速后,动画过程直接走DisplayList路径更新DisplayList的属性,动画流畅度能得到极大提高。
  • 场景4中,两者的性能差距更明显。简单修改透明度,软件绘制仍然要做很多工作;硬件加速后一般直接更新RenderNode的属性,不需要触发invalidate,也不会遍历View树(除了少数View可能要对Alpha做特殊响应并在onSetAlpha()返回true,代码如下)。
public class View {
    // ...
    public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {
        ensureTransformationInfo();
        if (mTransformationInfo.mAlpha != alpha) {
            mTransformationInfo.mAlpha = alpha;
            if (onSetAlpha((int) (alpha * 255))) {
                // ...
                invalidate(true);
            } else {
                // ...
                mRenderNode.setAlpha(getFinalAlpha());
                // ...
            }
        }
    }

    protected boolean onSetAlpha(int alpha) {
        return false;
    }
    // ...
}

在Android的绘制架构中,CPU主要负责了视图的测量、布局、记录、把内容计算成Polygons多边形或者Texture纹理,而GPU主要负责把Polygons或者Textture进行Rasterization栅格化,这样才能在屏幕上成像。在使用硬件加速后,GPU会分担CPU的计算任务,而CPU会专注处理逻辑,这样减轻CPU的负担,使得整个系统效率更高。


RefreshRate刷新频率和FrameRate帧率


RefreshRate刷新频率是屏幕每秒刷新的次数,是一个与硬件有关的固定值,在Android平台上,这个值一般为60HZ,即屏幕每秒刷新60次,Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染
FrameRate帧率是每秒绘制的帧数,由绘制的界面的复杂度决定。通常只要帧率和刷新频率保持一致即每帧的绘制都在16Ms内完成,就能够看到流畅的画面。

在Android平台,我们应该尽量维持60FPS的帧率。但有时候由于视图的复杂导致绘制时间需要比较长,它们可能就会出现不一致的情况。


如图,当绘制比较费时每秒绘制的帧数达不到60,即帧率小于刷新频率时,比如图中的30FPS < 60HZ,就会出现相邻两帧看到的是同一个画面,这就造成了卡顿。这就是为什么我们总会说,要尽量保证一帧画面能够在16ms内绘制完成,就是为了和屏幕的刷新率保持同步。
Android绘制模式
在硬件加速出现之前,原来的图像处理,渲染工作是由软件实现的。
当我们启用硬件加速的时候,android Framework会使用一个新的绘制模式,这个模式会使用display list来渲染画面。为了搞清楚display list以及它是怎么运行的,在学习新的绘制模式之前,我们应该学习一下在硬件加速出现之前,android使用的基于软件的绘制模式。
基于软件的绘制模式
在软件绘制模式,view是按照下面两个步骤进行绘制的:
1、无效化View层次结构
2、绘制View的层次结构
当应用需要更新它的一部分UI,它会调用view的invalidate方法,无效化消息就会通过各种途径传递到View的层次结构,然后计算屏幕中需要重绘的区域(脏区域),android系统还会对View层次结构中和脏区域重叠的所有view进行绘制,对于这种绘制模式,有两个不好的地方:

第一、这个模式在每一次绘制都需要执行大量的代码,比如,如果你的应用对一个button调用invalidate,而这个button坐标在其他view的上方,那么android系统就会重绘这些view,即使他们没有发生改变,仅仅因为它们处于和button重叠的区域

第二、绘制模式会有一些隐藏bug,当android系统重绘那些重叠的views的时候,即使你没有调用invalidate,重叠区域内的一个被改变过的view可能也会进行重绘,这个时候,你通过其他的view的绘制来给这个view进行重绘,当你不经意间修改你的代码,这个时候可能你已经忘了这一段代码有这个隐藏的bug,修改代码后你发现显示的效果有问题,本该进行重绘的区域没有重绘,你就会怀疑是自己的代码逻辑出现问题了,所以应该尽可能在修改view的数据或状态的时候,对每一个你修改过的自定义View,主动调用他们的invalidate方法。
实际阅读源码并实验,得出通常情况下的软件绘制刷新逻辑:
  • 默认情况下,View的clipChildren属性为true,即每个View绘制区域不能超出其父View的范围。如果设置一个页面根布局的clipChildren属性为false,则子View可以超出父View的绘制区域。
  • 当一个View触发invalidate,且没有播放动画、没有触发layout的情况下:
    • 对于全不透明的View,其自身会设置标志位PFLAG_DIRTY,其父View会设置标志位PFLAG_DIRTY_OPAQUE。在draw(canvas)方法中,只有这个View自身重绘。
    • 对于可能有透明区域的View,其自身和父View都会设置标志位PFLAG_DIRTY
      • clipChildren为true时,脏区会被转换成ViewRoot中的Rect,刷新时层层向下判断,当View与脏区有重叠则重绘。如果一个View超出父View范围且与脏区重叠,但其父View不与脏区重叠,这个子View不会重绘。
      • clipChildren为false时,ViewGroup.invalidateChildInParent()中会把脏区扩大到自身整个区域,于是与这个区域重叠的所有View都会重绘。

PS:当view的属性发生改变的时候,例如TextView上的background color或者text,这个时候android view会自动调用invalidate,进行重绘



硬件加速绘制模式

在Honeycomb版本中引入了硬件加速(Hardware Accleration)后,我们的应用在绘制的时候就有了全新的绘制模型。它引入了DisplayList结构,用来记录View的绘制命令,以便更快的进行渲染。android系统依然使用invalidate和draw函数来请求屏幕刷新渲染界面,但实际上绘制的时候是有区别的,不同于立即执行绘制命令,android系统会先把它们记录在display list上,这个display lists包含view的层次结构的绘制代码。其他的优化是android系统只需要记录和更新display lists,通过调用invalidate函数来标记那些脏view,那些没有被标记为invalidate的view可以通过事先记录在display list上的记录简单的进行重绘 。新的绘制模式包含三个步骤:
1、无效化View的层次结构
2、记录和更新显示列表
3、绘制显示列表
使用这个模式,你不能再依赖脏区域内重叠的view来绘制其他的view,为了确保android系统记录这个view变化的display list,你必须调用invalidate,不调用的话,就会导致一个view看起来跟之前是一样的,即使你已经改变它了。所以这就一次性解决了两个问题,对于View层次结构中不想重绘的View,只要不调用那个View的invalidate即可。

使用display list对动画效果也有好处,因为设置指定的属性,例如alpha或者rotation,不需要调用目标view的invalidate,因为它会自动完成,这个优化也应用到view的display list。例如,假设有一个LinearLayout作为父容器,上面是一个ListView ,下面是一个Button,那么对于LinearLayout的display list就会像这样的:
DrawDisplayList(ListView)
DrawDisplayList(Button)
假设现在你通过调用setAlpha(0.5)来修改ListView的透明度,那么display list就变成这样了:
SaveLayerAlpha(0.5)
DrawDisplayList(ListView)
Restore
DrawDisplayList(Button)
关于ListView的复杂的绘制代码并没有被执行,系统只是更新了LinearLayout的display list,如果应用没有启用硬件加速,那么listview以及它的父容器LinearLayout的绘制代码都会再次执行。

还有一些很好的功能开发者们往往会忽略或者使用不当——View layers。使用View layers(硬件层),我们可以将view渲染入一个非屏幕区域缓冲区(off-screen buffer),并且根据我们的需求来操控它。这个功能主要是针对动画,因为它能让复杂的动画效果更加的流畅。而不使用硬件层的话,View会在动画属性(例如coordinate, scale, alpha值等)改变之后进行一次刷新。而对于相对复杂的view,这一次刷新又会连带它所有的子view进行刷新,并各自重新绘制,相当的耗费性能。使用View layers,通过调用硬件层,GPU直接为我们的view创建一个结构,并且不会造成view的刷新。而我们可以在避免刷新的情况下对这个结构进行进行很多种的操作,例如x/y位置变换,旋转,透明度等等。总之,这意味着我们可以让一个复杂view执行动画的同时,又不会刷新!这会让动画看起来更加的流畅。看下面这段代码的操作:
// Using the Object animator
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 20f);
objectAnimator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        view.setLayerType(View.LAYER_TYPE_NONE, null);
    }
});
objectAnimator.start();
 
// Using the Property animator
view.animate().translationX(20f).withLayer().start();

是的,但是在使用硬件layers的时候还是有几点要牢记在心:
回收 – 硬件层会占用GPU中的一块内存。只在必要的时候使用他们,比如动画,并且事后注意回收。例如在上面ObjectAnimator的例子中,我们增加了一个动画结束监听以便在动画结束后可以移除硬件层。而在Property animator的例子中,我们使用了withLayers(),这会在动画开始时候自动创建硬件层并且在结束的时候自动移除。
如果你在调用了硬件View layers后改变了View,那么会造成硬件层的刷新并且再次重头渲染一遍view到非屏幕区域缓存中。这种情况通常发生在我们使用了硬件层暂时还不支持的属性(目前为止,硬件层只针对以下几种属性做了优化:otation、scale、x/y、translation、pivot和alpha)。例如,如果一个view执行动画并且使用硬件层,在屏幕滑动他们的同时改变他的背景颜色,这就会造成硬件层的持续刷新。而硬件层的持续刷新所造成的性能消耗来说,可能让它在这里的使用变得并不那么值。

Android Studio 1.4中的一个全新工具,可以查看GPU绘制。

每一条线意味着一帧被绘制出来,而每条线中的不同颜色又代表着在绘制过程中的不同阶段:
Draw (蓝色) 代表着View#onDraw()方法。在这个环节会创建/刷新DisplayList中的对象,这些对象在后面会被转换成GPU可以明白的OpenGL命令。而这个值比较高可能是因为view比较复杂,需要更多的时间去创建他们的display list,或者是因为有太多的view在很短的时间内被创建。
Prepare (紫色) – 在Lollipop版本中,一个新的线程被加入到了UI线程中来帮助UI的绘制。这个线程叫作RenderThread。它负责转换display list到OpenGL命令并且送至GPU。在这过程中,UI线程可以继续开始处理后面的帧。而在UI线程将所有资源传递给RenderThread过程中所消耗的时间,就是紫色阶段所消耗的时间。如果在这过程中有很多的资源都要进行传递,display list会变得过多过于沉重,从而导致在这一阶段过长的耗时。
Process (红色) – 执行Display list中的内容并创建OpenGL命令。如果有过多或者过于复杂的display list需要执行的话,那么这阶段会消耗较长的时间,因为这样的话会有很多的view被重绘。而重绘往往发生在界面的刷新或是被移动出了被覆盖的区域。
Execute (黄色) – 发送OpenGL命令到GPU。这个阶段是一个阻塞调用,因为CPU在这里只会发送一个含有一些OpenGL命令的缓冲区给GPU,并且等待GPU返回空的缓冲区以便再次传递下一帧的OpenGL命令。而这些缓冲区的总量是一定的,如果GPU太过于繁忙,那么CPU则会去等待下一个空缓冲区。所以,如果我们看到这一阶段耗时比较长,那可能是因为GPU过于繁忙的绘制UI,而造成这个的原因则可能是在短时间内绘制了过于复杂的view。
过度绘制,在Execute黄色阶段,如果GPU有过多的东西要在屏幕上绘制,整个阶段会消耗更多的时间,同时也增加了每一帧所消耗的时间。过度绘制往往发生在我们需要在一个东西上面绘制另外一个东西,例如在一个红色的背景上画一个黄色的按钮。那么GPU就需要先画出红色背景,再在他上面绘制黄色按钮,此时过度绘制就是不可避免的了。如果我们有太多层需要绘制,那么则会过度的占用GPU导致我们每帧消耗的时间较大。
透明度,使用透明度可能会影响性能,让我们瞅瞅当我们给view设置透明度的时候到底发生了什么。我们来看一下下面这个布局:
我们看到这个layout中有三个ImageView并且重叠摆放。在使用最常规的设置透明度的方法setAlpha()时,方法会传递到每一个ImageView。而后,这些ImageView会携带新的透明值被绘制入帧缓冲区。而结果就是:
这并不是我们想要看到的结果。因为每一个ImageView都被赋予了一个透明值,导致了本应覆盖的部分被融合在一起。幸运的是,系统为我们解决了这个问题。布局会被复制到一个非屏幕区域缓冲区中,并且以一个整体来接收透明度,其结果再被复制到帧缓冲区。结果就是:
但是,我们是要付出性能上面的代价的。假如在帧缓冲区内绘制之前,还要在off-screen缓冲区中绘制一遍的话,相当于增加了一层不可见的绘制层。而系统并不知道我们是希望这个透明度以何种的形式去展现,所以系统通常会采用相对复杂的一种。但是也有很多设置透明度的方法能够避免在off-screen缓冲区中的复杂操作:
TextView – 使用setTextColor()方法替代setAlpha()。这种方法使用Alpha通道来改变字色,字也会直接使用它进行绘制。
ImageView – 使用setImageAlpha()方法替代setAlpha()。原理同上。
自定义控件 – 如果你的自定义控件并不支持相互覆盖,那就无所谓了。所有的子view并不会像上面的例子中一样,因为覆盖而相互融合。而通过复写hasOverlappingRendering()并将其返回false后,便会通知系统使用最直接的方式绘制view。同时我们也可以通过复写onSetAlpha()返回true来手动操控设置透明度后的逻辑。

上面提到的RenderThread对动画显示有两个优化:
第一个优化是在动画显示期间,临时将动画的目标View的Layer Type设置为LAYER_TYPE_HARDWARE,这样就可以使得目标View以Open GL里面的Frame Buffer Object(FBO)进行渲染。这种优化的效果就如同RenderThread直接以Open GL里面的Texture来渲染TextureView一样。
第二个优化是在MainThread中不需要参与动画的显示过程,动画会被注册到RenderThread中,这样动画的计算和显示过程就完全由RenderThread来负责。这种优化带来的好处就是在动画显示期间,MainThread可以去处理其它的用户输入,而且动画的显示也会更加流畅。


最后是一些API提到的小tips和技巧

切换到硬件加速,界面固然是更加流畅了,但是我们开发应用的时候要想让GPU的效率更加的高,应该注意以下几点:
减少应用中view的数量
要绘制的view越多,那么必然就越慢,使用软件渲染管道也是一样的,减少view的数量是优化UI的最简单的途径。
避免透支
不要在顶部画太多互相混合的图层,移除那些完全被其他不透明View覆盖的View,如果你需要在顶部画几个互相混合的图层,考虑把他们放到一个单一的图层里,一个很好的经验法则与当前硬件是不绘制超过2.5倍的每帧屏幕上的像素数(在一个位图的像素的透明像素!),简而言之就是不要嵌套太多层。
不要老是创建Paint和Path对象
一个普遍的错误是,每次调用draw方法的时候总是new一个Paint对象,或者new一个Path对象,这样就强迫垃圾回收器频繁地运行,而且也失去了硬件管道中的缓存和优化。
不要频繁修改外形
比如复杂的外形,路径和圆,它们都是使用texture mask进行渲染的,每次修改路径,硬件管道就创建一个新的mask,这样开销是很大的。
不要频繁修改bitmap
每一次修改一个bitmap中的内容,当你下次绘制它的时候,它就会再次上传到GPU中的texture。
小心使用alpha
当你使用setAlpha,或者AlphaAnimation,或者ObjectAnimator来改变一个View的透明度时,它渲染在离屏缓存中需要两倍填充率,当需要在在一个非常大的view上修改alpha,就要考虑设置view的layer type为LAYER_TYPE_HARDWARE

总结


至此,硬件加速相关的内容就介绍完了,这里做个简单总结:

1.硬件加速是从API 11引入,API 14之后才默认开启。对于标准的绘制操作和控件都是支持的,但是对于自定义View的时候或者一些特殊的绘制函数就需要考虑是否需要关闭硬件加速。

2.我们面对不支持硬件加速的情况,就需要限制硬件加速,这个兼容性的问题是因为硬件加速是把View的绘制函数转化为使用OpenGL的函数来进完成实际的绘制的,那么必然会存在OpenGL中不支持原始回执函数的情况,对于这些绘制函数,就会失效。

3.硬件加速的消耗问题,因为是使用OpenGL,需要把系统中OpenGL加载到内存中,OpenGL API调用就会占用8MB,而实际上会占用更多内存,并且使用了硬件必然增加耗电量了。

4.硬件加速的优势还有display list的设计,使用这个我们不需要每次重绘都执行大量的代码,基于软件的绘制模式会重绘脏区域内的所有控件,而display只会更新列表,然后绘制列表内的控件。

5. CPU更擅长复杂逻辑控制,而GPU得益于大量ALU和并行结构设计,更擅长数学运算。

6. 页面由各种基础元素(DisplayList)构成,渲染时需要进行大量浮点运算。

7. 硬件加速条件下,CPU用于控制复杂绘制逻辑、构建或更新DisplayList;GPU用于完成图形计算、渲染DisplayList。

8. 硬件加速条件下,刷新界面尤其是播放动画时,CPU只重建或更新必要的DisplayList,进一步提高渲染效率。

9. 实现同样效果,应尽量使用更简单的DisplayList,从而达到更好的性能(Shape代替Bitmap等)。


原创粉丝点击