在这个小型的博客文章系列中,我们试图窥视Android图形流水线的内部构件。Google已经发布了这一主题的一些洞见和文档,例如Chet Haase和Romain Guy在Google I/O 2012上的座谈“For Butter or Worse”(如果你还没看过,一定要看看!),还有名为“Graphics architecture”的文章。这些资料可以在一定程度上帮助我们对如何在屏幕上获取一个简单的视点有一个大致了解。然而,当我们试图理解背后的源代码时,它们就不是那么有用了。这个系列正是想用恰当适度的方式将读者带入Android图形流水线这一有趣的世界里。
请注意,这一系列文章里将涉及许多源代码和时序图!哪怕你对Android图形流水线只有一丝兴趣,我们也建议你通读它们,这样的话,你将获益匪浅(至少欣赏到了一些漂亮的图片)。那么,请备好咖啡往下读吧!
引言
为了充分理解视点是如何显示到屏幕上这一过程,我们将使用一个小的应用实例来描述Android图形流水线的每一个主要步骤,从公开的Android Java API(SDK)开始,到本地C++代码,最终观察原生的OpenGL绘制操作。
示例中的活动(activity)由一个简单的RelativeLayout(相关布局),一个带有应用程序图标和标题的ActionBar(动作条)以及一个写着“Hello world”的简易按钮组成。
在Android的视图层级中,相关布局由简单的颜色渐变背景组成。更进一步,这种和位图组合起来的渐变背景、One Button文本元素以及同样是位图的应用程序图标共同组成了动作条。按钮的背景使用9-Patch,文本元素“Hello World!”绘制其上。导航条和状态条分别位于屏幕顶部和底部,但是不属于应用活动,它们是由名为“SystemUI”的系统服务直接绘制的。
概览:流水线综述
如果观看了Google I/O座谈“For Butter or Worse”,你一定能认出下面这张显示完整的Android图形流水线的幻灯片。
显示合成系统(Surface Flinger)负责创建图形缓冲区,并将其合成显示在住显示器上。尽管它是Android系统中的重要一环,却不在这次讲解的内容里。
相反地,我们有选择地观察那些在视图绘制到屏幕的过程中承担绝大部分工作的模块。
显示列表
你可能已经知道,Android使用名为“DisplayLists”的概念绘制所有的视图。对不知道这一概念的读者来说,一个显示列表就是图形命令的序列,它们是执行绘制特定视图所必需的。这些显示列表在高性能Android图形流水线的实现中扮演重要角色。
每个开发者都知道,视图层级中的每个视图都有一个对应的显示列表,这个列表由views类的onDraw()方法创建。为了将视图层级绘制到屏幕,只须评估和执行对应的显示列表即可。当一个视图无效时(由于用户的输入、动画或是转场),受到影响的显示列表将被重建并最终重绘。这种机制可以防止在绘制每一帧时调用开销高昂的onDraw()方法。
显示列表还可以嵌套使用,也就是说,一个显示列表可以包含绘制子显示列表的命令。这对于利用显示列表来重现视图层级来说相当重要。毕竟,即便是我们使用的简易程序也拥有多重嵌套的视图。
绘制命令是各种声明的混合体,可以直接映射为OpenGL命令,诸如平移、设置裁剪矩形等,除此之外,还有其他更复杂的命令如DrawText、DrawPatch等。它们需要更加复杂的OpenGL命令集。
1 2 3 4 5 6 7 8 |
Save 3 DrawPatch Save 3 ClipRect 20.00, 4.00, 99.00, 44.00, 1 Translate 20.00, 12.00 DrawText 9, 18, 9, 0.00, 19.00, 0x17e898 Restore RestoreToCount 0 |
在上面的例子里,你能看到简易按钮的显示列表里都有哪些种类的操作。第一个操作用来将当前平移矩阵存储到栈中,以便稍后读取。接下来绘制按钮的9-Patch,然后又是一条存储命令。这样做是必要的,因为对将要绘制的文本来说,裁剪矩形只会影响文本的绘制区域。移动设备的GPU可以将裁剪矩阵视为一个痕迹,用来优化后续的绘制调用命令。接下来,绘制原点移动到文本位置并绘制文本。最后,从栈里读取原始的平移矩阵和状态,这将重置裁剪矩形。
实例程序里关于显示列表的完整日志请参看本帖最后。
深入源码
带着刚获得的知识,我们可以进一步深入理解源码。
根视图
每个Android活动在视图层级的顶部都有一个隐式的根视图,它正好包含一个子视图。而这个子视图就是程序开发者定义的第一个真实视图。根视图负责调度和执行多种操作,诸如绘制视图、使视图无效化,以及其他。
类似地,每个视图都有父视图的一个引用。视图层级中的第一个视图,引用了根视图,便以根视图为父视图。View类作为每个可视元素和部件的基类,并不支持任何子类。反而是由它派生的ViewGroup类,支持多种子类,并可作为容器基类被标准布局(相关布局等)使用。
如果一个视图是(部分)无效的,它将调用根视图的invalidateChildInParent()方法。根视图将保留所有失效区域的记录并在下一次VSync事件触发是安排执行一次新的choreographer遍历。
1 2 3 4 5 6 7 8 9 10 |
public ViewParent invalidateChildInParent(int[] location, Rect dirty) { // Add the new dirty rect to the current one mDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom); // Already scheduled? if (!mWillDrawSoon) { scheduleTraversals(); } return null; } |
创建显示列表
如前所述,每个视图负责创建自己的显示列表。当VSync事件触发时,choreographer调用根视图的performTrayersals,并请求HardwareRenderer来绘制视图,这又将再次请求视图生成新的显示列表。
在Android框架已有的将近20000行代码中,View是其中最大的类之一。这丝毫不令人惊讶,因为View类是每一个部件和应用的基石。它负责处理键盘输入、轨迹球和触摸等事件,还有滚动、滚动条、布局和测量等等事务。
Hardware Renderer调用View.getDisplayList()方法创建一个新的内部显示列表,用于视图生命周期的剩余部分。接下来,内部显示列表请求一个足够大的画布(canvas)来容纳视图。视图和它的所有子类都将利用draw(…)方法绘制在GLES20RecordingCanvas上。画布略微有些特殊,它并不执行绘制命令,而是将它们作为命令存入绘制列表。这意味着部件和每一个视图都可以使用标准绘制API而不必在意这些命令是被绘制到一个显示列表的。
在draw(…)方法中,视图将执行onDraw()代码,将自己绘制到所支持的画布上。如果视图还有子类,它同样会调用每一个子类的draw()方法。这些子视图可以使任何东西,标准按钮也可,另一个视图组的布局也可。这个布局同样可以包含其它子类,它们也都将被绘制。
未完待续
到生成显示列表这里,这篇文章的第一部分也完结了。请跳转到第二部分,继续观察这些显示列表如何绘制到屏幕上!
下载
本文基于一篇学位论文而做,全文可供下载。