前面学习的内容:
Android自定义View(一) – 初识
Android自定义View(二) – Paint详解
Android自定义View(三) – drawText()
Android自定义View(四) – Canvas
Android自定义View(五) – 绘制顺序
Android自定义View(六) – 属性动画(上)
Android自定义View(七) – 属性动画(下)
今天学习自定义View部分的最有一篇:硬件加速因为无法录制GIF,所以本篇内容基本为原博
硬件加速经常被提及,很多人感兴趣,这个词给人的概念大概有两种:快速、不稳定。
对很多人来说,硬件加速似乎是一个只可远观而不可亵玩的高科技:是,听说很牛逼,但是不敢乱用,甚至不知道什么时候使用
今天就试着把硬件加速的原理和应用,好好了解一下:
1.硬件加速的本质和原理;
2.硬件加速在Android中的应用;
3.硬件加速在Android正宗的限制。
在正式开始之前需要说明一下,作为绘制部分的最后一期,本期内容只是为了内容的完整性做一个补充,因为之前好几期的内容里都有涉及硬件加速的技术点,而一些读者因为不了解硬件加速而产生了一些疑问。所以仅仅从难度上来讲,这期的内容并不难,并且本期的大部分内容你都可以从这两个页面中找到:
Hardware Acceleration | Android Developers
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,不过这怎么就能起到加速作用,让绘制变快了呢?
硬件加速能够让绘制变快,主要有三个原因:
本来CPU的工作,分摊一部分给GPU,自然可以提高效率;
相对于CPU来说,GPU自身的设计本来就对于很多常见类型内容的计算(例如简单的圆形、方形)具有优势;
由于绘制流程的不同;硬件加速在界面内容发生重绘的时候绘制流程可以得到优化,避免一些重复操作,从而大幅提升绘制效率。
其中前两点可以总结为一句话:用了GPU,绘制就是快,原因很直观,不再多说。
关于第三点,它的原理大致说一下:
前面说到,关闭硬件加速时,绘制内容会被CPU转为实际的像素,然后直接渲染到屏幕,具体来说,这个[实际的像素],是由bitmap承载的,在界面的某个View由于内容发生改变而调用invalidat()方法时,如果没有开启硬件加速,为了正确计算bitmap的像素,这个View的父View、父View的父View乃至一直向上知道最顶级的View,以及所有和它相交的View,都需要被调用invalidate()来重绘,一个View的改变使得大半个界面甚至整个界面重绘一遍,这个工作量是非常大的。
而在开启硬件加速时,前面说过,绘制的内容会被转换成GPU的操作保存下来(承载的形式成为displaylist,对应的类也叫作DisplayList),再转交给GPU。由于所有绘制的内容都没有变成最终的像素,所以它们之间是相互独立的,那么在界面内容发生改变时,只需把发生了改变的View调用invalidate()方法以更新它所对应的GPU就好,至于它的父View和兄弟View,只需要保持原样,那么这个工作量就很小了。
正是由于上面的原因,硬件加速不仅是由于GPU的引入提高效率,而且因为绘制机制的改变,而极大的提高了界面内容改变时的刷新效率
所以把上面三条压缩总结一下,硬件加速更快的原因有两条:
用了GPU,绘制更快了
绘制机制的改变,导致界面内容改变时的刷新效率极大提高。
如果仅仅是这样,硬件加速只有好处没有坏处,那大家不必要关心其他问题,直接使用就行了,那这篇文章也没有必要了,既然是好东西,关心那么多原理干嘛?
可事实就是,硬件加速不止有好处,也有限制:收到GPU绘制方式的限制,Canvas的有些方法在硬件加速开启时会失效或者无法正常工作,比如:开启硬件加速,clipPath()在 API 18及以上系统中才有效,具体的 API 限制和 API 版本的关系如下图:
所以,如果你对自定义控件有自定义绘制的内容,最好参照一下表格,确保你的绘制操作可以正确地在所有用户手机中正常显示,而不是只在你最新Android系统的 Nexus 或 Pixel 里测试一遍没问题就发布。那就小心被祭天了。
不过有一点可以放心的是,所有的原生自带控件,都没有用到 API 版本不兼容的绘制操作,可以放心使用。所以你只要检查你写的自定义绘制就好。
在之前几期的内容里我提到过几次,如果你的绘制操作不支持硬件加速,你需要手动关闭硬件加速来绘制界面,关闭的方式是通过这行代码:
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,可以让本来就依靠硬件加速而变流畅了的动画变得更加流畅。实现方式大概是这样:
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
,那么更简单:
view.animate()
.rotationY(90)
.withLayer(); // withLayer() 可以自动完成上面这段代码的复杂操作
不过一定要注意,只有你在对 translationX
translationY
rotation
alpha
等无需调用 invalidate()
的属性做动画的时候,这种方法才适用,因为这种方法本身利用的就是当界面不发生时,缓存未更新所带来的时间的节省。所以简单地说——
这种方式不适用于基于自定义属性绘制的动画。一定记得这句话。
另外,除了用于关闭硬件加速和辅助属性动画这两项功能外,Layer 还可以用于给 View 增加一些绘制效果,例如设置一个 ColorMatrixColorFilter
来让 View 变成黑白的:
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的:
参数为 LAYER_TYPE_SOFTWARE
时,使用软件来绘制View Layer,绘制到一个Bitmap,并顺便关闭硬件加速;
参数为 LAYER_TYPE_HARDWARE
时,使用GPU来绘制View Layer,绘制到一个OpenGL texture(如果硬件加速关闭,那么行为和LAYER_TYPE_SOFTWARE一致);
参数为 LAYER_TYPE_NONE
时,关闭View Layer。
View Layer 可以加速无 invalidate()
时的刷新效率,但对于需要调用 invalidate()
的刷新无法加速。
View Layer 绘制所消耗的实际时间是比不使用 View Layer 时要高的,所以要慎重使用。