Android官方英文原文地址:https://developer.android.com/guide/topics/graphics/hardware-accel.html
Android 3.0(API 11)版本开始android 2D 渲染支持硬件加速,意味着所有在view的canvas上面的绘制操作将会使用GPU。因为越来越多的资源需要开启硬件加速,你的app将会消耗更多的RAM。
如果你的Target API >= 14 (android 4.0)硬件加速默认将开启,但也可以手动开启。如果你的应用只使用了标准的view和Drawable,全局打开硬件加速不会导致任何的绘制影响。但是由于硬件加速并不支持所有的2D绘制操作,打开硬件加速可能会影响一些你的自定义view或者绘制方法调用。问题的表现通常是绘制的控件不可见,异常,或者像素渲染出错。为了解决这些问题,android提供了一种方式可以让你在不同的级别开启和禁用硬件加速。
如果你的应用需要自定义绘制,你需要在真实的支持硬件加速的设备上开启应用的硬件加速开关来测试是否有问题。文章下面的“不支持的绘图操作”部分将描述已知的硬件加速问题和如何规避这些问题。
控制硬件加速
你可以控制以下级别的硬件加速
- Application
- Activity
- Window
- View
Application 级别
在你的android manifest文件,在<application> tag里面添加以下的属性来开启你整个application的硬件加速:
1 |
<application android:hardwareAccelerated="true" ...> |
Activity 级别
当你在application级别全局开启硬件加速后表现不正常的时候,你也可以为单独的activity开始硬件加速。在activity级别开启或关闭硬件加速,你可以使用<activity>标签里面的android:hardwareAccelerated属性。下面的例子展示了在整个application里面开启硬件加速,而在其中一个activity关闭了硬件加速:
1 2 3 4 |
<application android:hardwareAccelerated="true"> <activity ... /> <activity android:hardwareAccelerated="false" /> </application> |
Window 级别
如果你甚至需要更细粒度的控制,你可以使用下面的代码在一个指定的window开启硬件加速:
1 2 3 |
getWindow().setFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); |
注意: 执行了上面的代码后,您目前无法在窗口级别再禁用硬件加速。
View 级别
你可以使用下面的代码在单独的view里面关闭硬件加速:
1 |
myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); |
注意: 执行了上面的代码后,你现在不能在view级别再开启硬件加速了。view层除了禁用硬件加速还有更多其它的方法:参考文章下面“View layers”部分内容关于它的更多使用。
判断一个 View 是否开启了硬件加速
有时当你知道了application当前是否开启了硬件加速会非常有帮助,尤其是对于自定义view而言。当你的application处理了大量的自定义绘图或者不是所有的操作都被新的渲染机制支持时也会非常有帮助。
有两种方法可以检查application是否开启了硬件加速:
View.isHardwareAccelerated()
返回true
如果这个View
关联在了一个开启了硬件加速的window上面Canvas.isHardwareAccelerated()
返回true
如果这个Canvas
开启了硬件加速
如果你需要在你绘制的代码里面检查,建议尽可能使用Canvas.isHardwareAccelerated()而不是View.isHardwareAccelerated() 。当一个view在一个开启了硬件加速的window上面显示时,它仍然可以使用一个关闭硬件加速的canvas来绘图。这种现象,例如,为了缓存目的将视图绘制成位图时。
Android 绘图模型
当硬件加速开启了,Android Framework利用一个新的绘图模型利用显示列表将您的应用程序呈现在屏幕上。为了更全面的理解显示列表和他们如何影响你的application,你也需要知道Android是如何在没有硬件加速的情况下绘制views。接下来的部分会描述基于软件的和硬件加速的绘图模型。
基于软件的绘图模型
在软件绘图模型里,view按照以下两个步骤绘图:
- 刷新层级
- 绘制层级
任何时候当一个application需要更新它某一部分的UI时,它需要调用有内容改变的view的invalidate()方法(或者类似的方法),这个刷新的消息将会一直沿着view的层级传递来计算屏幕中需要被重汇的区域(也就是脏区域)。接下来Android系统会把与这个脏区域相交的层级中的所有的view进行绘制。不幸的是,这种绘图模型有两个缺点:
- 第一点,这个模型需要为每一次绘图执行很多代码。例如:你的应用里面为一个叠在另一个view上面的Button调用了invalidate()方法时,Android系统也会重新绘制这个被重叠的view,甚至这个view都没有发生变化。
- 第二个问题是这个绘图模型会隐藏你application里面的bug。因为Android 系统会重新绘制与脏区域相交的view, 也意味着一个view内容改变了虽然没有调用Invalidate也有可能被重新绘制。那么你就依赖了另一个view的invalidate结果来获得正确的行为。而这个行为会随着你对appication的修改而随时变化,由于这个你就需要在任何时候你修改了数据或者影响这个view状态的绘制逻辑时就需要在你的自定义view上频繁调用invalidate()方法.
注意: Android 的view会自动调用invalidate()方法当它们的属性发生变化。例如TextView的背景颜色或者文字变化了。
硬件加速绘图模型
Android系统仍然是使用invalidate()和draw()方法来请求屏幕刷新来渲染view,但是实际绘图的处理会不同。不同于立即执行绘图命令,Android系统将它们记录在一个显示列表中,这个显示列表包含view层级绘制代码的输出。另一个优化是Android系统只需要给哪些被invalidte()方法调用标记为有变动的view记录和更新显示列表。那些没有被标记为刷新的view会简单的从前一个记录的显示列表中重发这个重绘操作。这个新的绘图模型包含3个阶段:
- 刷新view层级
- 记录和更新显示列表
- 绘制这个显示列表
基于这个绘图模型,你不需要依赖脏区域相交的view的draw()方法去执行。为了确保Android系统记录view的显示列表,你需要调用invalidate()。如果忘记调用的话即使这个view变化了展示也和之前一样。
使用显示列表也对动画的性能非常有帮助,因为当设置指定的属性,例如透明度和旋转,不需要刷新指定的view(这个会自动的完成)。这个优化也应用于有显示列表的view(当你的application开启了硬件加速你的所有view都会被应用)。例如,假定有一个LinearLayout 包含一个ListView并且在另一个Button的上面。这个LinearLayout的显示列表像这样:
- DrawDisplayList(ListView)
- DrawDisplayList(Button)
假设你想改变 ListView
的不透明度. 当你调用了ListView的setAlpha(0.5f)
方法, 这个显示列表现在包含这些:
- SaveLayerAlpha(0.5)
- DrawDisplayList(ListView)
- Restore
- DrawDisplayList(Button)
复杂的ListView绘制代码没有被执行,系统仅仅更新了更简单的LinearLayout的显示列表。如果在没有开启硬件加速的application里面,这个ListView和它的父view的绘制方法都会被再次执行。
不支持的绘图操作
当硬件加速开启,2D渲染管道支持大多数常用的Canvas绘图方法,也包括一些不经常使用的方法。Android自带的用于渲染application的所有绘图操作,默认的控件和布局和常用的高级视觉效果(例如倒影效果,纹理平铺)都支持。
下面的表格描述了各种方法在不同的API等级的支持级别:
最早支持的API等级 | ||||
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 |
Canvas 缩放
2D渲染管道的硬件加速开始时是不支持缩放的,当一些绘图操作在高度缩放后质量降低了很明显。这些操作实现是以1.0比例绘制的纹理,由GPU转换的。在API等级<17时,使用这些操作会导致缩放问题随着比例增加。
下面的表格展示了大比例缩放可以被正确处理是在什么时候实现的:
绘图的操作被缩放 | First supported API level |
drawText() | 18 |
drawPosText() | ✗ |
drawTextOnPath() | ✗ |
Simple Shapes* | 17 |
Complex Shapes* | ✗ |
drawPath() | ✗ |
Shadow layer | ✗ |
注意: ‘Simple’ shapes 包括drawRect()
, drawCircle()
, drawOval()
, drawRoundRect()
, and drawArc()
( useCenter=false 时) 这些绘图命令和没有PathEffect效果的Paint一起使用时,并且也不包含非默认的joins(例如:setStrokeJoin / setStrokeMiter)。其它的绘图命令都属于上面表里面提到的“Complex”
如果你的应用被这些限制所影响,你可以对这些受影响的部分关闭硬件加速(通过调用 setLayerType(View.LAYER_TYPE_SOFTWARE, null)
)。这样的话你仍然可以在程序的其它部分继续享受到硬件加速的魅力。看文章上面的“控制硬件加速”部分了解更多关于如何在应用的不同级别关闭或开启硬件加速的内容。
View 图层
在Android的所有版本中,view 都可以渲染到画面以外的缓冲区,使用view的drawing cache,又或者是使用Canvas.saveLayer()。画面以外的缓冲区,或者图层,有很多的使用场景。你可以用它来为复杂的view的动画或者组合效果提升更好的表现性能。例如,你可以用Canvas.saveLayer来帮助实现一个渐隐效果,把一个view渲染到一个图层中然后把它通过非透明度的参数再合成回屏幕展示。
Android 3.0开始(API 等级 11), 通过View.setLayerType()可以更好地控制如何以及何时使用图层。这个方法有两个参数:图层的类型和一个选填的描述图层如何合成的Paint对象。你可以使用Paint参数来添加彩色滤镜,特殊的混合模式,或者为图层添加不透明效果。可以从下面3种图层类型中为一个View选取你需要的图层:
LAYER_TYPE_NONE
: View使用普通方式渲染,没有画面以外的缓冲处理。这个是默认的行为。LAYER_TYPE_HARDWARE
: 如果application开启了硬件加速,View将通过硬件加速渲染成硬件纹理 。如果application没有开启硬件加速,这个图层类型最终的展现将和LAYER_TYPE_SOFTWARE
.是一样的效果。LAYER_TYPE_SOFTWARE
: 这个View将通过软件方式渲染成一个bitmap
选择用哪一种图层类型取决于你的目标:
- 性能: 用硬件的图层类型可以把view渲染成硬件纹理。一旦一个view被渲染成图层,它的的绘图代码不会被执行直到view调用了invalidate()方法。 一些动画,例如透明度动画,可以直接作用在图层上,GPU来做效率是非常高的。
- 视觉效果: 使用硬件或者软件图层并一个Paint对象来给一个view特别的效果。例如,你可以用ColorMatrixColorFilter来以黑白方式绘制一个view。
- 兼容性: 用软件图层类型强制把一个view用软件方式渲染。如果一个view开启了硬件加速(例如:你的整个application开启了硬件加速),但是有渲染问题,这是一个很简单的方法可以绕过硬件加速渲染管道的限制。
View 图层和动画
当你的application开启了硬件加速,硬件图层可以让动画更加快和流畅。当你的一个有很多绘图操作的复杂的view运行动画时一秒钟60帧几乎是不可能的。但是使用硬件图层把view渲染成硬件纹理那么这种情况就可以得到缓解,这个硬件纹理就可以被用来为view执行动画了,可以消除在动画过程中view频繁的重绘操作。这个view不会被重新绘制除非你改变了view的属性,例如底层代码调用了invalidate(), 或者你手动调用了invalidate()。如果在你的应用中需要运行一段动画,但是动画的流畅度达不到你的预期,可以考虑在你需要动画的view上开启硬件图层。
当view有硬件层支持时,它的一些属性是以图层在屏幕上合成的方式进行处理。设置这些属性将会很高效,原因是不需要view去刷新和重新绘制。下面的属性列表影响了图层的合成方式。调用这些属性的setter方法会优化刷新过程,不会让目标view重新绘制。
alpha
: 改变图层的透明度x
,y
,translationX
,translationY
: 改变图层的位置scaleX
,scaleY
: 改变图层的大小rotation
,rotationX
,rotationY
: 改变图层在3D空间中的方向pivotX
,pivotY
: 转换图层
这些属性是ObjectAnimator的一些别名用于对一个view执行动画。如果你想访问这些属性,可以调用相应的setter或getter方法。例如,需要修改alpha属性,可以调用setAlpha()。. 下面的代码片段展示了最高效的方式让一个view在3D空间围绕Y轴旋转:
1 2 |
view.setLayerType(View.LAYER_TYPE_HARDWARE, null); ObjectAnimator.ofFloat(view, "rotationY", 180).start(); |
因为硬件图层消耗现存,所以强烈建议仅仅在动画过程中开启硬件加速,并且在动画结束后关闭它。你可以通过动画的状态监听接口来实现:
1 2 3 4 5 6 7 8 9 |
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(); |
关于属性动画更多的信息,请学习: Property Animation.
提示和技巧
2D图像在切换了硬件加速后效果会立即提升,但是你仍然应该遵循下面的建议来设计的你的应用,这样可以更有效的利用GPU:
- 减少你应用中view的数量
- 绘制的view越多会越慢。这个对软件渲染管道也是如此。减少view的数量是优化你的UI的最简单方式。
- 避免过度绘制
- 不要叠太多的图层,移除被不透明view完全盖住的那些view。如果你需要把多个图层叠在一起,考虑将它们合并在一个图层中。使用硬件加速一个好的实践是不要在一帧里面绘制屏幕像素总数2.5倍以上的像素数 (位图中的透明像素!)。
- 不要在绘图方法里面创建渲染对象
- 一个常见的错误是在每次渲染的方法调用的时候创建一个Paint对象或者一个Path对象。这个会强制垃圾回收器频繁运行,并且绕过了硬件管道中的缓存和优化。
- 不要频繁修改形状
- 复杂的形状,例如路径,圆圈,使用纹理遮罩渲染的。每次你创建或修改一个路径(path),硬件管道会创建一个新的遮罩,这个代价很高。
- 不要频繁修改位图
- 每次你改变位图的内容, 将会在下一次绘制时作为GPU纹理再次上传。
- 小心使用alpha
- 当你用
setAlpha()
,AlphaAnimation
, orObjectAnimator
方法让一个view半透明时,它被渲染在离屏缓冲区中,该缓冲区将所需填充率加倍。 在非常大的视图上应用alpha时,请考虑将视图的图层类型设置为LAYER_TYPE_HARDWARE。