硬件加速(Hardware Acceleration)

概述

从Android 3.0开始,Android的2D渲染通道开始支持硬件加速.这代表所有的在View的Canvas上的绘制操作都将使用GPU.因为要启用硬件加速增加了资源,所以APP将会消耗更多的内存.

如果Android API的版本>=14的话,那么默认情况下就会启动硬件加速,也可以明确的启动/不启动.如果我们的APP只使用标准的View和Drawable,全部打开硬件加速不会产生任何不利的效果.但是因为硬件加速并不是支持所有的2D绘制操作,开启硬件加速可能会影响一些自定义的View或者绘制方法.问题通常表现为元素不可见,异常或者错误渲染.为了解决这个问题,Android为我们提供了多种开启/不开启硬件加速的等级.稍后介绍这个功能.如果APP需要处理自定义的绘制,那么就需要在真实的硬件条件下测试硬件加速功能.下面会有章节介绍硬件加速已知的问题,并如何避开它们.

控制硬件加速

我们可以使用下面这些级别来控制硬件加速:Application,Activity,Window,View.下面来依次介绍:
Application:在Manifest文件中添加下列属性到<application>中就可以为整个APP提供了硬件加速:

  • Application

    在Manifest文件中添加下列属性到<application>中就可以为整个APP提供了硬件加速:
<application android:hardwareAccelerated="true" ...>
  • Activity

    如果我们的APP不适合全局都启用硬件加速,那么也可以指定为某个Activity单独启用硬件加速.想要这样做我们需要使用<activity>的android:hardwareAccelerated属性,这个栗子是全局启用但是单个Activity不启用:
<application android:hardwareAccelerated="true">
    <activity .../>
    <activity android:hardwareAccelerated="false" />
</application>
  • Window

    如果我们需要更细粒度的控制,那么我们可以使用window来指定硬件加速:
getWindow().setFlags(
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);

当前不能在window级别禁用硬件加速.

  • View

    我们可以在运行时使用下面的代码禁用某个view的硬件加速:
myView.setLayerType(View.LAYER_TYPE_SOFTWARE,null);

当前我们不能在view级别启用硬件加速.

确定一个View是否使用硬件加速

有些时候,APP需要知道当前是否启用了硬件加速,特别是对那些自定义的View.如果我们做了很多自定义的绘制,并且并不是所有的渲染管道都支持的时候就应该关注一下这点.有两种方法可以让我们知道APP是否启动了硬件加速:

  • 使用View.isHardwareAccelerated()方法,如果返回true则表示View关联了一个硬件加速的window.

  • Canvas.isHardwareAccelerated()方法,如果返回true则表示canvas被硬件加速了.

如果我们必须查看的话,尽量使用Canvas.isHardwareAccelerated()方法代替View.isHardwareAccelerated().当view关联了硬件加速的window的时候,它依然可以使用一个没有硬件加速的Canvas.比如我们绘制view到一个高速缓存的位图中的时候.

Android的绘制模式

当硬件加速可用的时候,Android framework采用了一种新的渲染方式:利用display list来渲染我们的APP到屏幕上.要充分的理解display list和它是如何影响我们的APP的,就得先理解Android是如何在没有硬件加速的情况下绘制view的.下面会介绍软件绘制和硬件加速模式.

基于软件的绘制模式

在软件绘制模式下,绘制一个view需要以下两步:invalidate the hierarchy和draw the hierarchy.不管什么时候只要APP需要刷新它的一部分UI,它就得调用它所在view的invalidate()方法.这个invalidation消息一路沿着view层计算出哪些区域需要重绘(这些需要重绘的区域叫dirtyregion)并传递给Android,然后Android负责重绘它们,但是这种重绘机制有两个缺点:

  • 首先,这种模型需要在每次绘制的时候执行很多代码.栗如:有个button在一个view上,这时候我们的APP调用了这个button的invalidate()方法,这时候计算这个view没有改变,Android还是会重绘它.

  • 其次,这种机制可能会隐藏一些我们APP中的bug.因为当dirty region交叉时,Android将会重绘view,所以有时候有些被更改的View即使没有调用validate()方法也可能会被重绘.当这种事情发生的时候,我们需要为另一个可能被重绘的view保留正确行为.每次修改app的时候这一行为都可以发生改变.因为这样我们在修改影响view绘制的数据或者状态的时候总是应该调用invalidate().

Android在view的属性发生改变的时候,总是会调用invalidate().比如背景色或者TextView中的文本发生了变化.

硬件加速绘制模式

当需要屏幕更新和渲染view的时候Android使用invalidate()和draw()方法,但是处理实际绘制(actual drawing)的时候却又不同.Android不会直接执行绘制命令,而是将它们记录到display list中,display list包含了view层绘制代码的输出.还有一个优化措施是,android只会为invalidate()方法标记的view来记录和更新display list.还没有被重绘的view可以通过简单的重发前一个display list记录来重新绘制.这种新的绘制模式包含三个阶段:

  • Invalidate the hierarchy.

  • Record and update display lists.

  • Draw the display lists.

通过这个模式,我们就不能指望一个与dirty region相交叉的地方去自动执行它的draw()方法了.为了保证Android记录一个view的display list,我们必须调用invalidate()方法.如果忘记这样做会导致一个view在它改变属性之后还是老样子不变.

使用display list也会是动画性能受益,因为设置指定的属性比如透明度,旋转,都不需要重绘目标view(它会自动执行).这个优化也适用于View(任何启用硬件加速的view).比如,假设有个LinearLayout,包含一个在Button上的ListView.LinearLayout的display list看起来是这样:

  • DrawDisplayList(ListView)

  • DrawDisplayList(Button)

假设现在想要修改ListView为不透明.调用setAlpha(0.5f)之后,display list现在是这样的:

  • SaveLayerAlpha(0.5)

  • DrawDisplayList(ListView)

  • Restore

  • DrawDisplayList(Button)

ListView复杂的绘制代码并没有被执行,Android只是升级了LinearLayout的displaylist.在不启用硬件加速的APP中,这些list中的绘制代码会在ListView和它的父容器中执行两次.

不支持的绘制操作

当启用硬件加速的时候,2D渲染管道对很多不常用的操作和常用的Canvas绘制操作支持的一样好.所有的Android附带APP或者默认控件和layout的绘制操作,还有常用高级的视觉效果比如反射和瓷砖纹理都可以被支持.下表描述了不同API等级所支持的操作:

First supported API level
Canvas
drawBitmapMesh() (colors array) 18
drawPicture() 23
drawPosText() 16
drawTextOnPath() 16
drawVertices()
setDrawFilter() 16
clipPath() 18
clipRegion() 18
clipRect(Region.Op.XOR) 18
clipRect(Region.Op.Difference) 18
clipRect(Region.Op.ReverseDifference) 18
clipRect() with rotation/perspective 18
Paint
setAntiAlias() (for text) 18
setAntiAlias() (for lines) 16
setFilterBitmap() 17
setLinearText()
setMaskFilter()
setPathEffect() (for lines)
setRasterizer()
setShadowLayer() (other than text)
setStrokeCap() (for lines) 18
setStrokeCap() (for points) 19
setSubpixelText()
Xfermode
PorterDuff.Mode.DARKEN (framebuffer)
PorterDuff.Mode.LIGHTEN (framebuffer)
PorterDuff.Mode.OVERLAY (framebuffer)
Shader
ComposeShader inside ComposeShader
Same type shaders inside ComposeShader
Local matrix on ComposeShader 18

View层

在所有版本的Android中,view都有能力渲染到屏幕外缓冲区中,不管是通过使用view的drawing缓存,或者是使用Canvas.saveLayer().屏幕外缓冲区(Off-screen buffers),或者层(layers)有多种用途.我们可以使用它们在实现view的复杂动画或者组合效果的时候获得更好的性能.栗如,我们可以通过使用Canvas.saveLayer()方法临时渲染一个view到一个层中,然后用不透明特性将其组合到屏幕上来实现一个褪色的效果.

从Android 3.0开始,我们可以通过View.setLayerType()方法来决定如何/什么时候使用层.这个API接收两个参数:想要使用的层的类型和一个可选的描述了层该被如何组合的Paint对象.我们可以使用Paint参数来应用一个颜色过滤器,指定混合模式,或者让一个层不透明.一个View可以使用下列三种层类型:

  • LAYER_TYPE_NONE

    这个view使用普通的方法渲染,不会由一个屏幕外缓冲区备份(backed).这是默认的行为.

  • LAYER_TYPE_HARDWARE

    如果APP是硬件加速的,那么这个view会被硬件渲染为一个硬件纹理(hardwaretexture).如果APP不是硬件加速的,那么这个层类型的行为跟下面的LAYER_TYPE_SOFTWARE一样.

  • LAYER_TYPE_SOFTWARE

    这个view被软件渲染为一个Bitmap.

我们使用的层的类型由我们的目的决定(更重视哪个):

  • 性能

    这种情况下应该使用一个硬件层类型来渲染一个view为一个硬件纹理.一旦一个view被渲染到了一个layer里,它的绘制代码在调用invalidate()的时候才会被执行.有些动画,比如透明度动画,可以被直接应用于层上,GPU完成这些操作十分的高效.

  • 视觉效果

    这种情况应该使用一个硬件或者软件层,然后通过Paint指定一个特殊的视觉效果.栗如:我们可以使用ColorMatrixColorFilter来绘制一个黑白相间的view.

  • 兼容性

    这种情况下应该使用一个软件层类型来强制view由软件渲染.如果一个view由硬件渲染,那么可能会存在渲染问题,这是一个规避硬件渲染管道限制简单的方法.

View层和动画

当我们的APP使用硬件加速的时候,硬件层可以提供更快和更流畅的动画体验.当动画很复杂的时候,并不能保证总是保持60帧,这种问题可以通过硬件层将view渲染到一个硬件纹理上来解决.这样就可以通过硬件纹理来完成view的动画,而不用view不断的重绘自己.除非view的属性被改变(这时候会调用invalidate()方法)或者手动调用invalidate()方法,否则view不会重绘.如果我们想要运行的APP的动画并没有获得我们理想中的流畅效果,那么就应该考虑在view上使用硬件层.

当view处在一个硬件层中的时候,它的某些属性将会由该层与屏幕合并的方式决定.设置这些属性的效率很高,因为它们不会导致view被重绘.下面的属性将会影响层合并的方式:

  • Alpha

    修改层的透明度.

  • x,y,translationX,translation

    修改层的位置.

  • scaleX,scaleY

    修改层的尺寸.

  • rotation,rotationX,rotationY

    在3D空间中修改层的方向.

  • pivotX,pivotY

    修改层的变换的原点.

当view使用ObjectAnimator实现动画的时候,可以通过名字指定这些属性.如果我们想要访问这些属性,只需要调用相应的setter和getter.栗如:想要修改alpha,调用setAlpha()方法.下面的代码段演示了在3D下绕Y轴旋转的动画:

view.setLayerType(View.LAYER_TYPE_HARDWARE,null);
ObjectAnimator.ofFloat(view,"rotationY",180).start();

因为硬件层需要消耗显存,所以官方强烈建议我们在动画启动的时候使用它,动画结束之后停用它.我们可以通过动画监听器来实现这个操作:

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();

一些技巧

使用硬件加速可以增强2D图片的处理性能,但是我们仍然应该通过下面的小技巧设计APP以高效的利用GPU:

  • 减少APP中的view:

    绘制越多的view就会越慢.这一原则对于软件渲染通道同样适用.减少view数量是最简单的优化UI的方法了.

  • 避免过度绘制:

    不要在层上面叠加太多.应该移除任何被不透明的view盖住的view.如果需要将多个层叠加混合使用,应该考虑将它们放在同一个层里.

  • 不要在绘制方法中创建渲染对象:

    一个常见的的错误是在每次调用渲染的方法的时候都创建一个新的Paint或者一个新的Path.这迫使垃圾回收系统更加频繁的运行,同时也绕过了缓存和硬件管道优化.

  • 不要太频繁的修改外形:

    复杂的形状,路径和圆形都会使用texture标记渲染.每次创建或者修改一个路径,硬件管道都会创建一个新的标记,这会付出不少代价.

  • 不要太频繁的修改Bitmap:

    每次我们修改Bitmap的时候,它都会作为一个GPU texture被重新绘制.

  • 慎用透明度:

    当我们通过setAlpha(),AlphaAnimation或者ObjectAnimator修改view的透明度的时候,它都会被渲染到一个屏幕外缓冲区,它会消耗双倍的填充率(fill-rate).当修改一个大的view的透明度的时候,应该考虑设置view层的类型为LAYER_TYPE_SOFTWARE.

总结

硬件加速就是使用GPU来处理图形操作.

硬件加速可以按级别提供给APP,包括Application,Activity,Window,View.

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1