参考
https://developer.android.com/guide/topics/graphics/hardware-accel.html
硬件加速背景知识
在手机客户端尤其是Android应用的开发过程中,我们经常会接触到"硬件加速"这个词。由于操作系统对底层软硬件封装非常完善,上层软件开发者往往对硬件加速的底层原理了解很少,也不清楚了解底层原理的意义,因此常会有一些误解,如硬件加速是不是通过特殊算法实现页面渲染加速,或是通过硬件提高CPU/GPU运算速率实现渲染加速。
本文尝试从底层硬件原理,一直到上层代码实现,对硬件加速技术进行简单介绍,其中上层实现基于Android 6.0。
了解硬件加速对App开发的意义
对于App开发者,简单了解硬件加速原理及上层API实现,开发时就可以充分利用硬件加速提高页面的性能。以Android举例,实现一个圆角矩形按钮通常有两种方案:使用PNG图片;使用代码(XML/Java)实现。简单对比两种方案如下。
页面渲染背景知识
- 页面渲染时,被绘制的元素最终要转换成矩阵像素点(即多维数组形式,类似安卓中的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 3.0 (API level 11), 开始支持
所有的View 的canvas都会使用GPU,但是硬件的加速会占有一定的RAM。
在API >= 14上,默认是开启的,如果你的应用只是标准的View和Drawable,全局都打开硬件加速,是不会有任何问题的。
然而,硬件加速并不支持所有的2D画图的操作,这时开着它,可能会影响到你的自定义控件或者绘画,出现异常等行为,
所以android对于硬件加速提供了可选性
如果你的应用执行了自定义的绘画,可以通过在真机上测试开启硬件加速查找问题
控制硬件加速
你可以在不同level控制hardware acceleration:
- Application
- Activity
- Window
- View
Application
<application android:hardwareAccelerated="false" ...> </application>
Activity
<application android:hardwareAccelerated="true"> <activity ... /> <activity android:hardwareAccelerated="false" /> </application>
Window
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
Note:
- 设置此标志必须在setContentView之前设置。
- 如果在manifest中开启了硬件加速,那么不能使用此标志禁用硬件加速。
- 一般是 在manifest中activity/application明确指定关闭硬件加速情况下,使用这个flag开启该window的硬件加速。
- 如果在manifest中activity/application上将android:hardwareAccelerated 属性设置为true,则系统将自动设置此标志。
View
在代码中:
myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
或者在布局中android:layerType="software":
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" android:paddingLeft="2dp" android:layerType="software" android:paddingRight="2dp" >
Note: 并不是关闭硬件加速,而是强制使用软件加速,如果硬件加速是关闭的,那么就算设置了LAYER_TYPE_HARDWARE也使用软件加速。
两种获取是否支持硬件加速的方式
- View.isHardwareAccelerated() //returns true if the View is attached to a hardware accelerated window.
- Canvas.isHardwareAccelerated() //returns true if the Canvas is hardware accelerated
如果必须进行这样的验证,建议你在draw的代码块中使用:Canvas.isHardwareAccelerated(),
因为如果当一个View被attach到一个硬件加速的Window上,仍然能使用非硬件加速的canvas来绘制。
比如:将一个View以bitmap的形式进行缓存
Android Drawing Models
当硬件加速被启用,Android framework就会使用一个新的绘制模型,这个新的绘制模型使用display lists来渲染你的app,显示到屏幕上。
要理解硬件加速display lists,及它是如何影响你的app的,就需要先了解一下软件加速时Android是怎么来绘制view的。
Software-based drawing model
在软件绘制模型中,view的绘制有两个步骤:
- Invalidate the hierarchy
- Draw the hierarchy
当一个app要去更新它的一部分ui时,app中的那些内容改变的view就会调用 invalidate()或类似的方法。Invalidate的消息按照View的层级关系向上传递用以计算需要重画的部分(即脏区域),然后Android系统会对和脏区域有交集的所有View进行绘制。(或者说有绘图缓存时,此view和所有父view)
不幸的是这种模型中有两个缺点:
- 第一,在这种模型中当在不同的层进行画的时候,会额外执行很多代码。例如一个Button是和另外一个View有重叠时,当对Button调用 Invalidate()时,Android系统也会对另外一个View进行重绘,即便这个View没有发生任何变化。
- 第二,这种绘制模型会隐藏你app中的Bug。因为Android系统会对和脏区域有交集的所有View进行重绘。在这种情况下如果一个view的内容发生了改变,即便这个View的Invalidate()的方法并没有得到调用,它也可能被重绘,所以原view就需要依赖其他有交集的view能够正常显示。因此每当你的Application发生改变时,这种行为多要随之发生改变。也因如此,当你的view的数据或者是状态影响了View的绘制代码时,在你的自定义控件中你必须不断地调用invalidate()方法。
Note: Android views automatically call invalidate() when their properties change, such as the background color or the text in a TextView.
Hardware accelerated drawing model
Android系统硬件加速仍然使用invalidate()和draw()去请求屏幕更新和渲染view,但是实际过程是不同的。
当invalidate()时并不会立即执行绘制动作,而是Android系统会记录绘制动作到一个display lists里边,这个display lists包含了View的hierarchy的绘制代码的输出。
另外一个优化就是,Android系统只需要记录和更新 调用invalidate()的view中被标记为脏区域。没有被请求invalidated 的view可以通过重新发布(re-issuing)先前的记录的display list以便重画。
新的绘制模型包含三步:
- Invalidate the hierarchy
- Record and update display lists
- Draw the display lists
使用这个模型,你不能依赖一个View和脏区域有交集就会执行draw()方法。要保证Android系统记录了一个View的display list,你必须调用invalidate()方法,如果忘记了调用,会使View即便是发生了改变后也会看起来相同,这是一个比较容易发现bug的方式。
使用display lists的方式对动画的表现也是很有好处的,因为设置指定的属性值就不需要请求刷新目标View(这将自动执行),比如透明度或者旋转。
这项优化也应用于有display lists的Views(启用了硬件加速的View),
例如,现在有一个LinearLayout包含了一个ListView和Button,listview在button的上面。这时候LinearLayout的display lists如下所示:
- DrawDisplayList(ListView)
- DrawDisplayList(Button)
假设现在,你想要改变ListView的透明度,在调用ListView的setAlpha(0.5f) 之后,LinearLayout的display lists如下所示:
- SaveLayerAlpha(0.5)
- DrawDisplayList(ListView)
- Restore
- DrawDisplayList(Button)
这时候绘制Listview的复杂绘制过程就不会被执行,取而代之的是简单的更新了LinearLayout的display list。
而如果没有启用硬件加速,Listview和它的父view的绘制代码都会重新执行。
硬件加速时2D图形绘制的局限
开启GPU硬件加速会提升程序的绘图效率,但是也存在一定的局限性。
1.启用GPU硬件加速会增加内存的使用。
2.Android中有些2D绘图API在GPU硬件加速时不能使用或者要到某个指定的版本才能使用。
第一列是受限制的方法,第二列是开始支持的API Level,红叉代表到目前还不支持。
Canvas
以下为Canvas中在GPU硬件加速时受限制的功能:
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
以下为Paint中在GPU硬件加速时受限制的功能:
setAntiAlias() (for text)
18
setAntiAlias() (for lines)
16
setFilterBitmap()
17
setLinearText()
?
setMaskFilter()
?
setPathEffect() (for lines)
28
setShadowLayer() (other than text)
28
setStrokeCap() (for lines)
18
setStrokeCap() (for points)
19
setSubpixelText()
28
Xfermode
以下为Xfermode在GPU硬件加速时受限制的功能:
PorterDuff.Mode.DARKEN (framebuffer)
28
PorterDuff.Mode.LIGHTEN (framebuffer)
28
PorterDuff.Mode.OVERLAY (framebuffer)
28
Shader
以下为Shader在GPU硬件加速时受限制的功能:
ComposeShader inside ComposeShader
28
Same type shaders inside ComposeShader
28
Local matrix on ComposeShader
18
硬件加速对Canvas缩放的影响
硬件加速对Canvas缩放导致绘图质量会明显降低 ,
Android中硬件加速的2D渲染管线最初只支持无缩放的绘图,这会导致在将缩放比例设置为较大的时候,绘图质量会明显降低。最初,GPU加速下的2D绘图操作会被渲染成一个缩放比例为1.0的纹理,然后GPU会将它缩放到指定比例尺。在API Level小于17的时候,随着缩放比例scale的变大,绘图质量就更加难以保证。从API级别28开始,所有绘图操作都可以无问题地缩放。
下面的表格表示了从什么版本开始Android能在GPU硬件计算下正确处理2D图形的大比例缩放问题:
Drawing operation to be scaled
First supported API level
drawText()
18
drawPosText()
28
drawTextOnPath()
28
Simple Shapes*
17
Complex Shapes*
28
drawPath()
28
Shadow layer
28
注意: ‘Simple‘ shapes是drawRect(),drawCircle(),drawOval(),drawRoundRect()和drawArc()(使用useCenter = false)命令,使用没有PathEffect的Paint发出,并且没有 包含非默认joins (通过setStrokeJoin()/ setStrokeMiter())。 这些绘图命令的其他实例属于上图中的"‘Complex"。
总结
现在我们开发的App一般将targetSdkVersion写为最新版本,并且市场上的手机绝大部分都是Android 4.0以上的,所以我们现在开发的App默认情况下在绝大部分手机上基本都是默认开启了GPU硬件加速的。如果我们自己要自定义一个View,我们要重写其onDraw方法,通过调用各种绘图方法实现复杂的效果,但是如果我们调用的API在GPU硬件加速下不支持的话,就画不出我们想要的效果,举个例子,比如我们想在自定义View中绘制一个具有模糊效果的椭圆,需要调用画笔Paint的setMaskFilter()方法,但是我们通过上面的受限API列表可以发现,在GPU硬件加速下,Pait的setMaskFilter()方法不被支持,虽然调用不报错,但是不会起到任何效果。为了画出我们想要的效果,我们可以通过View的setLayerType(View.LAYER_TYPE_SOFTWARE, null)方法单独把我们的View禁用掉GPU硬件加速,这样在软件渲染模式下所有的2D绘图API都可以正常使用了。
最后有点需要说明,上述Android在GPU硬件加速下2D图形绘制API存在的局限问题是基于当前最新API Level 28的,随着以后更新Android版本的发布,可能上述受限API会逐渐在GPU下得到更好的支持。
View Layers
简介
在Android的所有版本中,View都能够通过使用View的drawing cache或使用Canvas.saveLayer()渲染到屏幕外缓冲区(off-screen buffers)。 屏幕外缓冲区或layers具有多种用途。 在呈现view复杂的动画或应用合成效果时,可以使用它们来获得更好的性能。 例如,您可以使用Canvas.saveLayer()实现淡入淡出效果,以暂时将view 渲染到一个layer 中,然后使用不透明度因子将其合成到屏幕上。
从Android 3.0(API level 11)开始,你就能够通过View.setLayerType(int layerType, Paint paint)方法对何时以及如何使用layers 有了更多的控制,这个API具有两个参数:一个是你想使用的layerType,另外一个是可选参数Paint(表明了Layer是如何被合成到一起的)。你可以把Paint参数应用color filters,或特殊的混合模式 或者是对一个layer进行不透明效果。
一个View可以使用如下的三种layer类型之一:
- LAYER_TYPE_NONE: The view is rendered normally and is not backed by an off-screen buffer. This is the default behavior. 没有缓存
- LAYER_TYPE_HARDWARE: The view is rendered in hardware into a hardware texture if the application is hardware accelerated. If the application is not hardware accelerated, this layer type behaves the same as LAYER_TYPE_SOFTWARE.
- LAYER_TYPE_SOFTWARE: The view is rendered in software into a bitmap.
使用何种类型的layer全靠你的目的:
- 性能: Use a hardware layer type to render a view into a hardware texture. Once a view is rendered into a layer, its drawing code does not have to be executed until the view calls invalidate(). Some animations, such as alpha animations, can then be applied directly onto the layer, which is very efficient for the GPU to do.
- 可视效果: 使用硬件或者软件层和Paint来对一个View进行特殊的视觉处理,例如你可以对一个View通过使用ColorMatrixColorFilter来实现黑白效果。
- 兼容性: Use a software layer type to force a view to be rendered in software. If a view that is hardware accelerated (for instance, if your whole application is hardware acclerated), is having rendering problems, this is an easy way to work around limitations of the hardware rendering pipeline.
View layers and animations
当你的应用程序已经使用了硬件加速的话,硬件层能够带来更为快速和更为平滑的动画效果。当对一个复杂的View进行动画操作时,因为要进行很多的画操作,所以并不可能总是能达到60帧每秒。This can be alleviated(减轻) by using hardware layers to render the view to a hardware texture.。硬件纹理操作可以用作对一个view进行动画操作,当进行动画的时候可以消除对View自身频繁的重绘。view不会重绘,除非你要改变这个view的属性(那些自动调用invalidate()的属性 或者 你手动的调用invalidate())。如果在你的应用中运行一个动画,但是并没有得到你想要的平滑效果,可以考虑为你要动画的view开启硬件层。
在硬件加速的情况下,下列属性的改变不会使得view的displaylist进行重绘,而只用改变displaylist中对应的属性即可,大大提升了渲染效率。
- alpha: Changes the layer‘s opacity
- x, y, translationX, translationY: Changes the layer‘s position
- scaleX, scaleY: Changes the layer‘s size
- rotation, rotationX, rotationY: Changes the layer‘s orientation in 3D space
- pivotX, pivotY: Changes the layer‘s transformations origin
这些属性是通过ObjectAnimator对一个view进行动画操作时所使用的,如果你想访问这些属性,直接调用这些属性的setter或者getter方法,例如想改变View的alpha则直接调用setAlpha()。
如下的代码片段显示了一个View通过Y轴进行3D旋转。
因为硬件层消耗显存,强烈建议只为动画期间开启,动画结束禁用。这一点可通过animation listeners实现:
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();
或者如果是使用 ViewPropertyAnimator,那么更简单:
view.animate() .rotationY(90) .withLayer(); // withLayer() 可以自动完成上面这段代码的复杂操作
技巧与提示
切换到硬件加速的2D图形可以立即提高性能,但仍建议遵循以下推荐规范来设计应用程序以便有效地利用GPU:
Reduce the number of views in your application
The more views the system has to draw, the slower it will be. This applies to the software rendering pipeline as well. Reducing views is one of the easiest ways to optimize your UI.
Avoid overdraw
不要过多的叠加层,当一个View被其他view完全遮挡住了的话,最好把被遮挡的view移除掉。如果你需要绘制多个layer做一个叠加效果的话,考虑把这些层合并为一个层。就现在的硬件来看,有一个好的经验就是每帧不要绘制多余屏幕像素2.5倍的像素数量(bimap中的透明像素也计算在内)。
Don‘t create render objects in draw methods
一个常见的错误是,每一次渲染方法被调用的时候都创建一个新的Paint或一个新的Path。这会迫使gc更加频繁,也会绕过硬件管道的缓存和优化。
Don‘t modify shapes too often
复杂的形状如Path,Circle,都是通过纹理masks来渲染的,每次创建或者修改Path,这个硬件管道会创建一个新的mask,这个代价是十分昂贵的。
Don‘t modify bitmaps too often
Every time you change the content of a bitmap, it is uploaded again as a GPU texture the next time you draw it.
小心使用alpha
当你使用setAlpha ,AlphaAnimation或者ObjectAnimator设置一个View的透明效果时,它将需要双倍填充率的屏幕外缓存,当应用alpha到一个大的View上的时候,考虑设置view 层的类型为LAYER_TYPE_HARDWARE。