Android系统的流畅性一直被拿来与iOS比较,并且认为不如后者。这一方面与Android设备硬件质量参差不齐有关,另一方面也与Android系统的实现有关。例如在3.0前,Android应用程序UI绘制不支持硬件加速。不过从4.0开始,Android系统一直以“run fast, smooth, and responsively”为目标对UI进行优化。本文对这些优化进行简要介绍和制定学习计划。
老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!
注意,上面我们说Android系统不支持硬件加速的UI 绘制,针对的是Android应用程序2D UI绘制。对于3D UI,例如游戏,一直是支持硬件加速渲染的。此外,从前面Android应用程序与SurfaceFlinger服务的关系概述和学习计划、Android系统Surface机制的SurfaceFlinger服务简要介绍和学习计划和Android应用程序窗口(Activity)实现框架简要介绍和学习计划这三个系列的文章可以知道,Android系统的UI从绘制到显示到屏幕是分两步进行的:第一步是在Android应用程序进程这一侧进行的;第二步是在SurfaceFlinger进程这一侧进行的。前一步将UI绘制一个图形缓冲区中,并且将该图形缓冲区交给后一步进行合成以及显示在屏幕中。其中,后一步的UI合成一直都是以硬件加速方式完成的。
在支持Android应用程序UI硬件加速渲染之前,Android应用程序UI的绘制是以软件方式进行的,为了更好地理解Android应用程序UI硬件加速渲染技术,我们先回顾在Android应用程序窗口(Activity)实现框架简要介绍和学习计划这个系列的文章提及的软件渲染技术,如图1所示:
图1 Android应用程序UI软件渲染过程
在Android应用程序进程这一侧,每一个窗口都关联有一个Surface。每当窗口需要绘制UI时,就会调用其关联的Surface的成员函数lock获得一个Canvas,其本质上是向SurfaceFlinger服务Dequeue一个Graphic Buffer。Canvas封装了由Skia提供的2D UI绘制接口,并且都是在前面获得的Graphic Buffer上面进行绘制的。绘制完成之后,Android应用程序进程再调用前面获得的Canvas的成员函数unlockAndPost请求显示在屏幕中,其本质上是向SurfaceFlinger服务Queue一个Graphic Buffer,以便SurfaceFlinger服务可以对Graphic Buffer的内容进行合成,以及显示到屏幕上去。
接下来我们再来看Android应用程序UI硬件加速渲染技术,如图2所示:
图2 Android应用程序UI硬件加速渲染过程
这里我们首先要明确什么是硬件加速渲染,其实就是通过GPU来进行渲染。GPU作为一个硬件,用户空间是不可以直接使用的,它是由GPU厂商按照Open GL规范实现的驱动间接进行使用的。也就是说,如果一个设备支持GPU硬件加速渲染,那么当Android应用程序调用Open GL接口来绘制UI时,Android应用程序的UI就是通过硬件加速技术进行渲染的。因此,在接下来的描述中,我们提及到GPU、硬件加速和Open GL时,它们表达的意思都是等价的。
从图2可以看到,硬件加速渲染和软件渲染一样,在开始渲染之前,都是要先向SurfaceFlinger服务Dequeue一个Graphic Buffer。不过对硬件加速渲染来说,这个Graphic Buffer会被封装成一个ANativeWindow,并且传递给Open GL进行硬件加速渲染环境初始化。在Android系统中,ANativeWindow和Surface可以是认为等价的,只不过是ANativeWindow常用于Native层中,而Surface常用于Java层中。另外,我们还可以将ANativeWindow和Surface看作是像Skia和Open GL这样图形渲染库与操作系统底层的图形系统建立连接的一个桥梁。
Open GL获得了一个ANativeWindow,并且进行了硬件加速渲染环境初始化工作之后,Android应用程序就可以调用Open GL提供的API进行UI绘制了,绘制出来内容就保存在前面获得的Graphic Buffer中。当绘制完毕,Android应用程序再调用libegl库提供的一个eglSwapBuffer接口请求将绘制好的UI显示到屏幕中,其本质上与软件渲染过程是一样的,都是向SurfaceFlinger服务Queue一个Graphic Buffer,以便SurfaceFlinger服务可以对Graphic Buffer的内容进行合成,以及显示到屏幕上去。
关于Android应用程序UI的硬件加速渲染过程中涉及到Open GL环境初始化和绘制的简化版本,可以参考前面Android系统的开机画面显示过程分析一文提到的Android系统开机动画的实现。在Android系统的开机画面显示过程分析这篇文章中,开机动画其实是由一个/system/bin/bootanimation程序实现的。这个程序可以看成是一个没有使用Android SDK来开发的一个Native应用程序。
在这个系列的文章中,我们将通过Android 5.0的源码来分析Android应用程序UI的硬件加速渲染技术。不过为了更好地理解Android 5.0的硬件加速渲染实现,我们有必要先了解从Android 3.0以来,Android应用程序UI硬件加速渲染的进化历史:
1. Android 3.0,也就是Honeycomb版本,开始引用OpenGLRenderer图形渲染库,支持Android应用程序UI可选地使用硬件加速渲染。
2. Android 4.0,也就是Ice Cream Sandwich版本,要求设备默认支持Android应用程序UI硬件加速渲染,并且增加一个TextureView控件,该控件直接支持以Open GL纹理的形式来绘制UI。
3. Android 4.1、4.2和4.3,也就是Jelly Bean版本,加入了Project Butter(黄油计划)的特性,包括:A. 通过Vsync信号来同步UI绘制和动画,使得它们可以获得一个达到60fps的固定的帧率;B. 三缓冲支持,改善GPU和CPU之间绘制节奏不一致的问题;C. 将用户输入,例如touch event,同步到下一个Vsync信号到来时再处理;D. 预测用户的touch行为,以获得更好的交互响应;E. 每次用户touch屏幕时,进行CPU Input Boost,以便减少处理延时。
4. Android 4.4,也就是KitKat版本,一方面通过优化内存使用,另一方面是可选地支持使用ART运行时替换Dalvik虚拟机,来提高应用程序的运行效率,使得其UI更流畅。
5. Android 5.0,也就是Lollipop版本,ART运行时引进了Compacting GC,进一步优化了Android应用程序的内存使用,并且ART运行时正式替换了Dalvik虚拟机,同时,Android应用程序增加了一个Render Thread,专门负责UI渲染和动画显示。
从Android应用程序UI硬件加速渲染的进化历史可以看出,Android系统确实是在践行"run fast, smooth, and responsively"的宏伟计划,并且也是做到了。
有了前面的基础知识之后,我们接下来再来Android 5.0的窗口和动画是如何通过硬件加速技术来渲染的,如图3所示:
图3 Android应用程序窗口和动画的硬件加速渲染框架
在Android应用程序窗口中,每一个View都抽象为一个Render Node,而且如果一个View设置有Background,这个Background也被抽象为一个Render Node。这是由于在OpenGLRenderer库中,并没有View的概念,所有的一切可绘制的元素都抽象为一个Render Node。
每一个Render Node都关联有一个Display List Renderer。这里又涉及到另外一个概念——Display List。注意,这个Display List不是Open GL里面的Display List,不过它们在概念上是差不多的。Display List是一个绘制命令缓冲区。也就是说,当View的成员函数onDraw被调用时,我们调用通过参数传递进来的Canvas的drawXXX成员函数绘制图形时,我们实际上只是将对应的绘制命令以及参数保存在一个Display List中。接下来再通过Display List Renderer执行这个Display List的命令,这个过程称为Display List Replay。
引进Display List的概念有什么好处呢?主要是两个好处。第一个好处是在下一帧绘制中,如果一个View的内容不需要更新,那么就不用重建它的Display List,也就是不需要调用它的onDraw成员函数。第二个好处是在下一帧中,如果一个View仅仅是一些简单的属性发生变化,例如位置和Alpha值发生变化,那么也无需要重建它的Display List,只需要在上一次建立的Display List中修改一下对应的属性就可以了,这也意味着不需要调用它的onDraw成员函数。这两个好处使用在绘制应用程序窗口的一帧时,省去很多应用程序代码的执行,也就是大大地节省了CPU的执行时间。
注意,只有使用硬件加速渲染的View,才会关联有Render Node,也就才会使用到Display List。我们知道,目前并不是所有的2D UI绘制命令都是GPU可以支持的。这一点具体可以参考官方说明文档:http://developer.android.com/guide/topics/graphics/hardware-accel.html。对于使用了GPU不支持的2D UI绘制命令的View,只能通过软件方式来渲染。具体的做法是将创建一个新的Canvas,这个Canvas的底层是一个Bitmap,也就是说,绘制都发生在这个Bitmap上。绘制完成之后,这个Bitmap再被记录在其Parent View的Display List中。而当Parent View的Display List的命令被执行时,记录在里面的Bitmap再通过Open GL命令来绘制。
另一方面,对于前面提到的在Android 4.0引进的TextureView,它也不是通过Display List来绘制。由于它的底层实现直接就是一个Open GL纹理,因此就可以跳过Display List这一中间层,从而提高效率。这个Open GL纹理的绘制通过一个Layer Renderer来封装。Layer Renderer和Display List Renderer可以看作是同一级别的概念,它们都是通过Open GL命令来绘制UI元素的。只不过前者操作的是Open GL纹理,而后者操作的是Display List。
我们知道,Android应用程序窗口的View是通过树形结构来组织的。这些View不管是通过硬件加速渲染还是软件渲染,或者是一个特殊的TextureView,在它们的成员函数onDraw被调用期间,它们都是将自己的UI绘制在Parent View的Display List中。其中,最顶层的Parent View是一个Root View,它关联的Root Node称为Root Render Node。也就是说,最终Root Render Node的Display List将会包含有一个窗口的所有绘制命令。在绘制窗口的下一帧时,Root Render Node的Display List都会通过一个Open GL Renderer真正地通过Open GL命令绘制在一个Graphic Buffer中。最后这个Graphic Buffer被交给SurfaceFlinger服务进行合成和显示。
以上就是Android应用程序窗口和动画的硬件加速渲染框架,里面提到的Render Thread还需要进一步解释。Render Thread是在Android 5.0中引进的,它用来分担Android应用程序的Main Thread的工作。在Android 5.0之前,Android应用程序的Main Thread不仅负责渲染UI,还负责处理用户输入。通过引进Render Thread,我们就可以将UI渲染工作从Main Thread释放出来,交由Render Thread来处理,从而也使得Main Thread可以更专注高效地处理用户输入,这样使得在提高UI绘制效率的同时,也使得UI具有更高的响应性。
Main Thread与Render Thread的交互模型如图4所示:
图4 Android应用程序Main Thread与Render Thread的交互模型
Main Thread主要是负责调用View的成员函数onDraw来构造它们的Display List,然后在下一个Vsync信号到来时,再通过一个Render Proxy对象向Render Thread发出一个drawFrame命令。Render Thread内部有一个Task Queue,从Main Thread发送过来的drawFrame命令就会保存在Render Thread的Task Queue,等待Render Thread处理。
上面分析的应用程序UI绘制机制还没有涉及到动画。当一个View需要以动画的形式显示时,我们可以通过调用这个View的成员函数animate获得一个ViewPropertyAnimator。ViewPropertyAnimator可以通过两种方式来显示动画。第一种方式是旧的方式,动画的每一帧由Main Thread进行计算,然后再由Render Thread进行渲染。第二种方式是Android 5.0引进的,Main Thread将动画注册到Render Thread中去,然后由Render Thread计算和显示动画的每一帧。第二种方式在动画的显示期间,完全不需要Main Thread参与,不过前提是Main Thread不想参与。如果Main Thread需要参与,例如,Main Thread想在动画开始之前,将View的Layer Type设置为LAYER_TYPE_HARDWARE,以便它可以以Open GL的Frame Buffer Object(FBO)的形式进行渲染,或者Main Thread想侦听动画的每一帧显示,那么就不能将动画的计算和显示完全交给Render Thread来做了。将动画的计算和显示完全交给Render Thread来做的好处就是使得动画的显示不影响Main Thread响应用户的其它输入。
上面提到,在显示一个View的动画之前,Main Thread可以将View的Layer Type设置为LAYER_TYPE_HARDWARE,这样View可以以FBO的形式进行渲染更,这是通过调用与View关联的ViewPropertyAnimator的成员函数withLayer来实现的。这是对动画显示的另一种优化。回忆TextureView的特点,它是直接通过Open GL纹理来绘制,这样可以省去Display List这一中间步骤。同样的,对于Layer Type为LAYER_TYPE_HARDWARE的View,它将直接通过FBO来实现,这样也是可以提高渲染效率。等到动画结束的时候,ViewPropertyAnimator会将View的Layer Type将恢复为原来设置的类型。
上面描述的两种动画显示优化涉及到Main Thread与Render Thread的交互 如图5所示:图5 Android应用程序Main Thread与Render Thread的动画交互模型
从View获得一个用来显示动画的ViewPropertyAnimator之后,调用它的成员函数withLayer,就会导致Main Thread向Render Thread发送一个buildLayer的命令。Render Thread执行这个buildLayer的命令的时候,就会为与View关联的Render Node设置一个Layer。以后Render Thread就以Layer的方式,即FBO的方式,来渲染View的动画。
另一方面,如果一个View的动画显示不需要Main Thread的参与,那么从View获得一个ViewPropertyAnimator会将动画注册到Render Thread里面的一个AnimatorManager中。Render Thread通过AnitmatorManager检测注册到它里面的动画是否还没有结束。如果还没有结束,那么Render Thread就自动地计算和显示动画的下一帧,直到动画显示结束为止。
至此,Android应用程序UI的硬件加速渲染涉及到的关键概念我们就介绍完成了,接下来我们还会按照以下五个情景进一步分析它的实现:
1. Android应用程序UI硬件加速渲染的环境初始化过程分析;
2. Android应用程序UI硬件加速渲染的预加载资源地图集创建过程分析;
3. Android应用程序UI硬件加速渲染的Display List构建过程分析;
4. Android应用程序UI硬件加速渲染的Display List渲染过程分析;
5. Android应用程序UI硬件加速渲染的动画执行过程分析。
其中,第二个情景是第四个情景涉及到的一个重要优化,因此我们将它单独列出来分析。通过这五个情景的学习,我们就可以深入地掌握Android应用程序UI的硬件加速渲染技术了,敬请期待!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo。