Android 的窗口管理系统 (View, Canvas, WindowManager)
在图解Android - Zygote 和 System Server 启动分析一 文里,我们已经知道Android 应用程序是怎么创建出来的,大概的流程是 ActivityManagerService -> Zygote -> Fork App, 然后应用程序在ActivityThread 中的进入loop循环等待处理来自AcitivyManagerService的消息。如果一个Android的应用有Acitivity, 那它起来后的第一件事情就是将自己显示出来,这个过程是怎样的? 这就是本章节要讨论的话题。
Android 中跟窗口管理相关(不包括显示和按键处理)主要有两个进程,Acitivty所在进程 和 WndowManagerService 所在进程(SystemServer). 上图中用不同颜色区分这两个进程,黄色的模块运行在Activity的进程里,绿色的模块则在System Server内部,本文主要讨论的是WindowManager Service。它们的分工是,Activity进程负责窗口内View的管理,而WindowManager Service 管理来自与不同Acitivity以及系统的的窗口。
1. Acitivty显示前的准备工作
在图解Android - Zygote, System Server 启动分析中我们已经知道,一个新的应用被fork完后,第一个调用的方法就是 ActivityThread的main(),这个函数主要做的事情就是创建一个ActivityThread线程,然后调用loop()开始等待。当收到来自 ActivityManager 的 LAUNCH_ACTIVITY 消息后,Activity开始了他的显示之旅。下图描绘的是Activity在显示前的准备流程。
图分为三部分, 右上角是Acitivity应用的初始化。中间部分是Acitivity 与WindowManager Service的交互准备工作,左下角是window显示的开始。本文主要描述后两部分,而Activity的启动会放在图解Android - Android GUI 系统 (4) - Activity的生命周期里讲解。
- Activity内部的准备过程,这里面有一个重要对象,ContextImpl 生成,它是Context类的具体实现,它里面封装了应用程序访问系统资源的一些基本API, 比如说,连接某一个服务并获取其IBinder,发送Intent, 获取应用程序的信息,访问数据库等等,在应用看来,它就是整个AndroidSDK的入口。ContextImpl 除了实现函数,里面还维护成员变量,其中有一个mDisplay,代表当前应用输出的显示设备,如果应用没有特别指定,一般指向系统的默认显示输出,比如手机的液晶屏。
- 在图解Android - Android GUI 系统 (1) - 概论中我们已经介绍过ViewRootImpl 的地位相当与MVC架构中的C,Controller是连接View和Modal的关键,所以需要首先创建它。当addView(view, param)被调用的时候,一个ViewRoot就被创建出来,addView()的实现如下:
public void addView(View view, ViewGroup.LayoutParams params) { mGlobal.addView(view, params, mDisplay, mParentWindow); }
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow){ ... root = new ViewRootImpl(view.getContext(), display); ... }
这里的参数View是想要添加到WindowManagerService 的“window", 一般一个Activity只需要一个’Window', 所以,Acitivy的默认实现是将DecorView作为”Window" 交给Window Manager Service 进行管理。Params是Layout相关的参数,里面包含有长,宽,边缘尺寸(Margin)等信息,mDisplay就是这个窗口想要输出的Display设备编号,由ContextImpl传递过来。mParentWindow 就是Activity的成员变量mWindow,从最上面的类图可以很容易看出来,对于手机而言,就是一个PhoneWindow对象,对于GoogleTV,就是TVWindow对象。
- ViewRootImpl 在构造过程成初始化一些重要的成员变量,包括一个Surface对象(注意这是一个空的Surface对象,没有赋给任何有效的值,后面会通过CopyFromParcel来填充),还有mChoreophaer定时器(Singleton对象,每个进程只有一个),与此同时,ViewRootImp通过WindowManagerGlobal创建了一个和WindowManagerService 的通话通道,接下来会利用这条通道做进一步的初始化工作。
- 还是在addView()里,WindowManagerImpl拿到ViewRoot对象后调用它的setView方法,将view, layout参数交给ViewRootImpl开始接管。在setView()里ViewRootImpl做进一步的初始化工作,包括创建一个InputChannel接收用户按键输入,enable图形硬件加速,请求第一次的Layout等等,这里只介绍跟WindowManagerService 有关系的一件事,就是向WindowManager service 报道,加入到WindowManager的窗口管理队列中。这个函数是 addToDisplay(),
int addToDisplay(in IWindow window, //提供给WMS的回调接口 in int seq, in windowManager.LayoutParams attrs, // layout参数 in int viewVisibility, in int layerStackId, // display ID out Rect outContentInsets, // WMS计算后返回这个View在显示屏上的位置 out InputChannel outInputChannel); // 用户输入通道Handle
addToDisplay() 最终会调到WindowManager Service的addWindow() 接口。
- addWindow() 里首先生成了一个WindowState对象,它是ViewRootImpl 在WindowManager Service端的代表。在它的构造函数里,WindowState 会生成IWindowId.Stub 对象和DeathRecipient对象来分别监听Focus和窗口死亡的信息,根据用户传进来的Window Type计算出窗口的mBaseLayer,mSubLayer和mLastLayer值,分别对应于主窗口,主窗口上弹出的子窗口(如输入法),以及动画时分别对应的ZOrder值,(在本文后面会具体介绍),生成一个WindowStateAnimation 负责整个Window的动画,并在内部将windowToken, appWindowToken等关联起来。
- WindowManager Service 调用openInputChannelPair() and RegisterInputChannel(), 创建用于通信的SocketPair , 将其传给InputManagerService, 用于接下来的用户输入事件对应的响应窗口(参考Android的用户输入处理),
- 最后,WindowManagerService 调用WindowState的attach(),创建了一个Surface Session 并将Surface Session,WindowSession 还有WindowState 三者关联起来.
- WindowManager Service 调用 assignLayersLocked()计算所有Window的Z-Order。
- addToDisplay() 返回,ViewRootImpl 和 WindowManager Service 内部的准备工作就绪。ActivityThread会发送ACTIVITY_RESUMED消息告诉Activity显示开始。可以是图还没有画,不是吗?对的,此刻Surface还没有真正初始化(我们前面说过ViewRootImpl只是New了一个空的对象,需要有人往里面填东西)。底层存放绘制结果的Buffer也没有创建,但是最多16ms以后这一切就会开始。
2. Choreographer 和 Surface的创建
所有的图像显示输出都是由时钟驱动的,这个驱动信号称为VSYNC。这个名词来源于模拟电视时代,在那个年代,因为带宽的限制,每一帧图像都有分成两次传输,先扫描偶数行(也称偶场)传输,再回到头部扫描奇数行(奇场),扫描之前,发送一个VSYNC同步信号,用于标识这个这是一场的开始。场频,也就是VSYNC 频率决定了帧率(场频/2). 在现在的数字传输中,已经没有了场的概念,但VSYNC这一概念得于保持下来,代表了图像的刷新频率,意味着收到VSYNC信号后,我们必须将新的一帧进行显示。
VSYNC一般由硬件产生,也可以由软件产生(如果够准确的话),Android 中VSYNC来着于HWComposer,接收者没错,就是Choreographer。Choreographer英文意思是编舞者,跳舞很讲究节奏不是吗,必须要踩准点。Choreographer 就是用来帮助Android的动画,输入,还是显示刷新按照固定节奏来完成工作的。看看Chroreographer 和周边的类结构。
从图中我们可以看到, Choreographer 是ViewRootImpl 创建的(Choreographer是一个sigleton类,第一个访问它的ViewRootImpl创建它),它拥有一个Receiver, 用来接收外部传入的Event,它还有一个Callback Queue, 里面存放着若干个CallbackRecord, 还有一个FrameHandler,用来handleMessage, 最后,它还跟Looper有引用关系。再看看下面这张时序图,一切就清楚了,
首先Looper调用loop() 后,线程进入进入睡眠,直到收到一个消息。Looper也支持addFd()方法,这样如果某个fd上发生了IO操作(read/write), 它也会从睡眠中醒来。Choreographer的实现用到了这两种方式,首先他通过某种方式获取到SurfaceFlinger 进程提供的fd,然后将其交给Looper进行监听,只要SurfaceFlinger往这个fd写入VSync事件,looper便会唤醒。Lopper唤醒后,会执行onVsync()时间,这里面没有做太多事情,而是调用Handler接口 sendMessageAtTime() 往消息队列里又送了一个消息。这个消息最终调到了Handler (实际是FrameHandler)的handleCallback来完成上层安排的工作。为什么要绕这么大个圈?为什么不在onVSync里直接handleCallback()? 毕竟onVSync 和 handleCallback() 都在一个线程里。这是因为MessageQueue 不光接收来自SurfaceFlinger 的VSync 事件,还有来自上层的控制消息。VSync的处理是相当频繁的,如果不将VSync信号送人MessageQueue进行排队,MessageQueue里的事件就有可能得不到及时处理,严重的话会导致溢出。当然了,如果因为VSync信号排队而导致处理延迟,这就是设计的问题了,这也是为什么Android文档里反复强调在Activity的onXXX()里不要做太耗时的工作,因为这些回调函数和Choreographer运行在同一个线程里,这个线程就是所谓的UI线程。
言归正传,继续往前,VSync事件最终在doFrame()里调了三次doCallbacks()来完成不同的功能, 分别处理用户输入事件,动画刷新(动画就是定时更新的图片), 最后执行performTraversals(),这个函数里面主要是检查当前窗口当前状态,比如说是否依然可见,尺寸,方向,布局是否发生改变(可能是由前面的用户输入触发的),分别调用performMeasure(), performLayout, performDraw()完成测量,布局和绘制工作。我们会在后面详细学习这三个函数,这里我们主要看一下第一次进入performTraversals的情况,因为第一次会做些初始化的工作,最重要的一件就是如本节标题,创建Surface对象。
回看图2,我们可以看到Surface的创建不是在Activity进程里,而是在WindowManagerService完成的(粉颜色)。当一个Activity第一次显示的时候,Android显示切换动画,因此Surface是在动画的准备过程中创建的,具体发生在类WindowStateAnimator的createSurfaced()函数。它最终创建了一个SurfaceControl 对象。SurfaceControl是Android 4.3 里新引进的类,Google从之前的Surface类里拆出部分接口,变成SurfaceControl,为什么要这样? 为了让结构更清晰,WindowManagerService 只能对Surface进行控制,但并不更新Surface里的内容,分拆之后,WindowManagerService 只能访问SurfaceControl,它主要控制Surface的创建,销毁,Z-order,透明度,显示或隐藏,等等。而真正的更新者,View会通过Canvas的接口将内容画到Surface上。那View怎么拿到WMService创建的Surface,答案是下面的代码里,surfaceControl 被转换成一个Surface对象,然后传回给ViewRoot, 前面创建的空的Surface现在有了实质内容。Surface通过这种方式被创建出来,Surface对应的Buffer 也相应的在SurfaceFlinger内部通过HAL层模块(GRAlloc)分配并维护在SurfaceFlinger 内部,Canvas() 通过dequeueBuffer()接口拿到Surface的一个Buffer,绘制完成后通过queueBuffer()还给SurfaceFlinger进行绘制。
SurfaceControl surfaceControl = winAnimator.createSurfaceLocked(); if (surfaceControl != null) { outSurface.copyFrom(surfaceControl); if (SHOW_TRANSACTIONS) Slog.i(TAG, " OUT SURFACE " + outSurface + ": copied"); } else { outSurface.release(); }
到这里,我们知道了Activity的三大工作,用户输入响应,动画,和绘制都是由一个定时器驱动的,Surface在Activity第一次启动时由WindowManager Service创建。接下来我们具体看一下View是如何画在Surface Buffer上的,而Surface Buffer的显示则交由图解Android - Android GUI 系统 (3) - Surface Flinger 来讨论。
3. View的Measure, Layout 和 Draw
直接从前面提到的performMeasure()函数开始.
因为递归调用,实际的函数调用栈比这里显示的深得很多,这个函数会从view的结构树顶(DecorView), 一直遍历到叶节点。中间会经过三个基类,DecorView, ViewGroup 和 View, 它们的类结构如下图所示:
所有可见的View(不包括DecorView 和 ViewGroup)都是一个矩形,Measure的目的就是算出这个矩形的尺寸, mMeasuredWidth 和 mMeasuredHeight (注意,这不是最终在屏幕上显示的尺寸),这两个尺寸的计算受其父View的尺寸和类型限制,这些信息存放在 MeasureSpec里。MeasureSpec 里定义了三种constraints,
-
/*父View对子View尺寸没有任何要求,其可以设任意尺寸*/ public static final int UNSPECIFIED = 0 << MODE_SHIFT; /* 父View为子View已经指定了大小*/ public static final int EXACTLY = 1 << MODE_SHIFT; /*父View没有指定子View大小,但其不能超过父View的边界 */ public static final int AT_MOST = 2 << MODE_SHIFT;
widthMeasureSpec 和 heightMeasureSpec 作为 onMeasure的参数出入,子View根据这两个值计算出自己的尺寸,最终调用 setMeasuredDimension() 更新mMeasuredWidth 和 mMeasuredHeight.
performMeasure() 结束后,所有的View都更新了自己的尺寸,接下来进入performLayout().
performLayout() 的流程和performMeasure基本上一样,可以将上面图中的measure() 和 onMeasure 简单的换成 layout() 和 onLayout(), 也是遍历整课View树,根据之前算出的大小将每个View的位置信息计算出来。这里不做太多描述,我们把重心放到performDraw(), 因为这块最复杂,也是最为重要的一块。
很早就玩过Android手机的同学应该能体会到Android2.3 到 Android 4.0 (其实Android3.0就有了,只是这个版本只在平板上有)的性能的巨大提升,UI界面的滑动效果一下变得顺滑很多,到底是framework的什么改动带来的?我们马上揭晓。。。(这块非本人工作领域,网上相关的资料也很少,所以纯凭个人砖研,欢迎拍砖指正)
Android Graphics Hardware Acceleration
OK, 如标题所述,最根本的原因就是引入了硬件加速, GPU是专门优化图形绘制的硬件单元,很多GPU(至少手机上的)都支持OpenGL,一种开放的跨平台的3D绘图API。Android3.0以前,几乎所有的图形绘制都是由Skia完成,Skia是一个向量绘图库,使用CPU来进行运算, 所以它的performance是一个问题(当然,Skia也可以用GPU进行加速,有人在研究,但好像GPU对向量绘图的提升不像对Opengl那么明显),所以从Android3.0 开始,Google用hwui取代了Skia,准确的说,是推荐取代,因为Opengl的支持不完全,有少量图形api仍由Skia完成,另外还要考虑到兼容性,硬件加速的功能并不是默认打开,需要程序在AndroidManifests.xml 或代码里控制开关。当然,大部分Canvas的基本操作都通过hwui重写了,hwui下面就是Opengl和后面的GPU,这也是为什么Android 4.0的launcher变得异常流畅的缘故。OK,那我们接下来的重点就是要分析HWUI的实现了。
在此之前,简单的介绍一下OpenGL的一些概念,否则很难理解。要想深入理解Opengl,请必读经典的红包书:http://www.glprogramming.com/red/
Opengl说白了,就是一组图形绘制的API。 这些API都是一些非常基本的命令,通过它,你可以构造出非常复杂的图形和动画,同时,它又是跟硬件细节无关的,所以无需改动就可以运行在不同的硬件平台上(前提是硬件支持所需特性)。OpenGL的输入是最基本几何元素(geometric primitives), 点(points), 线(lines), 多边形(polygons), 以及bitmap和pixle data, 他的输出是一个或两个Framebuffer(真3D立体). 输入到输出的流程(rendering pipeline)如下图所示:
这里有太多的概念,我们只描述跟本文相关的几个:
vertex data
所有的几何元素(点线面)都可以用点(vertics)来描述, 每个点都对应三维空间中的一个坐标(x,y,z), 如下图所示,改变若干点的位置,我们便可以构造出一个立体的图形。
Triangles
OpenGL只能画非凹(nonconvex)的多边形,可是现实世界中存在太多的凹性的物体,怎么办呢?通过连线可以将凹的物体分成若干个三角形,三角形永远都是凸(convex)的。同时三角形还有一个特性,三个点可以唯一确定一个平面,所以用尽可能多的三角形就可以逼近现实世界中复杂的曲线表面,比如下图的例子,三角形的数目越多,球体的表示就越逼真。这也是为什么我们经常看到显卡的性能评测都以三角形的生成和处理作为一个非常重要的指标。
Display List
所有的Vertex和Pixel信息均可以存在Display List 里面,用于后续处理,换句话说,Display List 就是OpenGL命令的缓存。Display List的使用对OpenGL的性能提升有很大帮助。这个很容易理解,想象一个复杂的物体,需要大量的OpenGL命令来描绘,如果画一次都需要重新调用OpenGL API,并把它转换成Vertex data,显然是很低效的,如果把他们缓存在Display List里,需要重绘的时候,发一个个命令通知OpenGL直接从Display List 读取缓存的Vertex Data,那势必会快很多,如果考虑到Opengl是基于C/S架构,可以支持远程Client,这个提升就更大了。Display也可以缓存BitMap 或 Image, 举个例子,假设要显示一篇文章,里面有很多重复的字符,如果每个字符都去字库读取它的位图,然后告诉Opengl去画,那显然是很慢的。但如果将整个字库放到Display List里,显示字符时候只需要告诉Opengl这个字符的偏移量,OpenGL直接访问Display List,那就高效多了。
Pixel Data
Pixle data 包括位图(bitmap), Image, 和任何用于绘制的Pixel数据(比如Fonts)。通常是以矩阵的形式存放在内存当中。通过Pxiel data, 我们避免大量的图形绘制命令。同时通过现实世界中获取的纹理图片,可以将最终的物体渲染得更逼真。比如说画一堵墙,如果没有pixel data,我们需要将每块砖头都画出来,也就是说需要大量的Vertex。可是如果通过一张现实生活中拍摄的砖墙的图片,只需要4个点画出一个大矩形,然后上面贴上纹理,显然,速度和效果都要好得多。
FrameBuffer
Framebuffer就是Opengl用来存储结果的buffer。Opengl的frameBuffer类型有几种。Front Buffer 和 Back Buffer, 分别用于显示和绘制,两者通过swapBuffer 进行交换。Left Buffer 和 Right buffer, 用于真立体(需要带眼镜的那种) 图像的左右眼Buffer,Stencil buffer, 用于禁止在某些区域上进行绘制,想像一下如果在一件T恤上印上图案,你是不是需要一个镂空的纸板?这个纸板就是stencil buffer.
OK, 对Opengl rendering pipeline简单介绍到此,有兴趣的同学可以阅读opengl的红包书或运行一些简单的例子来深入理解Opengl。回到主题,仅仅使用Opengl 和 GPU 取代Skia 就能够大幅提升性能?答案当然不是,性能的优化很大程度上取决于应用,应用必须正确的使用Opengl命令才能发挥其最大效能。Android从pipeline 角度提供了两种机制来提升性能,一个就是我们刚才说到的Display List,另一个叫 Hardware Layer, 其实就是缓存的FrameBuffer, 比如说Android的墙纸,一般来说,他是不会发生变化的,因此我们可以将它缓存在Hardware Layer里,这张就不需要每次进行拷贝和重绘,从而大幅提升性能。
说白了,优化图形性能的核心在于 1)用硬件来减少CPU的参与,加速图形计算。 2)从软件角度,通过Display List 和 Hardware Layer, 将已经完成的工作尽可能的缓存起来,只做必须要做的事情,尽可能的减少运算量。
接下来看实现吧。
Canvas, Renderer, DisplayList, HardwareLayer 实现
这块代码相当的复杂,花了两天时间才把下面的图整理出来,但还是没有把细节完全吃透,简单的介绍一下框架和流程吧,如果有需要大家可以下来细看代码。
图中上半部为Java 代码,下半部为Native层。先介绍里面出现的一些概念:
Canvas
Canvas是Java层独有的概念,它为View提供了大部分图形绘制的接口。这个类主要用于纯软件的绘制,硬件加速的图形绘制则由HardwareCanvas取代。
HardwareCanvas,GLES20Canvas, GLES20RecordingCanvas
hardwareCanvas是一个抽象类,如果系统属性和应用程序指定使用硬件加速(现已成为默认),它将会被View(通过AttachInfo,如 下图所示) 引用来完成所有的图形绘制工作。GLES20Canvas 则是hardwareCanvas的实现,但它也只是一层封装而已,真正的实现在Native 层,通过jni (andriod_view_gles20Canvas.cpp)接口来访问底层的Renderer, 进而执行OpenGL的命令。 此外,GLES20Canvas还提供了一些静态接口,用于创建各类Renderer对象。
GLES20RecordingCanvas 继承GLES20Canvas, 通过它调用的OpenGL命令将会存储在DisplayList里面,而不会立即执行。
HardwareRenderer, GLRender, GL20Renderer
这三个类都是Java的Wrapper类,通过访问各种Canvas来控制绘制流程。详见下面两张时序图。
OpenGLRenderer, DisplayListRenderer, HardwareLayerRenderer
Java的HardwareCanvas 和 HardwareRenderer在底层的对应实现。OpenGLRenderer是基类,只有它直接访问底层OpenGL库。DisplayListRenderer 将View通过GLES20Canvas传过来的OpenGL 命令存在OpenGL的DisplayList中。而HardwareLayerRenderer 管理HardwareLayer的资源。
GLES20
就是OpenGL ES 2.0 的API。它的实现一般由GPU的设计厂家提供,可以在设备的/system/lib/egl/ 找到它的so,名字为 libGLES_xxx.so, xxx 就是特定设备的代号,比如说,libGLES_gc.so 就是Vivante公司的GPU实现,libGLESv2_mali.so 就是ARM公司提供的Mali GPU的实现。它也可以由软件实现,比如说 libGLES_android.so, 是Google提供的软件实现。
EGL
虽然对于上层应用来说OpenGL接口是跨平台的,但是它的底层(GPU)实现和平台(SoC)是紧密相关的,于是OpenGL组织定义一套接口用来访问平台本地的窗口系统(native platform window system),这套接口就是EGL,比如说 eglCreateDisplay(), eglCreateSurface(), eglSwapBuffer()等等。EGL的实现一般明白libEGL.so, 放在/system/lib/egl/ 下面。
View, Canvas, Renderer, DisplayList, HardwareLayer 的关系如下图所示:
每个View都对应一个DisplayList, 在Native层代码里管理。每个View通过GLESRecordingCanvas 以及Native层对应的DisplayRenderer 将OpenGL命令存入DisplayList.最后View 通过GLES20Canvas 通知OpenGLRenderer 执行这些DisplayList 里面的OpenGL 命令。
他们的生命周期如下图所示 (粉红代表 New, 黑色代表 Delete, 黄色代表Java类,蓝色代表C++, 绿色代表JNI).
- 如果系统支持硬件加速,ViewRootImpl首先创建一个GL20Renderer, 存在成员变量 mHardwareRenderer 里。
- Surafce是绘画的基础,如果不存在,HardwareRenderer会调用GLRenderer->createEglSurface() 创建一个新的Surface。
- Surface创建好后,接着生成Canvas,因为大部分应用程序不会直接操作Surface。
- 在Canvas的构造函数里,会依次创建Native层对应的OpenGLRenderer对象,它会直接访问库 libGLES_xxx提供的OpenGL ES API,来负责整个View树的绘制工作。
- 与此同时,一个CanvasFinalizer 成员对象也会被创建,它保存刚刚创建出来的OpenGLRenderer 指针,当它的finalizer()函数被调用是,负责将其销毁,回收资源。
- 在运行过程中,如果窗口死掉或者不在可见,ViewRootImpl 会调用DestroyHardwareResource() 来释放资源。这里会最终将底层创建的Hardware Layer回收。
- 同时Java端的GLES20Layer 对象也会因为被赋值 NULL 被GC在将来回收。
- 接下来,ViewRootImpl调用 destroyHardwareRenderer() 将之前创建的Native Renderer(DisplayListRenderer,OpenGLRenderer)依次回收。
- 最后将Java 层的mHardwareRenderer 赋空,GC将会回收最开始创建的GL20Renderer对象。支持,一个View树的生命周期完成,所有资源清楚干净。
等等!好像少了点什么,怎么没有DisplayList? 前面不是说它是性能优化的帮手之一吗?对了,上面只介绍了绘制的开始和结尾,在View的生命周期中,还有最重要的一步,Draw 还没有被介绍,DisplayList 相关的操作就是在Draw()里面完成的。
Draw 流程
绕了好大一圈,终于回到最初的话题,Android是怎样将View画出来的? 让我们按照图中的序号一一进行讲解。(黄色:Java, 绿色:C++,蓝色:JNI,粉色:New, 黑色:Delete).
- 首先,ViewRootImpl直接访问的HardwareRenderer 对象,首先在BeginFrame() 里获取EGLDisplay(用于显示) 和 初始化一个EGLSurface,OpenGL将在这个Surface上进行绘图。关于EGLDisplay 和 EGLSurface 将在 图解Android - Android GUI 系统 (3) - Surface Flinger 里详细描述。
- 我们前面说过,Android 4.0 带来的图形性能的提升,有很大程度是DisplayList 带来的,所以,HardwareRenderer接下来就通过buildDisplayList() 创建整个View树的DisplayList. 最开始,GL20Renderer为View生成了一个GLES20DisplayList, 这是一个影子类,没有实际用途,主要用来管理Native层的DisplayList的销毁。
- View 调用刚刚生成的GLES20DisplayList的 start() 方法,真正开始构建DisplayList的上下文。obtain() 函数里会判断是否mCanvas 已经存在,如果没有则New 一个新的GLES20RecordingCanvas对象。
- 创建与GLES20RecordingCanvas 一一对应的Native层的 DisplayListRenderer。
- 3,4是一个递归的过程,直到所有的View的DisplayList上下文生成,View才真正开始Draw(), 这里,OpenGL命令(Op)不会被立即执行,而是被存储到刚刚生成的DisplayListRenderer里。
- 接着View调用GLESDisplayList的 end() 方法,这里Native层的DisplayList 对象才真正被创建出来,Java 和 Native 的 DisplayList 对象一一对应起来。
- end() 方法最后,调用GLES20RecordingCanvas.recycle() 方法,首先将Native的DisplayListRender 进行重新初始化,然后将刚才创建出来的临时对象赋NULL值(GLES20RecordingCanvas 和 GLES20DisplayList), 因为它们的使命已经完成,将View的OpenGL命令存储在Native层的DisplayList对象里。
- GC() 被调用后,GLES20RecordingCanvas 和 GLES20DisplayList 被释放,相应的Finalizer 对象方法会被调用,进而调用Natice层的deleteDisplayListDefered(), 将之前不用的DisplayList 送入 Caches的Gabage 队列中,等待回收。(这是在Native层实现的类似Java GC的机制)
- 到此,所有的DisplayList 上下文准备就绪。进入preDraw 状态,在GL20Renderer的 onPreDraw() 方法里,最终调用到底层的clearGarbage将上一次绘图操作的DisplayList在底层释放。
- HardwareRenderer调用 GLES20Canvas 的drawDisplayList(), 通知 Native层的 OpenGLRenderer,其最终调用每个DisplayList的Replay() 方法执行OpenGL命令。
- 所有OpenGL命令执行完后, 图形被绘制到第一步生成的EGLSurface, hardwareRender 调用 GLES20Canvas 的 eglSwapBuffers() 交换Buffer,交由SurfaceFlinger 在下一个VSync到来时进行显示。
Hardware Layer
即便是使用了DisplayList, 对于复杂的图形,仍然需要执行大量的OpenGL命令,如果需要对这一部分进行优化,就需要使用到 HardwareLayer对绘制的图形进行缓存,如果图形不发生任何变化,就不需要执行任何OpenGL命令,而是将之前缓存在GPU内存的Buffer 直接与其他View进行合成,从而大大的提高性能。这些存储在GPU内部的Buffer就称为 Hardware Layer。除了Hardware Layer, Android 还支持Software Layer,和Hardware Layer 不同之处在于,它不存在于GPU内部,而是存在CPU的内存里,因此它不经过前面所说的 Hardware Render Pipeline, 而是走Android最初的软件Render pipeline。不管是Hardware Layer 还是 Software Layer, 在Draw() 内部均称为Cache,只要有Cache的存在,相对应的View将不用重绘,而是使用已有的Cache。Hardware Layer, Software Layer 和 Display List 是互斥的,同时只能有一种方法生效(当然,Hardware Layer的第一次绘制还是通过Display List 完成),下表总结了它们的差别, 从中可以看到,Hardware Layer 对性能的提升是最大的,唯一的问题是占用GPU的内存(这也是为什么显卡的内存变得越来越大的原因之一),所以一般来说,Hardware Layer使用在那些图片较为复杂,但不经常改变,有动画操作或与其他窗口有合成的场景,比如说WallPaper, Animation 等等。
Enabled if | GPU accelerated? | Cached in | Performance(from Google I/O 2001) |
Usage | |
Display List | Hardware Accelerated = True | Y | GPU DisplayList | 2.1 | Complex View |
Hardware Layer | LayerType = HARDWARE | Y | GPU Memory | 0.009 | Complex View, Color Filter(颜色过滤), Alpha blending (透明度设置), etc. |
Software Layer | LayerType = SOFTWARE | N | CPU Memory | 10.3 | No Hardware Accelerated, Color filter, Alpha blending, etc. |
重绘 - Invaliate
前面介绍了View的第一次绘制的过程。但是一个View在运行中终究是要发生变化的,比如说,用户在TextView的文字发生了改变,或者动画导致View的尺寸发生变化,再或者说一个对话框弹出然后又消失,被遮挡的部分重新露了出来,这些都需要对View进行重绘。在介绍重绘之前,我们先了解一下View内部的一些Flag 定义。
Flags | == 1 | Set at | Clear at |
PFFLAG_HAS_BOUNDS | 1: View has size and Position set. |
View::setFrame() | |
PFFLAG_DRAWN | 1: Has been drawn, only after which, invalidate() is valid. |
ViewGroup::addViewInLayout |
View::invalidate() |
PFFLAG_DRAWING_CACHE_VALID | 1: Has DisplayList / Hardware Layer / Software Layer |
View::GetHardwareLayer() |
View::invalidate(true) |
PFFLAG_INVALIDATED | View is specifically invalidated, not just dirty(child for instance). DisplayList will be recreated if set. |
ViewGroup::addViewInLayout ViewGroup::attachViewToParent View::force/requestLayout() View::invalidate() |
View::draw() |
PFLAG_DIRTY | Set to indicate that the view need to be redrawn. (use displaylist cache if PFFLAG_INVALIDATED flag is false) |
View::invalidate() | ViewGroup::addViewInLayout |
PFLAG_DIRTY_OPAQUE | DIRTY because of OPAQUE (hidden by others). | View::invalidateChild() | ? |
从上表可以看出,View通过内部这些Flag来控制重绘。基本上重绘分两种情况,一种是需要重新生成DisplayList, 另外一种是使用之前已有的Cache,包括DisplayList或者是Hardware Layer。使用哪种重绘方式由当前View的Flags,以及应用程序传入的参数决定。控制它的就是一组Invalidate() 函数。
void invalidate(boolean invalidateCache) { if (skipInvalidate()) { //如果View不可见,并且不再动画退出过程中(fade out),将不执行Invalidate(). return; } if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) || //DRAWN -> 已经被Draw()过 (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) || //有Cache,且被要求重新刷新Cache (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED || isOpaque() != mLastIsOpaque) //没有正在Invalidate()中 { mLastIsOpaque = isOpaque(); mPrivateFlags &= ~PFLAG_DRAWN; mPrivateFlags |= PFLAG_DIRTY; if (invalidateCache) { mPrivateFlags |= PFLAG_INVALIDATED; mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; //标记将来清除Cache,如果为false,则有系统根据Dirty Region决定是否需要重新生成DisplayList。 } final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { //系统不支持Dirty Region,必须重绘整个区域, 基本不会进去 } if (p != null && ai != null) { final Rect r = ai.mTmpInvalRect; r.set(0, 0, mRight - mLeft, mBottom - mTop); p.invalidateChild(this, r); //通知兄弟view(有共同的ViewParent(ViewGroup 或者 ViewRoot)进行 Invalidate. } } }
假如所有的条件都支持重绘,便会调用到ViewParent的invalidateChild()方法。(ViewParent是一个接口类,它的实现类是ViewGroup 和 ViewRootImpl。)这个方法会从当前View开始,向上遍历到ViewRoot 或者 到某个ViewGroup的区域与当前View的Dirty区域没有重叠为止。途中的每个ViewGroup都会被标记上Dirty。在接下来VSYNC的performDraw()里,ViewRootImpl 会遍历所有标记Dirty的ViewGroup,然后找到里面标记Dirty的View,只有这些View的DisplayList 被重建,而其他实际上没有变化的View(虽然它们在同一个ViewGroup里面),如果没有Hardware Layer, 只需重新执行对应Display List 里面的OpenGL 命令。通过这种方式,Android只重绘需要重绘的View,从软件层面将GPU的输入最小化,从而优化图形性能。
4. Windows 的管理
到此,我们已经了解了一个Acitivty(Window)是如何画出来的,让我们在简要重温一下这个过程:
- Acitivity创建, ViewRootImpl将窗口注册到WindowManager Service,WindowManager Service 通过SurfaceFlinger 的接口创建了一个Surface Session用于接下来的Surface 管理工作。
- 由Surface Flinger 传上来的VSYNC事件到来,Choreographer 会运行ViewRootImpl 注册的Callback函数, 这个函数会最终调用 performTraversal 遍历View树里的每个View, 在第一个VSYNC里,WindowManager Service 会创建一个SurafceControll 对象,ViewRootImpl 根据Parcel返回的该对象生成了Window对应的Surface对象,通过这个对象,Canvas 可以要求Sruface Flinger 分配OpenGL绘图用的Buffer。
- View树里的每个View 会根据需要依次执行 measure(),layout() 和 draw() 操作。Android 在3.0之后引入了硬件加速机制,为每个View生成DisplayList,并根据需要在GPU内部生成Hardware Layer,从而充分利用GPU的功能提升图形绘制速度。
- 当某个View发生变化,它会调用invalidate() 请求重绘,这个函数从当前View 出发,向上遍历找到View Tree中所有Dirty的 View 和 ViewGroup, 根据需要重新生成DisplayList, 并在drawDisplayList() 函数里执行OpenGL命令将其绘制在某个Surface Buffer上。
- 最后,ViewRootImpl 调用 eglSwapBuffer 通知OpenGL 将绘制的Buffer 在下一个VSync点进行显示。
注意的是,上面讨论的只是一个窗口的流程,而Android是个多窗口的系统,窗口之间可能会有重叠,窗口切换会有动画产生,窗口的显示和隐藏都有可能会导致资源的分配和释放,这一切需要有一个全局的服务进行统一的管理,这个服务就是我们大名鼎鼎的Window Manager Service (简写 WMS).
其实Window Manager Service 的工作不仅仅是管理窗口,还会跟很多其他服务打交道,如 InputManager Service, AcitivityManager Service 等等,但本章只讨论它在Window Manager 方面的工作,下图中红色标记部分。
Layout
首先来看Layout。Layout 是Window Manager Service 重要工作之一,它的流程如下图所示:
- 每个View将期望窗口尺寸交给WMS(WindowManager Service).
- WMS 将所有的窗口大小以及当前的Overscan区域传给WPM (WindowPolicy Manager).
- WPM根据用户配置确定每个Window在最终Display输出上的位置以及需要分配的Surface大小。
- 返回这些信息给每个View,他们将在给会的区域空间里绘图。
Android里定义了很多区域,如下图所示
Overscan:
Overscan 是电视特有的概念,上图中黄色部分就是Overscan区域,指的是电视机屏幕四周某些不可见的区域(因为电视特性,这部分区域的buffer内容显示时被丢弃),也意味着如果窗口的某些内容画在这个区域里,它在某些电视上就会看不到。为了避免这种情况发生,通常要求UI不要画在屏幕的边角上,而是预留一定的空间。因为Overscan的区域大小随着电视不 同而不同,它一般由终端用户通过UI指定,(比如说GoogleTV里就有确定Overscan大小的应用)。
OverscanScreen, Screen:
OverscanScreen 是包含Overscan区域的屏幕大小,而Screen则为去除Overscan区域后的屏幕区域, OverscanScreen > Screen.
Restricted and Unrestricted:
某些区域是被系统保留的,比如说手机屏幕上方的状态栏(如图纸绿色区域)和下方的导航栏,根据是否包括这些预留的区域,Android把区域分为Unrestricted Area 和 Resctrited Aread, 前者包括这部分预留区域,后者则不包含, Unrestricted area > Rectricted area。
mFrame, mDisplayFrame, mContainingFrame
Frame指的是一片内存区域, 对应于屏幕上的一块矩形区域. mFrame的大小就是Surface的大小, 如上上图中的蓝色区域. mDisplayFrame 和 mContainingFrame 一般和mFrame 大小一致. mXXX 是Window(ViewRootImpl, Windowstate) 里面定义的成员变量.
mContentFrame, mVisibleFrame
一个Surface的所有内容不一定在屏幕上都得到显示, 与Overscan重叠的部分会被截掉, 系统的其他窗口也会遮挡掉部分区域 (比如短信窗口,ContentFrame是800x600(没有Status Bar), 但当输入法窗口弹出是,变成了800x352), 剩下的区域称为Visible Frame, UI内容只有画在这个区域里才能确保可见. 所以也称为Content Frame. mXXX也是Window(ViewRootImpl, WindowState) 里面定义的成员变量.
Insects
insets的定义如上图所示, 用了表示某个Frame的边缘大小.
Layout 在WMS 内部的时序如下图所示,外部调整Overscan参数或View内部主动调用requestLayout() 都会触发WMS的重新layout,layout完成后,WMS会通过IWindow的resized()接口通知ViewRoot, 最终会调用requestLayout(), 并在下一个VSYNC 事件到来时更新。
计算Layout主要有图中三个红色的函数完成,它们代码很多,涉及到很多计算,但只要对着我们上面给的三个图来看,不难看出它的意思,本文将不详细深入。
Animation
Animation的原理很简单,就是定时重绘图形。下面的类图中给出了Android跟Animation相关的类。
Animation:
Animation抽象类,里面最重要的一个接口就是applyTranformation, 它的输入是当前的一个描述进度的浮点数(0.0 ~ 1.0), 输出是一个Transformation类对象,这个对象里有两个重要的成员变量,mAlpha 和 mMatrix, 前者表示下一个动画点的透明度(用于灰度渐变效果),后者则是一个变形矩阵,通过它可以生成各种各样的变形效果。Android提供了很多Animation的具体实现,比如RotationAnimation, AlphaAnimation 等等,用户也可以实现自己的Animation类,只需要重载applyTransform 这个接口。注意,Animation类只生成绘制动画所需的参数(alpha 或 matrix),不负责完成绘制工作。完成这个工作的是Animator.
Animator:
控制动画的‘人’, 它通常通过向定时器Choreographer 注册一个Runnable对象来实现定时触发,在回调函数里它要做两件事情:1. 从Animation那里获取新的Transform, 2. 将Transform里的值更新底层参数,为接下来的重绘做准备。动画可以发生在Window上,也可以发生在某个具体的View。前者的动画会通过SurfaceControl直接在某个Surface上进行操作(会在SurfaceFlinger里详细描述),比如设置Alpha值。后者则通过OpenGL完成(生成我们前面提过的DisplayList).
WindowStateAnimator, WindowAnimator, AppWindowAnimator:
针对不同对象的Animator. WindowAnimator, 负责整个屏幕的动画,比如说转屏,它提供Runnable实现。WindowStateAnimator, 负责ViewRoot,即某一个窗口的动画。AppWindowAnimator, 负责应用启动和退出时候的动画。这几个Animator都会提供一个函数,stepAnimationLocked(), 它会完成一个动画动作的一系列工作,从计算Transformation到更新Surface的Matrix.
具体来看一下Window的Animation和View的Animation
- WindowManagerService 的 scheduleAnimationLocked() 将windowAnimator的mAnimationRunnable 注册到定时器 Choreographer.
- 如果应用程序的res/anim/下有xml文件定义animation,在layout过程中,会通过appTransition类的loadAnimation() 函数将XML转换成 Animation_Set 对象,它里面可以包含多个Animation。
- 当下一个VSYNC事件到来,刚才注册的Callback函数被调用,即WindowAnimator的mAnimationRunnable,里面调用 animateLocked(), 首先,打开一个SurfaceControl的动画会话,animationSession。
- 首先执行的动画是 appWindowAnimator, 如果刚才loadAnimation() 返回的animation不为空,便会走到Animation的getTransform() 获取动画的参数,这里可能会同时有多个动画存在,通过Transform的compose()函数将它们最终合为一个。
- 接下来上场的是DisplayContentsAnimator, 它主要用来实现灰度渐变和转屏动画。同样,首先通过stepAnimation() 获取动画变形参数,然后通过SurfaceControl将其更新到SrufaceFlinger内部对应的Layer. 这里首先完成的是转屏的动画
- 然后就是每个窗口的动画。后面跟着的 perpareSurfaceLocked() 则会更新参数。
- Wallpaper的动画。
- 接下来,就是上面提到的DisplayContentsAnimator的第二部分,通过DimLayer实现渐变效果。
- Surface的控制完成后,关闭对话。然后scheduleAnimationLocked() 规划下一步动画。
- 接下来的performDraw()会把所有更新参数的View,或Surface交给OpenGL或HWcomposer进行处理,于是我们就看到了动画效果。
View 的动画实现步骤与Windows 类似,有兴趣的同学可以去看View.java 的 drawAnimation() 函数。
管理窗口
WMS 里面管理着各式各样的窗口, 如下表所示(在WindowManagerService.java 中定义)
类型 | 用途 | |
mAnimatingAppToken | ArrayList |
正在动画中的应用 |
mExistingAppToken | ArrayList |
退出但退出动画还没有完成的应用。 |
mResizingWindows | ArrayList |
尺寸正在改变的窗口,当改变完成后,需要通知应用。 |
mFinishedStarting | ArrayList<AppWindowToken> | 已经完成启动的应用。 |
mPendingRemove | ArrayList |
动画结束的窗口。 |
mLosingFocus | ArrayList |
失去焦点的窗口,等待获得焦点的窗口进行显示。 |
mDestorySurface | ArrayList |
需要释放Surface的窗口。 |
mForceRemoves | ArrayList |
需要强行关闭的窗口,以释放内存。 |
mWaitingForDrawn | ArrayList |
等待绘制的窗口 |
mRelayoutWhileAnimating | ArrayList |
请求relayout但此时仍然在动画中的窗口。 |
mStrictModeFlash | StrictModeFlash | 一个红色的背景窗口,用于提示可能存在的内存泄露。 |
mCurrentFocus | WindowState | 当前焦点窗口 |
mLastFocus | WindowState | 上一焦点窗口 |
mInputMethodTarget | WindowState | 输入法窗口下面的窗口。 |
mInputMethodWindow | WindowState | 输入法窗口 |
mWallpaperTarget | WindowState | 墙纸窗口 |
mLowerWallpaperTarget | WindowState | 墙纸切换动画过程中Z-Order 在下面的窗口 |
mHigherWallpaperTarget | WindowState | 墙纸切换动画过程中Z-Order 在上面的窗口 |
可以看到这里大量的用到了队列,不同的窗口,或同一窗口在不同的阶段,可能会出现在不同的队列里。另外因为WindowManager Service 的服务可能被很多个线程同时调用,在这种复杂的多线程环境里,通过锁来实现线程安全非常难以实现,一不小心就可能导致死锁,所以在 WindowManager 内专门有一个执行线程(WM Thread)来将所有的服务请求通过消息进行异步处理,实现调用的序列化。队列是实现异步处理的常用手段。队列加Looper线程是Android 应用常用的设计模型。
此外,WindowManager还根据Window的类型进行了分类(在WindowManager.java),如下表,
类型 | 常量范围 | 子类 | 常量值 | 说明 | 例子 |
APPLICATION_WINDOW | 1~99 | TYPE_BASE_APPLICATION | 1 | ||
TYPE_APPLICATION | 2 | 应用窗口 | 大部分的应用程序窗口 | ||
TYPE_APPLICATION_STARTING | 3 | 应用程序的Activity显示之前由系统显示的窗口 | |||
LAST_APPLICATION_WINDOW | 99 | ||||
SUB_WINDOW | 1000~1999 | FIRST_SUB_WINDOW | 1000 | ||
TYPE_APPLICATION_PANEL | 1000 | 显示在母窗口之上,遮挡其下面的应用窗口。 | |||
TYPE_APPLICATION_MEDIA | 1001 | 显示在母窗口之下,如果应用窗口不挖洞,即不可见。 | SurfaceView,在小窗口显示时设为MEDIA, 全屏显示时设为PANEL | ||
TYPE_APPLICATION_SUB_PANEL | 1002 | ||||
TYPE_APPLICATION_ATTACHED_DIALOG | 1003 | ||||
TYPE_APPLICATION_MEIDA_OVERLAY | 1004 | 用于两个SurfaceView的合成,如果设为MEDIA, 则上面的SurfaceView 挡住下面的SurfaceView |
|||
SYSTEM_WINDOW | 2000~2999 | TYPE_STATUS_BAR | 2000 | 顶部的状态栏 | |
TYPE_SEARCH_BAR | 2001 | 搜索窗口,系统中只能有一个搜索窗口 | |||
TYPE_PHONE | 2002 | 电话窗口 | |||
TYPE_SYSTEM_ALERT | 2003 | 警告窗口,在所有其他窗口之上显示 | 电量不足提醒窗口 | ||
TYPE_KEYGUARD | 2004 | 锁屏界面 | |||
TYPE_TOAST | 2005 | 短时的文字提醒小窗口 | |||
TYPE_SYSTEM_OVERLAY | 2006 | 没有焦点的浮动窗口 | |||
TYPE_PRIORITY_PHONE | 2007 | 紧急电话窗口,可以显示在屏保之上 | |||
TYPE_SYSTEM_DIALOG | 2008 | 系统信息弹出窗口 | 比如SIM插上后弹出的运营商信息窗口 | ||
TYPE_KEYGUARD_DIALOG | 2009 | 跟KeyGuard绑定的弹出对话框 | 锁屏时的滑动解锁窗口 | ||
TYPE_SYSTEM_ERROR | 2010 | 系统错误提示窗口 | ANR 窗口 | ||
TYPE_INPUT_METHOD | 2011 | 输入法窗口,会挤占当前应用的空间 | |||
TYPE_INPUT_METHOD_DIALOG | 2012 | 弹出的输入法窗口,不会挤占当前应用窗口空间,在其之上显示 | |||
TYPE_WALLPAPER | 2013 | 墙纸 | |||
TYPE_STATUS_BAR_PANEL | 2014 | 从状态条下拉的窗口 | |||
TYPE_SECURE_SYSTEM_OVERLAY | 2015 | 只有系统用户可以创建的OVERLAY窗口 | |||
TYPE_DRAG | 2016 | 浮动的可拖动窗口 | 360安全卫士的浮动精灵 | ||
TYPE_STATUS_BAR_PANEL | 2017 | ||||
TYPE_POINTER | 2018 | 光标 | |||
TYPE_NAVIGATION_BAR | 2019 | ||||
TYPE_VOLUME_OVERLAY | 2020 | 音量调节窗口 | |||
TYPE_BOOT_PROGRESS | 2021 | 启动进度,在所有窗口之上 | |||
TYPE_HIDDEN_NAV_CONSUMER | 2022 | 隐藏的导航栏 | |||
TYPE_DREAM | 2023 | 屏保动画 | |||
TYPE_NAVIGATION_BAR_PANEL | 2024 | Navigation bar 弹出的窗口 | 比如说应用收集栏 | ||
TYPE_UNIVERSAL_BACKGROUND | 2025 | ||||
TYPE_DISPLAY_OVERLAY | 2026 | 用于模拟第二显示设备 | |||
TYPE_MAGNIFICATION | 2027 | 用于放大局部 | |||
TYPE_RECENTS_OVERLAY | 2028 | 当前应用窗口,多用户情况下只显示在用户节目 |
windowManager Service 会根据窗口的类型值来决定Z-Order (于常量值无关,值大说明是后面Android版本添加的,比如说2025~2028就是4.3 新加的)。比如说SurfaceView.java 里的一个函数,
public void setZOrderOnTop(boolean onTop) { if (onTop) { mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; //PANEL在上面 // ensures the surface is placed below the IME mLayout.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } else { mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; //MEDIA类型窗口在应用窗口之下,应用必需挖洞(设Alpha值)才能露出它。 mLayout.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } }
这些类型最终在WindowManager 内部转换成几个Z-Order 值,mBaseLayer, mSubLayer, mAnimationLayer, 分别表明主窗口,子窗口(附加在主窗口之上),和动画窗口的Z-Order值(越大越在上边)。不同的窗口类型在不同的硬件产品上有不同的定义,因此它是实现在WindowManagerPolicy里的windowTypeToLayerLw(), 举PhoneWindowManager 为例,它的ZOrder 顺序是:
Univese background < Wallpaper < Phone < Search Bar < System Dialog < Input Method Window < Keyguard < Volume < System Overlay < Navigation < System Error < < Display Overlay< Drag < Pointer < Hidden NAV consumer,
所以,我们如果要在手机锁屏时显示歌曲播放进度,就必须给这个窗口分配一个大于Keyguard的type,如 system overlay 等。
一个Window可以有若干个Sub Window, 他们和主窗口的ZOrder关系是
Media Sublayer(-2) < Media Overlay sublayer (-1) < Main Layer(0) < Attached Dialog (1) < Sub panel Sublayer (2)
通过 "adb shell dumpsys window" 可以查看系统当前运行的窗口的ZOrder 和 Visibility, 比如下面就是在短信输入界面下运行“dumpsys" 获得的结果,
1 Window #0 Window{4ea4e178 u0 Keyguard}: 2 mBaseLayer=121000 mSubLayer=0 mAnimLayer=121000+0=121000 mLastLayer=121000 3 mViewVisibility=0x8 mHaveFrame=true mObscured=false 4 Window #1 Window{4ea4aa7c u0 InputMethod}: 5 mBaseLayer=101000 mSubLayer=0 mAnimLayer=21020+0=21020 mLastLayer=21020 6 mViewVisibility=0x0 mHaveFrame=true mObscured=false 7 Window #2 Window{4ec1a150 u0 com.android.mms/com.android.mms.ui.ComposeMessageActivity}: 8 mBaseLayer=21000 mSubLayer=0 mAnimLayer=21015+0=21015 mLastLayer=21015 9 mViewVisibility=0x0 mHaveFrame=true mObscured=false 10 Window #3 Window{4ea7c714 u0 com.android.mms/com.android.mms.ui.ConversationList}: 11 mBaseLayer=21000 mSubLayer=0 mAnimLayer=21010+0=21010 mLastLayer=21015 12 mViewVisibility=0x8 mHaveFrame=true mObscured=true 13 Window #4 Window{4eaedefc u0 com.android.launcher/com.android.launcher2.Launcher}: 14 mBaseLayer=21000 mSubLayer=0 mAnimLayer=21005+0=21005 mLastLayer=21010 15 mViewVisibility=0x8 mHaveFrame=true mObscured=true 16 Window #5 Window{4ea17064 u0 jackpal.androidterm/jackpal.androidterm.Term}: 17 mBaseLayer=21000 mSubLayer=0 mAnimLayer=21000+0=21000 mLastLayer=22000 18 mViewVisibility=0x8 mHaveFrame=true mObscured=true
可以看到:
- 当前只有两个窗口可见, InputMethod 和 com.android.mms/com.android.mms.ui.ComposeMessageActivity, mViewVisibility = 0 (0在View.java定义是可见), 而其他 mViewVisibility=0x8 (定义在View.java里, 意思是”GONE").
- InputMethod(mLastLayer=21020) 在 ComposeMessageActivity(mLastLayer=21015) 之上. 细心的同学可能会发现,InputMethod的mBaseLayer = 101000,为什么mLastLayer小那么多?因为mLastLayer才是真正的z-order, 它经过了WidowManager的调整。当用户点击输入框,View会通过InputMethodManager 发送一个showSoftInput命令,经过InputManagerService的处理,输入法窗口(KeyboardView)会被加入到WndowManager Service里,WindowManager Service 会寻找它的目标窗口, 即需要输入的窗口,(遍历WindowList 然后根据窗口的Flags判断),然后将输入法窗口的mLayer值改为 目标窗口的mLayer + 5,这样,输入法窗口就显示在了目标窗口之上。在这里,输入法窗口存在于InputMethodManagerService 的上下文里,而不是某个Activity,所以他可以跟任何需要输入法的Activity绑定。其他一些应用,比如说PiP(三星的Galaxy S3可以在一个浮动的小窗口里显示视频)也是运用了类似的方法来实现的。Android的输入法是一个非常值得研究的模块,留到后面探讨。
所以,WindowManager Service 是通过调整窗口的mViewVisibility 和 mLayer 值来实现窗口重叠。最后给出跟Z-order相关的类图。
图中序号表示输入法窗口找到它的目标窗口的过程:
- WindowManagerService 找到默认输出(Default Display) 的DisplayContents成员变量。
- 里面有一个数组WindowList-mWindows, 按照Z-Order顺序保存了当前在这个Display上输出的所有窗口WindowState。
- 遍历所有的WindowState,判断它的mAppToken是否和输入法窗口的mAppToken一致。呼起输入法窗口的窗口会将自己的mAppToken拷贝给它。
- 相同的Token下,可能有多个窗口,通过WindowToken.windows 或者 AppWindowToken.allAppWindows, 可以找到他们。
WindowManager Service的介绍暂告一段落,它与其他重要的Service,SurfaceFlinger, ActivityManager, InputManager, PowerManager, WatchDog 之间的关系将在其他文章介绍。