Android自定义View(八) -- 硬件加速

前面学习的内容:
Android自定义View(一) – 初识
Android自定义View(二) – Paint详解
Android自定义View(三) – drawText()
Android自定义View(四) – Canvas
Android自定义View(五) – 绘制顺序
Android自定义View(六) – 属性动画(上)
Android自定义View(七) – 属性动画(下)


本文计划根据HenCoder系列文章进行学习,所以代码风格及博文素材可能会摘自其中


硬件加速经常被提及,很多人感兴趣,这个词给人的概念大概有两种:快速、不稳定。

对很多人来说,硬件加速似乎是一个只可远观而不可亵玩的高科技:是,听说很牛逼,但是不敢乱用,甚至不知道什么时候使用

今天就试着把硬件加速的原理和应用,好好了解一下:

1.硬件加速的本质和原理;

2.硬件加速在Android中的应用;

3.硬件加速在Android正宗的限制。

概念

在正式开始之前需要说明一下,作为绘制部分的最后一期,本期内容只是为了内容的完整性做一个补充,因为之前好几期的内容里都有涉及硬件加速的技术点,而一些读者因为不了解硬件加速而产生了一些疑问。所以仅仅从难度上来讲,这期的内容并不难,并且本期的大部分内容你都可以从这两个页面中找到:

  1. Hardware Acceleration | Android Developers
  2. Google I/O 2011: Accelerated Android Rendering

下面进入正题。

所谓硬件加速,指的是把某些计算工作交给专门的硬件来做,而不是和普通的计算工作一样交给 CPU 来处理。这样不仅减轻了 CPU 的压力,而且由于有了「专人」的处理,这份计算工作的速度也被加快了。这就是「硬件加速」。

而对于 Android 来说,硬件加速有它专属的意思:在 Android 里,硬件加速专指把 View 中绘制的计算工作交给 GPU 来处理。进一步地再明确一下,这个「绘制的计算工作」指的就是把绘制方法中的那些 Canvas.drawXXX() 变成实际的像素这件事。

原理

在硬件加速关闭的时候,Canvas 绘制的工作方式是:把要绘制的内容写进一个 Bitmap,然后在之后的渲染过程中,这个 Bitmap 的像素内容被直接用于渲染到屏幕。这种绘制方式的主要计算工作在于把绘制操作转换为像素的过程(例如由一句 Canvas.drawCircle() 来获得一个具体的圆的像素信息),这个过程的计算是由 CPU 来完成的。大致就像这样:

而开启硬件加速后,Canvas的工作方式改变了:它把绘制的内容转为GPU的操作保存下来,然后交给GPU来完成显示工作。大致过程:

如图,硬件加速开启时,CPU的工作是把绘制工作转换为GPU的操作,这个工作量相对来说还是非常小的。

怎么「加速」了

从上图可以看出,开启硬件加速后,绘制的计算工作有CPU交给GPU,不过这怎么就能起到加速作用,让绘制变快了呢?

硬件加速能够让绘制变快,主要有三个原因:

  1. 本来CPU的工作,分摊一部分给GPU,自然可以提高效率;
  2. 相对于CPU来说,GPU自身的设计本来就对于很多常见类型内容的计算(例如简单的圆形、方形)具有优势;
  3. 由于绘制流程的不同;硬件加速在界面内容发生重绘的时候绘制流程可以得到优化,避免一些重复操作,从而大幅提升绘制效率。

其中前两点可以总结为一句话:用了GPU,绘制就是快,原因很直观,不再多说。

关于第三点,它的原理大致说一下:

前面说到,关闭硬件加速时,绘制内容会被CPU转为实际的像素,然后直接渲染到屏幕,具体来说,这个[实际的像素],是由bitmap承载的,在界面的某个View由于内容发生改变而调用invalidat()方法时,如果没有开启硬件加速,为了正确计算bitmap的像素,这个View的父View、父View的父View乃至一直向上知道最顶级的View,以及所有和它相交的View,都需要被调用invalidate()来重绘,一个View的改变使得大半个界面甚至整个界面重绘一遍,这个工作量是非常大的。

而在开启硬件加速时,前面说过,绘制的内容会被转换成GPU的操作保存下来(承载的形式成为displaylist,对应的类也叫作DisplayList),再转交给GPU。由于所有绘制的内容都没有变成最终的像素,所以它们之间是相互独立的,那么在界面内容发生改变时,只需把发生了改变的View调用invalidate()方法以更新它所对应的GPU就好,至于它的父View和兄弟View,只需要保持原样,那么这个工作量就很小了。

正是由于上面的原因,硬件加速不仅是由于GPU的引入提高效率,而且因为绘制机制的改变,而极大的提高了界面内容改变时的刷新效率

所以把上面三条压缩总结一下,硬件加速更快的原因有两条:

  1. 用了GPU,绘制更快了
  2. 绘制机制的改变,导致界面内容改变时的刷新效率极大提高。

限制

如果仅仅是这样,硬件加速只有好处没有坏处,那大家不必要关心其他问题,直接使用就行了,那这篇文章也没有必要了,既然是好东西,关心那么多原理干嘛?

可事实就是,硬件加速不止有好处,也有限制:收到GPU绘制方式的限制,Canvas的有些方法在硬件加速开启时会失效或者无法正常工作,比如:开启硬件加速,clipPath()在 API 18及以上系统中才有效,具体的 API 限制和 API 版本的关系如下图:

所以,如果你对自定义控件有自定义绘制的内容,最好参照一下表格,确保你的绘制操作可以正确地在所有用户手机中正常显示,而不是只在你最新Android系统的 Nexus 或 Pixel 里测试一遍没问题就发布。那就小心被祭天了。

不过有一点可以放心的是,所有的原生自带控件,都没有用到 API 版本不兼容的绘制操作,可以放心使用。所以你只要检查你写的自定义绘制就好。

View Layer

在之前几期的内容里我提到过几次,如果你的绘制操作不支持硬件加速,你需要手动关闭硬件加速来绘制界面,关闭的方式是通过这行代码:

1
view.setLayerType(LAYER_TYPE_SOFTWARE, null);

很多人有过疑问:什么是layer type?如果这个方法是关闭硬件加速的开关,那么它的参数为什么不是一个LAYER_TYPE_SOFTWARE来关闭硬件加速以及一个LAYER_TYPE_HARDWARE来开启硬件加速,这两个参数,而是三个参数,第三个参数为LAYER_TYPE_NONE,难道还能既不用软件绘制又不用硬件绘制吗?

事实上,view.setLayerType(LAYER_TYPE_SOFTWARE, null)这个方法的作用并不是关闭硬件加速,只是当它的参数为LAYER_TYPE_SOFTWARE的时候,可以顺便把硬件加速关掉而已;并且除了这个方法外,Android并没有提供专门的View级别的硬件加速开关,所以它就顺便成了一个开关硬件加速的方法。

setLayerType() 这个方法,它的作用其实就是字面的意思:设置View Layer的类型。所谓ViewLayer,又称为离屏缓冲(off-screen Buffer),它的作用就是单独启用一块地方来绘制这个View,而不是使用绘制软件的Bitmap或者通过硬件加速的GPU,这块地方可能是一块单独的bitmap,也可能是一块OpenGL的纹理(texture,OpenGL的纹理可以简单理解为图像的意思),具体取决于硬件加速是否开启。采取什么来绘制View不是关键,关键在于当设置了View Layer的时候,它的绘制会被缓存下来,而且缓存的是最终的绘制结果,而不是像硬件加速那样只是把GPU的操作保存下来再交给GPU去计算。通过这样更进一步的缓存方式,View的重绘效率进一步提高了:只要绘制的内容没变,那么不论是CPU绘制还是GPU绘制,都不用重新计算,只要用之前缓存的结果就可以了。

多说一句,其实这个离屏缓冲(Off-screen Buffer),更准确的说应该叫做离屏缓存(Off-screen Cache)会更合适一点。原因在上面这一段里已经说过了,因为它其实是缓存而不是缓冲。(这段话仅代表个人意见)

基于这样的原理,在进行移动、旋转等(无需调用 invalidate())的属性动画的时候开启 Hardware Layer 将会极大地提升动画的效率,因为在动画过程中 View 本身并没有发生改变,只是它的位置或角度改变了,而这种改变是可以由 GPU 通过简单计算就完成的,并不需要重绘整个 View。所以在这种动画的过程中开启 Hardware Layer,可以让本来就依靠硬件加速而变流畅了的动画变得更加流畅。实现方式大概是这样:

1
2
3
4
5
6
7
8
9
10
11
view.setLayerType(LAYER_TYPE_HARDWARE, null);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setLayerType(LAYER_TYPE_NONE, null);
}
});
animator.start();

或者如果是使用 ViewPropertyAnimator,那么更简单:

1
2
3
view.animate()
.rotationY(90)
.withLayer(); // withLayer() 可以自动完成上面这段代码的复杂操作

不过一定要注意,只有你在对 translationX translationY rotation alpha 等无需调用 invalidate() 的属性做动画的时候,这种方法才适用,因为这种方法本身利用的就是当界面不发生时,缓存未更新所带来的时间的节省。所以简单地说——

这种方式不适用于基于自定义属性绘制的动画。一定记得这句话。

另外,除了用于关闭硬件加速和辅助属性动画这两项功能外,Layer 还可以用于给 View 增加一些绘制效果,例如设置一个 ColorMatrixColorFilter 来让 View 变成黑白的:

1
2
3
4
5
6
7
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(0);
Paint paint = new Paint();
paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
view.setLayerType(LAYER_TYPE_HARDWARE, paint);

另外,由于设置了ViewLayer后,View在初次绘制时以及每次invalidate()后重绘时,需要进行两次的绘制工作(一次绘制到Layer,一次从Layer绘制到显示屏),所以其实它的每次绘制的效率是被降低了的,所以一定要慎重使用View Layer,在需要用到它的时候再去使用。

总结

本期内容就是这些,就像文章开始说的,本期知识是作为一个完整的补充,并么有太多重要或者高难度的东西,我也没有准备视频或者太多的截图或者GIF 去说明,总结一下:

硬件加速指使用GPU来完成绘制的计算工作,代替CPU,它从工作分摊和绘制机制优化两个角度提升了绘制速度。

硬件加速可以使用setLayerType()来关闭硬件加速,但这个方法其实是用来设置View Layer的:

  1. 参数为 LAYER_TYPE_SOFTWARE 时,使用软件来绘制View Layer,绘制到一个Bitmap,并顺便关闭硬件加速;
  2. 参数为 LAYER_TYPE_HARDWARE 时,使用GPU来绘制View Layer,绘制到一个OpenGL texture(如果硬件加速关闭,那么行为和LAYER_TYPE_SOFTWARE一致);
  3. 参数为 LAYER_TYPE_NONE 时,关闭View Layer。

View Layer 可以加速无 invalidate() 时的刷新效率,但对于需要调用 invalidate() 的刷新无法加速。

View Layer 绘制所消耗的实际时间是比不使用 View Layer 时要高的,所以要慎重使用。

文章目录
  1. 1. 本文计划根据HenCoder系列文章进行学习,所以代码风格及博文素材可能会摘自其中
  2. 2. 概念
  3. 3. 原理
  4. 4. 怎么「加速」了
  5. 5. 限制
  6. 6. View Layer
  7. 7. 总结
|