1.初识WindowManagerService
WindowManagerService(以下简称WMS)是继ActivityManagerService与PackageManagerService之后又一个复杂却十分重要的系统服务。
在介绍WMS之前,首先要了解窗口(Window)是什么。
Android系统中的窗口是屏幕上的一块用于绘制各种UI元素并可以响应应用户输入的一个矩形区域。从原理上来讲,窗口的概念是独自占有一个Surface实例的显示区域。例如Dialog、Activity的界面、壁纸、状态栏以及Toast等都是窗口。
《卷I》第8章曾详细介绍了一个Activity通过Surface来显示自己的过程:
- Surface是一块画布,应用可以随心所欲地通过Canvas或者OpenGL在其上作画。
- 然后通过SurfaceFlinger将多块Surface的内容按照特定的顺序(Z-order)进行混合并输出到FrameBuffer,从而将Android"漂亮的脸蛋"显示给用户。
既然每个窗口都有一块Surface供自己涂鸦,必然需要一个角色对所有窗口的Surface进行协调管理。于是,WMS便应运而生。WMS为所有窗口分配Surface,掌管Surface的显示顺序(Z-order)以及位置尺寸,控制窗口动画,并且还是输入系统的一重要的中转站。
说明:一个窗口拥有显示和响应用户输入这两层含义,本章将侧重于分析窗口的显示,而响应用户输入的过程则在第5章进行详细的介绍。
本章将深入分析WMS的两个基础子系统的工作原理:
- 布局系统(Layout System),计算与管理窗口的位置、层次。
- 动画系统(Animation System),根据布局系统计算的窗口位置与层次渲染窗口动画。
1.输入系统和WMS的关系简介
输入事件的源头是位于/dev/input/下的设备节点,而输入系统的终点是由WMS管理的某个窗口。最初的输入事件为内核生成的原始事件,而最终交付给窗口的则是KeyEvent或MotionEvent对象。因此Android输入系统的主要工作是读取设备节点中的原始事件,将其加工封装,然后派发给一个特定的窗口以及窗口中的控件。这个过程由InputManagerService(以下简称IMS)系统服务为核心的多个参与者共同完成。
输入系统的总体流程和参与者如图所示。
图中描述了输入事件的处理流程以及输入系统中最基本的参与者。它们是:
-
Linux内核,接受输入设备的中断,并将原始事件的数据写入到设备节点中。
-
设备节点,作为内核与IMS的桥梁,它将原始事件的数据暴露给用户空间,以便IMS可以从中读取事件。
-
InputManagerService,一个Android系统服务,它分为Java层和Native层两部分。Java层负责与WMS的通信。而Native层则是InputReader和InputDispatcher两个输入系统关键组件的运行容器。
-
EventHub,直接访问所有的设备节点。并且正如其名字所描述的,它通过一个名为getEvents()的函数将所有输入系统相关的待处理的底层事件返回给使用者。这些事件包括原始输入事件、设备节点的增删等。
-
InputReader,是IMS中的关键组件之一。它运行于一个独立的线程中,负责管理输入设备的列表与配置,以及进行输入事件的加工处理。它通过其线程循环不断地通过getEvents()函数从EventHub中将事件取出并进行处理。对于设备节点的增删事件,它会更新输入设备列表于配置。对于原始输入事件,InputReader对其进行翻译、组装、封装为包含了更多信息、更具可读性的输入事件,然后交给InputDispatcher进行派发。
-
InputReaderPolicy,它为InputReader的事件加工处理提供一些策略配置,例如键盘布局信息等。
-
InputDispatcher,是IMS中的另一个关键组件。它也运行于一个独立的线程中。InputDispatcher中保管了来自WMS的所有窗口的信息,其收到来自InputReader的输入事件后,会在其保管的窗口中寻找合适的窗口,并将事件派发给此窗口。
-
InputDispatcherPolicy,它为InputDispatcher的派发过程提供策略控制。例如截取某些特定的输入事件用作特殊用途,或者阻止将某些事件派发给目标窗口。一个典型的例子就是HOME键被InputDispatcherPolicy截取到PhoneWindowManager中进行处理,并阻止窗口收到HOME键按下的事件。
-
WMS,虽说不是输入系统中的一员,但是它却对InputDispatcher的正常工作起到了至关重要的作用。当新建窗口时,WMS为新窗口和IMS创建了事件传递所用的通道。另外,WMS还将所有窗口的信息,包括窗口的可点击区域,焦点窗口等信息,实时地更新到IMS的InputDispatcher中,使得InputDispatcher可以正确地将事件派发到指定的窗口。
-
ViewRootImpl,对于某些窗口,如壁纸窗口、SurfaceView的窗口来说,窗口即是输入事件派发的终点。而对于其他的如Activity、对话框等使用了Android控件系统的窗口来说,输入事件的终点是控件(View)。ViewRootImpl将窗口所接收到的输入事件沿着控件树将事件派发给感兴趣的控件。
简单来说,内核将原始事件写入到设备节点中,InputReader不断地通过EventHub将原始事件取出来并翻译加工成Android输入事件,然后交给InputDispatcher。InputDispatcher根据WMS提供的窗口信息将事件交给合适的窗口。窗口的ViewRootImpl对象再沿着控件树将事件派发给感兴趣的控件。控件对其收到的事件作出响应,更新自己的画面、执行特定的动作。所有这些参与者以IMS为核心,构建了Android庞大而复杂的输入体系。
2.一个从命令行启动的动画窗口
1.SampleWindow的实现
在这一节里将编写一个最简单的Java程序SampleWindow,仅使用WMS的接口创建并渲染一个动画窗口。此程序将抛开Activity、Wallpaper等UI架构的复杂性,直接了当地揭示WMS的客户端如何申请、渲染并注销自己的窗口。同时这也初步地反应了WMS的工作方式。
这个例子很简单,只有三个文件:
- SampleWindow.java 主程序源代码。
- Android.mk 编译脚本。
- sw.sh 启动器。
分别看一下这三个文件的实现:
[-->SampleWindow.java::SampleWindow]
public class SampleWindow { public static void main(String[] args) { try { //SampleWindow.Run()是这个程序的主入口 new SampleWindow().Run(); } catch (Exception e) { e.printStackTrace(); } } //IWindowSession 是客户端向WMS请求窗口操作的中间代理,并且是进程唯一的 IWindowSession mSession = null; //InputChannel 是窗口接收用户输入事件的管道。在第5章中将对其进行详细的探讨 InputChannel mInputChannel = new InputChannel(); // 下面的三个Rect保存了窗口的布局结果。其中mFrame表示了窗口在屏幕上的位置与尺寸 // 在4.4中将详细介绍它们的作用以及计算原理 Rect mInsets = new Rect(); Rect mFrame = new Rect(); Rect mVisibleInsets = new Rect(); Configuration mConfig = new Configuration(); // 窗口的Surface,在此Surface上进行的绘制都将在此窗口上显示出来 Surface mSurface = new Surface(); // 用于在窗口上进行绘图的画刷 Paint mPaint = new Paint(); // 添加窗口所需的令牌,在4.2节将会对其进行介绍 IBinder mToken = new Binder(); // 一个窗口对象,本例演示了如何将此窗口添加到WMS中,并在其上进行绘制操作 MyWindow mWindow = new MyWindow(); //WindowManager.LayoutParams定义了窗口的布局属性,包括位置、尺寸以及窗口类型等 LayoutParams mLp = new LayoutParams(); Choreographer mChoreographer = null; //InputHandler 用于从InputChannel接收按键事件做出响应 InputHandler mInputHandler = null; boolean mContinueAnime = true; public void Run() throws Exception { Looper.prepare(); // 获取WMS服务 // IWindowManager wms = IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE)); IWindowManager wms = WindowManagerGlobal.getWindowManagerService(); // 通过WindowManagerGlobal获取进程唯一的IWindowSession实例。它将用于向WMS // 发送请求。注意这个函数在较早的Android版本(如4.1)位于ViewRootImpl类中 mSession = WindowManagerGlobal.getWindowSession(Looper.myLooper()); // 获取屏幕分辨率 IDisplayManager dm = IDisplayManager.Stub.asInterface(ServiceManager.getService(Context.DISPLAY_SERVICE)); DisplayInfo di = dm.getDisplayInfo(Display.DEFAULT_DISPLAY); Point scrnSize = new Point(di.appWidth, di.appHeight); // 初始化WindowManager.LayoutParams initLayoutParams(scrnSize); // 将新窗口添加到WMS installWindow(wms); // 初始化Choreographer的实例,此实例为线程唯一。这个类的用法与Handler // 类似,不过它总是在VSYC同步时回调,所以比Handler更适合做动画的循环器[1] mChoreographer = Choreographer.getInstance(); // 开始处理第一帧的动画 scheduleNextFrame(); // 当前线程陷入消息循环,直到Looper.quit() Looper.loop(); // 标记不要继续绘制动画帧 mContinueAnime = false; // 卸载当前Window uninstallWindow(wms); } public void initLayoutParams(Point screenSize) { // 标记即将安装的窗口类型为SYSTEM_ALERT,这将使得窗口的ZOrder顺序比较靠前 mLp.type = LayoutParams.TYPE_SYSTEM_ALERT; mLp.setTitle("SampleWindow"); // 设定窗口的左上角坐标以及高度和宽度 mLp.gravity = Gravity.LEFT | Gravity.TOP; mLp.x = screenSize.x / 4; mLp.y = screenSize.y / 4; mLp.width = screenSize.x / 2; mLp.height = screenSize.y / 2; // 和输入事件相关的Flag,希望当输入事件发生在此窗口之外时,其他窗口也可以接受输入事件, // 即没有这个flag时,窗口之外的输入事件还是会传递给此窗口,后边的窗口无法接收到 mLp.flags = mLp.flags | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; } public void installWindow(IWindowManager wms) throws Exception { // 首先向WMS声明一个Token,任何一个Window都需要隶属与一个特定类型的Token wms.addWindowToken(mToken, WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); // 设置窗口所隶属的Token mLp.token = mToken; /*通过IWindowSession将窗口安装进WMS,注意,此时仅仅是安装到WMS,本例的Window 目前仍然没有有效的Surface。不过,经过这个调用后,mInputChannel已经可以用来接受 输入事件了*/ mSession.add(mWindow, 0, mLp, View.VISIBLE, mInsets, mInputChannel); /*通过IWindowSession要求WMS对本窗口进行重新布局,经过这个操作后,WMS将会为窗口 创建一块用于绘制的Surface并保存在参数mSurface中。同时,这个Surface被WMS放置在 LayoutParams所指定的位置上 */ mSession.relayout(mWindow, 0, mLp, mLp.width, mLp.height, View.VISIBLE, 0, mFrame, mInsets, mVisibleInsets, mConfig, mSurface); if (!mSurface.isValid()) { throw new RuntimeException("Failed creating Surface."); } // 基于WMS返回的InputChannel创建一个Handler,用于监听输入事件mInputHandler一旦被创建,就已经在监听输入事件了 mInputHandler = new InputHandler(mInputChannel, Looper.myLooper()); } public void uninstallWindow(IWindowManager wms) throws Exception { // 从WMS处卸载窗口 mSession.remove(mWindow); // 从WMS处移除之前添加的Token wms.removeWindowToken(mToken); } public void scheduleNextFrame() { // 要求在显示系统刷新下一帧时回调mFrameRender,注意,只回调一次 mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, mFrameRender, null); } // 这个Runnable对象用以在窗口上描绘一帧 public Runnable mFrameRender = new Runnable() { @Override public void run() { try { // 获取当期时间戳 long time = mChoreographer.getFrameTime() % 1000; // 绘图 if (mSurface.isValid()) { Canvas canvas = mSurface.lockCanvas(null); canvas.drawColor(Color.DKGRAY); canvas.drawRect(2 * mLp.width * time / 1000 - mLp.width, 0, 2 * mLp.width * time / 1000, mLp.height, mPaint); mSurface.unlockCanvasAndPost(canvas); mSession.finishDrawing(mWindow); } if (mContinueAnime) scheduleNextFrame(); } catch (Exception e) { e.printStackTrace(); } } }; // 定义一个类继承InputEventReceiver,用以在其onInputEvent()函数中接收窗口的输入事件 class InputHandler extends InputEventReceiver { Looper mLooper = null; public InputHandler(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); mLooper = looper; } @Override public void onInputEvent(InputEvent event) { if (event instanceof MotionEvent) { MotionEvent me = (MotionEvent) event; if (me.getAction() == MotionEvent.ACTION_UP) { // 退出程序 mLooper.quit(); } } super.onInputEvent(event); } } // 实现一个继承自IWindow.Stub的类MyWindow。 class MyWindow extends IWindow.Stub { // 保持默认的实现即可 } }
由于此程序使用了大量的隐藏API(即SDK中没有定义这些API),因此需要放在Android源码环境中进行编译它。对应的Android.mk如下:
[-->Android.mk]
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE := samplewindow
include $(BUILD_JAVA_LIBRARY)
将这两个文件放在$TOP/frameworks/base/cmds/samplewindow/下,然后用make或mm命令进行编译。最终生成的结果是samplewindow.jar,文件位置在out/target/<ProductName>/system/framework/下。将该文件通过adb push到手机的/system/framework/下。
提示:读者可使用Android4.2模拟器来运行此程序。
然而,samplewindow.jar不是一个可执行程序,故需借助Android的app_process工具来加载并执行它。笔者编写了一个脚本做为启动器:
[-->sw.sh]
base=/system
export CLASSPATH=$base/framework/samplewindow.jar
exec app_process $base/binunderstanding.wms.samplewindow.SampleWindow "$@"
注意:app_process其实就是大名鼎鼎的zygote。不过,只有使用--zygote参数启动时它才会给改名为zygote[2],否则就像java –jar命令一样,运行指定类的main静态函数。
在手机中执行该脚本,其运行结果是一个灰色的方块不断地从屏幕左侧移动到右侧,如图4-1所示。
2.初识窗口的创建、绘制与销毁
SampleWindow的这段代码虽然简单,但是却很好地提炼了一个窗口的创建、绘制以及销毁的过程。注意,本例没有使用任何 WMS以外的系统服务,也没有使用Android系统四大组件的框架,也就是说,如果你愿意,可以利用WMS实现自己的UI与应用程序框架,这样就可以衍生出一个新的平台了。
总结在客户端创建一个窗口的步骤:
- 获取IWindowSession和WMS实例。客户端可以通过IWindowSession向WMS发送请求。
- 创建并初始化WindowManager.LayoutParams。注意这里是WindowManager下的LayoutParams,它继承自ViewGroup.LayoutParams类,并扩展了一些窗口相关的属性。其中最重要的是type属性。这个属性描述了窗口的类型,而窗口类型正是WMS对多个窗口进行ZOrder排序的依据。
- 向WMS添加一个窗口令牌(WindowToken)。本章后续将分析窗口令牌的概念,目前读者只要知道,窗口令牌描述了一个显示行为,并且WMS要求每一个窗口必须隶属于某一个显示令牌。
- 向WMS添加一个窗口。必须在LayoutParams中指明此窗口所隶属于的窗口令牌,否则在某些情况下添加操作会失败。在SampleWindow中,不设置令牌也可成功完成添加操作,因为窗口的类型被设为TYPE_SYSTEM_ALERT,它是系统窗口的一种。而对于系统窗口,WMS会自动为其创建显示令牌,故无需客户端操心。此话题将会在后文进行更具体的讨论。
- 向WMS申请对窗口进行重新布局(relayout)。所谓的重新布局,就是根据窗口新的属性去调整其Surface相关的属性,或者重新创建一个Surface(例如窗口尺寸变化导致之前的Surface不满足要求)。向WMS添加一个窗口之后,其仅仅是将它在WMS中进行了注册而已。只有经过重新布局之后,窗口才拥有WMS为其分配的画布。有了画布,窗口之后就可以随时进行绘制工作了。
而窗口的绘制过程如下:
- 通过Surface.lock()函数获取可以在其上作画的Canvas实例。
- 使用Canvas实例进行作画。
- 通过Surface.unlockCanvasAndPost()函数提交绘制结果。
提示:关于Surface的原理与使用方法,请参考《卷 I》第8章"深入理解Surface系统"。
这是对Surface作画的标准方法。在客户端也可以通过OpenGL进行作画,不过这超出了本书的讨论范围。另外,在SampleWindow例子中使用了Choreographer类进行了动画帧的安排。Choreographer意为编舞指导,是Jelly Bean新增的一个工具类。其用法与Handler的post()函数非Z且不会再显示新的窗口,则需要从WMS将之前添加的显示令牌一并删除。
3.窗口的概念!!!
在SampleWindow例子中,有一个名为mWindow(类型为IWindow)的变量。读者可能会理所当然地认为它就是窗口了。其实这种认识并不完全正确。IWindow继承自Binder,并且其Bn端位于应用程序一侧(在例子中IWindow的实现类MyWindow就继承自IWindow.Stub),于是其在WMS一侧只能作为一个回调,以及起到窗口Id的作用。
那么,窗口的本质是什么呢?
是进行绘制所使用的画布:Surface。
当一块Surface显示在屏幕上时,就是用户所看到的窗口了。客户端向WMS添加一个窗口的过程,其实就是WMS为其分配一块Surface的过程,一块块Surface在WMS的管理之下有序地排布在屏幕上,Android才得以呈现出多姿多彩的界面来。所以从这个意义上来讲,WindowManagerService被称之为SurfaceManagerService也说得通的。
于是,根据对Surface的操作类型可以将Android的显示系统分为三个层次,如图4-2所示。
在图4-2中:
- 第一个层次是UI框架层,其工作为在Surface上绘制UI元素以及响应输入事件。
- 第二个层次为WMS,其主要工作在于管理Surface的分配、层级顺序等。
- 第三层为SurfaceFlinger,负责将多个Surface混合并输出。
经过这个例子的介绍,相信大家对WMS的功能有了一个初步的了解。接下来,我们要进入WMS的内部,通过其启动过程一窥它的构成。
2.WMS的窗口管理结构
经过上一节的介绍,读者应该对WMS的窗口管理有了一个感性的认识。从这一节开将深入WMS的内部去剖析其工作流程。
根据前述内容可知,SampleWindow添加窗口的函数是IWindowSession.add()。IWindowSession是WMS与客户端交互的一个代理,add则直接调用到了WMS的addWindow()函数。我们将从这个函数开始WMS之旅。本小节只讨论它的前半部分。
注意由于篇幅所限,本章不准备讨论removeWindow的实现。
[-->WindowManagerService.java::WindowManagerService.addWindow()Part1]
public int addWindow(Session session, IWindow client, int seq, WindowManager.LayoutParams attrs, int viewVisibility,int displayId Rect outContentInsets, InputChannel outInputChannel) { // 首先检查权限,没有权限的客户端不能添加窗口 intres = mPolicy.checkAddPermission(attrs); ...... // 当为某个窗口添加子窗口时,attachedWindow将用来保存父窗口的实例 WindowState attachedWindow = null; // win就是即将被添加的窗口了 WindowState win = null; ...... finalint type = attrs.type; synchronized(mWindowMap){ ...... //①获取窗口要添加到的DisplayContent /* 在添加窗口时,必须通过displayId参数指定添加到哪一个DisplayContent。 SampleWindow例子没有指定displayId参数,Session会替SampleWindow选择 DEFAULT_DISPLAY,也就是手机屏幕 */ finalDisplayContent displayContent = getDisplayContentLocked(displayId); if(displayContent == null) { return WindowManagerGlobal.ADD_INVALID_DISPLAY; } ...... // 如果要添加的窗口是一个子窗口,就要求父窗口必须已经存在, // 注意, attrs.type表示了窗口的类型, // attrs.token则表示了窗口所隶属的对象,也就是说子window的attrs.token要为父window的attrs.token。 if(type >= FIRST_SUB_WINDOW &&.type <= LAST_SUB_WINDOW) { attachedWindow = windowForClientLocked(null, attrs.token, false); if (attachedWindow == null) { return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN; } // 在这里还可以看出WMS要求窗口的层级关系最多为两层 if (attachedWindow.mAttrs.type >= FIRST_SUB_WINDOW && attachedWindow.mAttrs.type <= LAST_SUB_WINDOW) { return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN; } } boolean addToken = false; // ②WindowToken出场!根据客户端的attrs.token取出已注册的WindowToken WindowToken token = mTokenMap.get(attrs.token); // 下面的if语句块初步揭示了WindowToken和窗口之间的关系 if(token == null) { // 对于以下几种类型的窗口,必须通过LayoutParams.token成员为其指定一个已经添加至WMS的WindowToken if (type >= FIRST_APPLICATION_WINDOW && type<= LAST_APPLICATION_WINDOW) { return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } if (type == TYPE_INPUT_METHOD) { return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } if (type == TYPE_WALLPAPER) { return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } if (type == TYPE_DREAM) { return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } // 其他类型的窗口则不需要事先向WMS添加WindowToken因为WMS会在这里隐式地创 // 建一个。注意最后一个参数false,这表示此WindowToken由WMS隐式创建。 token = new WindowToken(this, attrs.token, -1, false); addToken = true; } else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) { // 对于APPLICATION类型的窗口,要求对应的WindowToken的类型也为APPLICATION // WindowToken是AppWindowToken,并且是WindowToken.appWindowToken不能为空 AppWindowToken atoken = token.appWindowToken; if (atoken == null) { return WindowManagerImpl.ADD_NOT_APP_TOKEN; } else if (atoken.removed) { returnWindowManagerImpl.ADD_APP_EXITING; } if (type==TYPE_APPLICATION_STARTING && atoken.firstWindowDrawn){ return WindowManagerImpl.ADD_STARTING_NOT_NEEDED; } } else if (type == TYPE_INPUT_METHOD) { // 对于其他几种类型的窗口也有类似的要求:窗口类型必须与WindowToken的类型一致 if (token.windowType != TYPE_INPUT_METHOD) { return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } } else if (type == TYPE_WALLPAPER) { if (token.windowType != TYPE_WALLPAPER) { return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } } else if (type == TYPE_DREAM) { if (token.windowType != TYPE_DREAM) { return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } } // ③WMS为要添加的窗口创建了一个WindowState对象 // 这个对象维护了一个窗口的所有状态信息 win= new WindowState(this, session, client, token, attachedWindow, seq, attrs, viewVisibility, displayContent); ...... // WindowManagerPolicy出场了。这个函数的调用会调整LayoutParams的一些成员的取值 mPolicy.adjustWindowParamsLw(win.mAttrs); res= mPolicy.prepareAddWindowLw(win, attrs); if(res != WindowManagerGlobal.ADD_OKAY) { return res; } // 接下来将刚刚隐式创建的WindowToken添加到mTokenMap中去。通过这行代码 // 读者应该能想到,所有的WindowToken都被放入这个HashTable中 ...... if(addToken) { mTokenMap.put(attrs.token, token); } win.attach(); // 然后将WindowState对象加入到mWindowMap中 mWindowMap.put(client.asBinder(),win); // 剩下的代码稍后再做分析 ...... } }
addWindow()函数的前段代码展示了三个重要的概念,分别是WindowToken、WindowState以及DisplayContent。并且在函数开始处对窗口类型的检查判断也初步揭示了它们之间的关系:除子窗口外,添加任何一个窗口都必须指明其所属的WindowToken;窗口在WMS中通过一个WindowState实例进行管理和保管。同时必须在窗口中指明其所属的DisplayContent,以便确定窗口将被显示到哪一个屏幕上。
1.理解WindowToken
1.WindowToken的意义
为了搞清楚WindowToken的作用是什么,看一下其位于WindowToken.java中的定义。虽然它没有定义任何函数,但其成员变量的意义却很重要。
- WindowToken将属于同一个应用组件的窗口组织在了一起。所谓的应用组件可以是Activity、InputMethod、Wallpaper以及Dream。在WMS对窗口的管理过程中,用WindowToken指代一个应用组件。例如在进行窗口ZOrder排序时,属于同一个WindowToken的窗口会被安排在一起,而且在其中定义的一些属性将会影响所有属于此WindowToken的窗口。这些都表明了属于同一个WindowToken的窗口之间的紧密联系。
- WindowToken具有令牌的作用,是对应用组件的行为进行规范管理的一个手段。WindowToken由应用组件或其管理者负责向WMS声明并持有。应用组件在需要新的窗口时,必须提供WindowToken以表明自己的身份,并且窗口的类型必须与所持有的WindowToken的类型一致。从上面的代码可以看到,在创建系统类型的窗口时不需要提供一个有效的Token,WMS会隐式地为其声明一个WindowToken。看起来谁都可以添加个系统级的窗口,难道Android为了内部使用方便而置安全于不顾吗?非也,addWindow()函数一开始的mPolicy.checkAddPermission()的目的就是如此。它要求客户端必须拥有SYSTEM_ALERT_WINDOW或INTERNAL_SYSTEM_WINDOW权限才能创建系统类型的窗口。
2.向WMS声明WindowToken
既然应用组件在创建一个窗口时必须指定一个有效的WindowToken才行,那么WindowToken究竟该如何声明呢?
在SampleWindow应用中,使用wms.addWindowToken()函数声明mToken作为它的令牌,所以在添加窗口时,通过设置lp.token为mToken向WMS进行出示,从而获得WMS添加窗口的许可。这说明,只要是一个Binder对象(随便一个),都可以作为Token向WMS进行声明。对于WMS的客户端来说,Token仅仅是一个Binder对象而已。
WindowManagerService.addWindowToken并不是app端能调用的,因为没有权限。
为了验证这一点,来看一下addWindowToken的代码,如下所示:
[-->WindowManagerService.java::WindowManagerService.addWindowToken()]
@Override publicvoid addWindowToken(IBinder token, int type) { // 需要声明Token的调用者拥有MANAGE_APP_TOKENS的权限 if(!checkCallingPermission(android.Manifest.permission.MANAGE_APP_TOKENS, "addWindowToken()")) { thrownew SecurityException("Requires MANAGE_APP_TOKENS permission"); } synchronized(mWindowMap){ ...... // 注意其构造函数的参数与addWindow()中不同,最后一个参数为true,表明这个Token 是显式申明的 wtoken= new WindowToken(this, token, type, true); mTokenMap.put(token,wtoken); ...... } }
使用addWindowToken()函数声明Token,将会在WMS中创建一个WindowToken实例,并添加到mTokenMap中,键值为客户端用于声明Token的Binder实例。与addWindow()函数中隐式地创建WindowToken不同,这里的WindowToken被声明为显式的。隐式与显式的区别在于,当隐式创建的WindowToken的最后一个窗口被移除后,此WindowToken会被一并从mTokenMap中移除。显式创建的WindowToken只能通过removeWindowToken()显式地移除。
addWindowToken()这个函数告诉我们,WindowToken其实有两层含义:
- 对于显示组件(客户端)而言的Token,是任意一个Binder的实例,对显示组件(客户端)来说仅仅是一个创建窗口的令牌,没有其他的含义。
- 对于WMS而言的WindowToken这是一个WindowToken类的实例,保存了对应于客户端一侧的Token(Binder实例),并以这个Token为键,存储于mTokenMap中。客户端一侧的Token是否已被声明,取决于其对应的WindowToken是否位于mTokenMap中。
注意:在一般情况下,称显示组件(客户端)一侧Binder的实例为Token,而称WMS一侧的WindowToken对象为WindowToken。但是为了叙述方便,在没有歧义的前提下不会过分仔细地区分这两个概念。
接下来,看一下各种显示组件是如何声明WindowToken的。
(1)Wallpaper
Wallpaper的Token声明在WallpaperManagerService中。参考以下代码:
[-->WallpaperManagerService.java::WallpaperManagerService.bindWallpaperComponentLocked()]
Boolean bindWallpaperComponentLocked(......) { ...... WallpaperConnection newConn = new WallpaperConnection(wi, wallpaper); ...... mIWindowManager.addWindowToken(newConn.mToken, WindowManager.LayoutParams.TYPE_WALLPAPER); ...... }
WallpaperManagerService是Wallpaper管理器,它负责维护系统已安装的所有的Wallpaper并在它们之间进行切换,而这个函数的目的是准备显示一个Wallpaper。newConn.mToken与SampleWindow例子一样,是一个简单的Binder对象。这个Token将在即将显示出来的Wallpaper被连接时传递给它,之后Wallpaper即可通过这个Token向WMS申请创建绘制壁纸所需的窗口了。
注意: WallpaperManagerService向WMS声明的Token类型为TYPE_WALLPAPER,所以,Wallpaper仅能本分地创建TYPE_WALLPAPER类型的窗口。
相应的,WallpaperManagerService会在detachWallpaperLocked()函数中取消对Token的声明:
[-->WallpaperManagerService.java::WallpaperManagerService.detachWallpaperLocked()]
boolean detachWallpaperLocked(WallpaperData wallpaper){ ...... mIWindowManager.removeWindowToken(wallpaper.connection.mToken); ...... }
再此之后,如果这个被detach的Wallpaper想再要创建窗口便不再可能了。
WallpaperManagerService使用WindowToken对一个特定的Wallpaper做出了如下限制:
- Wallpaper只能创建TYPE_WALLPAPER类型的窗口。
- Wallpaper显示的生命周期由WallpaperManagerService牢牢地控制着。仅有当前的Wallpaper才能创建窗口并显示内容。其他的Wallpaper由于没有有效的Token,而无法创建窗口。
InputMethod
InputMethod的Token的来源与Wallpaper类似,其声明位于InputMethodManagerService的startInputInnerLocked()函数中,取消声明的位置在InputmethodManagerService的unbindCurrentMethodLocked()函数。InputMethodManagerService通过Token限制着每一个InputMethod的窗口类型以及显示生命周期。
输入法窗口
InputMethodService里的窗口为android.inputmethodservice.SoftInputWindow extends Dialog,
就是说输入法窗口其实是一个dialog,而dialog的window是phoneWindow。
输入法窗口布局为com.android.internal.R.layout.input_method:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/parentPanel" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" > //fullscreenArea中有extractArea和candidatesArea,默认横屏时extractArea会显示出来,是一个编辑框的放大,大小为剩余的空间,如果下边的输入区域沾满了屏幕,则extractArea的大小就为0. <LinearLayout android:id="@+id/fullscreenArea" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" > <FrameLayout android:id="@android:id/extractArea" android:layout_width="match_parent" android:layout_height="0px" android:layout_weight="1" android:visibility="gone"> </FrameLayout> <FrameLayout android:id="@android:id/candidatesArea" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="invisible"> </FrameLayout> </LinearLayout> // 其实就是inputview的根布局,在源码中的变量名称为mInputFrame <FrameLayout android:id="@android:id/inputArea" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone"> </FrameLayout> </LinearLayout>
由于PhoneWindow是有一个默认布局的root,就是decorView(extends FrameLayout),
root的里边又有一个LinearLayout的布局,这个布局就是里的id/content就是内容布局的根view。
经过使用Hierarchy Viewer追踪并结合代码,
最终发现这个LinearLayout布局为com.android.internal.R.layout.screen_simple
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:orientation="vertical"> <ViewStub android:id="@+id/action_mode_bar_stub" android:inflatedId="@+id/action_mode_bar" android:layout="@layout/action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" /> // contentParent,其他要显示的都需要添加到这布局里边。 <FrameLayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:foregroundInsidePadding="false" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" /> </LinearLayout>
InputMethodSerivce.onCreate()
->initViews()
->SoftInputWindow.setContentView(input_method);
->Dialog.setContentView(input_method)
->PhoneWindow.setContentView(input_method);
->mContentParent.addView(input_method, params);
(2)Activity的Token
Activity的Token的使用方式与Wallpaper和InputMethod类似,但是其包含更多的内容。毕竟,对于Activity,无论是其组成还是操作都比Wallpaper以及InputMethod复杂得多。对此,WMS专为Activity实现了一个WindowToken的子类:AppWindowToken。部分源码如下:
WindowToken :
class WindowToken { // The actual token. final IBinder token; // The type of window this token is for, as per WindowManager.LayoutParams. final int windowType; // Set if this token was explicitly added by a client, so should // not be removed when all windows are removed. final boolean explicit; // If this is an AppWindowToken, this is non-null. AppWindowToken appWindowToken; // All of the windows associated with this token. final WindowList windows = new WindowList(); WindowToken(WindowManagerService _service, IBinder _token, int type, boolean _explicit) { service = _service; token = _token; windowType = type; explicit = _explicit; } }
AppWindowToken:
/** * Version of WindowToken that is specifically for a particular application (or * really activity) that is displaying windows. */ class AppWindowToken extends WindowToken { // Non-null only for application tokens. final IApplicationToken appToken; // All of the windows and child windows that are included in this // application token. Note this list is NOT sorted! final WindowList allAppWindows = new WindowList(); AppWindowToken(WindowManagerService _service, IApplicationToken _token) { super(_service, _token.asBinder(), WindowManager.LayoutParams.TYPE_APPLICATION, true); appWindowToken = this; appToken = _token; mInputApplicationHandle = new InputApplicationHandle(this); mAnimator = service.mAnimator; mAppAnimator = new AppWindowAnimator(this); } }
WindowToken内部有一个AppWindowToken的引用,如果此window是activity的,则AppWindowToken就会引用自己。目的是用一个map来管理两种对象。
既然AppWindowToken是为Activity服务的,那么其声明自然在ActivityManagerService中。具体位置为ActivityStack.startActivityLocked(),也就是启动Activity的时候。相关代码如下:
[-->ActivityStack.java::ActivityStack.startActivityLocked()]
private final void startActivityLocked(......) { ...... mService.mWindowManager.addAppToken(addPos,r.appToken, r.task.taskId, r.info.screenOrientation, r.fullscreen); ...... }
startActivityLocked()向WMS声明r.appToken作为此Activity的Token,这个Token是在ActivityRecord的构造函数中创建的。随然后在realStartActivityLocked()中将此Token交付给即将启动的Activity。
[-->ActivityStack.java::ActivityStack.realStartActivityLocked()]
final boolean realStartActivityLocked(......) { ...... app.thread.scheduleLaunchActivity(newIntent(r.intent), r.appToken, System.identityHashCode(r), r.info, newConfiguration(mService.mConfiguration), r.compat, r.icicle, results, newIntents,!andResume, mService.isNextTransitionForward(),profileFile, profileFd, profileAutoStop); ...... }
启动后的Activity即可使用此Token创建类型为TYPE_APPLICATION的窗口了。
取消Token的声明则位于ActivityStack.removeActivityFromHistoryLocked()函数中。
Activity的Token在客户端是否和Wallpaper一样,仅仅是一个基本的Binder实例呢?其实不然。看一下r.appToken的定义可以发现,这个Token的类型是IApplicationToken.Stub。其中定义了一系列和窗口相关的一些通知回调,它们是:
- windowsDrawn(),当窗口完成初次绘制后通知AMS。
- windowsVisible(),当窗口可见时通知AMS。
- windowsGone(),当窗口不可见时通知AMS。
- keyDispatchingTimedOut(),窗口没能按时完成输入事件的处理。这个回调将会导致ANR。
- getKeyDispatchingTimeout(),从AMS处获取界定ANR的时间。
AMS通过ActivityRecord表示一个Activity。而ActivityRecord的appToken在其构造函数中被创建,所以每个ActivityRecord拥有其各自的appToken。而WMS接受AMS对Token的声明,并为appToken创建了唯一的一个AppWindowToken。因此,这个类型为IApplicationToken的Binder对象appToken粘结了AMS的ActivityRecord与WMS的AppWindowToken,只要给定一个ActivityRecord,都可以通过appToken在WMS中找到一个对应的AppWindowToken,从而使得AMS拥有了操纵Activity的窗口绘制的能力。
例如,当AMS认为一个Activity需要被隐藏时,以Activity对应的ActivityRecord所拥有的appToken作为参数调用WMS的setAppVisibility()函数。此函数通过appToken找到其对应的AppWindowToken,然后将属于这个Token的所有窗口隐藏。
注意:每当AMS因为某些原因(如启动/结束一个Activity,或将Task移到前台或后台)而调整ActivityRecord在mHistory中的顺序时,都会调用WMS相关的接口移动AppWindowToken在mAppTokens中的顺序,以保证两者的顺序一致。在后面讲解窗口排序规则时会介绍到,AppWindowToken的顺序对窗口的顺序影响非常大。
(3)dialog的token设置
dialog 的type是使用创建window时默认创建的LayoutParams(),为默认为TYPE_APPLICATION,
所以dialog不是 启动他的activity的window的 子window,只是和activity共享layoutparams.token,即appToken。
Dialog中:
Dialog(Context context, int theme, boolean createContextThemeWrapper) { // 此处的context是activity,所以mWindowManager是activity的mWindowManager,且之后会调用此mWindowManager.addView mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); Window w = PolicyManager.makeNewWindow(mContext); mWindow = w; w.setCallback(this); w.setWindowManager(mWindowManager, null, null); w.setGravity(Gravity.CENTER); mListenersHandler = new ListenersHandler(this); } public void show() { if (mShowing) { if (mDecor != null) { if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) { mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR); } mDecor.setVisibility(View.VISIBLE); } return; } mCanceled = false; if (!mCreated) { dispatchOnCreate(null); } onStart(); mDecor = mWindow.getDecorView(); WindowManager.LayoutParams l = mWindow.getAttributes(); // 此处如果是softInputMode的话,DecorView的layoutparams就和mWindow的LayoutParams不是一个对象了。 if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) { WindowManager.LayoutParams nl = new WindowManager.LayoutParams(); nl.copyFrom(l); nl.softInputMode |= WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; l = nl; } try { // 此处是activity的mWindowManager mWindowManager.addView(mDecor, l); mShowing = true; sendShowMessage(); } finally { } }
WindowManagerGlobal.addView:
// 由于是activity的mWindowManager,所以parentWindow就为activity的window public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ... final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; if (parentWindow != null) { parentWindow.adjustLayoutParamsForSubWindow(wparams); } ... }
Window.adjustLayoutParamsForSubWindow:
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) { ... if (wp.token == null) { wp.token = mContainer == null ? mAppToken : mContainer.mAppToken; } }
当dialog的type为非系统type时,为什么dialog不能使用非activity的context?
-
PopupWindow的token
PopupWindow的type时TYPE_APPLICATION_PANEL,所以它是一个子窗口。
private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { if (isShowing() || mContentView == null) { return; } registerForScrollChanged(anchor, xoff, yoff, gravity); mIsShowing = true; mIsDropdown = true; WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken()); preparePopup(p); updateAboveAnchor(findDropDownPosition(anchor, p, xoff, yoff, gravity)); if (mHeightMode < 0) p.height = mLastHeight = mHeightMode; if (mWidthMode < 0) p.width = mLastWidth = mWidthMode; p.windowAnimations = computeAnimationResource(); invokePopup(p); } public void showAtLocation(View parent, int gravity, int x, int y) { showAtLocation(parent.getWindowToken(), gravity, x, y); } public void showAtLocation(IBinder token, int gravity, int x, int y) { if (isShowing() || mContentView == null) { return; } unregisterForScrollChanged(); mIsShowing = true; mIsDropdown = false; WindowManager.LayoutParams p = createPopupLayout(token); p.windowAnimations = computeAnimationResource(); preparePopup(p); if (gravity == Gravity.NO_GRAVITY) { gravity = Gravity.TOP | Gravity.START; } p.gravity = gravity; p.x = x; p.y = y; if (mHeightMode < 0) p.height = mLastHeight = mHeightMode; if (mWidthMode < 0) p.width = mLastWidth = mWidthMode; invokePopup(p); } private void preparePopup(WindowManager.LayoutParams p) { if (mContentView == null || mContext == null || mWindowManager == null) { throw new IllegalStateException("You must specify a valid content view by " + "calling setContentView() before attempting to show the popup."); } if (mBackground != null) { final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); int height = ViewGroup.LayoutParams.MATCH_PARENT; if (layoutParams != null && layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { height = ViewGroup.LayoutParams.WRAP_CONTENT; } // when a background is available, we embed the content view // within another view that owns the background drawable PopupViewContainer popupViewContainer = new PopupViewContainer(mContext); PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, height ); popupViewContainer.setBackground(mBackground); popupViewContainer.addView(mContentView, listParams); mPopupView = popupViewContainer; } else { mPopupView = mContentView; } mPopupView.setElevation(mElevation); mPopupViewInitialLayoutDirectionInherited = (mPopupView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT); mPopupWidth = p.width; mPopupHeight = p.height; } private void invokePopup(WindowManager.LayoutParams p) { if (mContext != null) { p.packageName = mContext.getPackageName(); } mPopupView.setFitsSystemWindows(mLayoutInsetDecor); setLayoutDirectionFromAnchor(); mWindowManager.addView(mPopupView, p); }
(5)Toast的token
Toast的token是可以没有的,因为Toast的layoutparams.type为LayoutParams.TYPE_TOAST,也就是系统类型的window。
public static Toast makeText(Context context, CharSequence text, @Duration int duration) { Toast result = new Toast(context); LayoutInflater inflate = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null); TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message); tv.setText(text); result.mNextView = v; result.mDuration = duration; return result; } public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }
Toast显示出来最终是会调用Toast的内部类TN的show。
private static class TN extends ITransientNotification.Stub { private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); // 因为handler创建时会获取一个和当前线程相关的looper, // 所以要在子线程中使用的话,如果没有创建looper就会报错。 final Handler mHandler = new Handler(); int mGravity; int mX, mY; float mHorizontalMargin; float mVerticalMargin; View mView; View mNextView; WindowManager mWM; @Override public void show() { mHandler.post(mShow); } final Runnable mShow = new Runnable() { @Override public void run() { handleShow(); } }; TN() { // XXX This should be changed to use a Dialog, with a Theme.Toast // defined that sets up the layout params appropriately. final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = com.android.internal.R.style.Animation_Toast; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; } public void handleShow() { if (mView != mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; Context context = mView.getContext().getApplicationContext(); if (context == null) { context = mView.getContext(); } mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); // We can resolve the Gravity here by using the Locale for getting // the layout direction final Configuration config = mView.getContext().getResources().getConfiguration(); final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection()); mParams.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeView(mView); } if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this); mWM.addView(mView, mParams); trySendAccessibilityEvent(); } }
2.理解WindowState
从WindowManagerService.addWindow()函数的实现中可以看出,当向WMS添加一个窗口时,WMS会为其创建一个WindowState。WindowState表示一个窗口的所有属性,所以它是WMS中事实上的窗口。这些属性将在后面遇到时再做介绍。
类似于WindowToken,WindowState在显示组件(app端)一侧也有个对应的类型:IWindow.Stub。IWindow.Stub提供了很多与窗口管理相关通知的回调,例如尺寸变化、焦点变化等。
另外,从WindowManagerService.addWindow()函数中看到新的WindowState被保存到mWindowMap中,键值为IWindow的Bp端。mWindowMap是整个系统所有窗口的一个全集。
说明:对比一下mTokenMap和mWindowMap。这两个HashMap维护了WMS中最重要的两类数据:WindowToken及WindowState。它们的键都是IBinder,区别是:
- mTokenMap的键值可能是IAppWindowToken的Bp端(使用addAppToken()进行声明),或者是其他任意一个Binder的Bp端(使用addWindowToken()进行声明);
- 而mWindowMap的键值一定是IWindow的Bp端。
关于WindowState的更多细节将在后面的讲述中进行介绍。不过经过上面的分析,不难得到WindowToken和WindowState之间的关系,参考图4-4。
更具体一些,以一个正在回放视频并弹出两个对话框的Activity为例,WindowToken与WindowState的意义如图4-5所示。
3.理解DisplayContent
如果说WindowToken按照窗口之间的逻辑关系将其分组,那么DisplayContent则根据窗口的显示位置将其分组。隶属于同一个DisplayContent的窗口将会被显示在同一个屏幕中。每一个DisplayContent都对应这一个唯一的ID,在添加窗口时可以通过指定这个ID决定其将被显示在那个屏幕中。
DisplayContent是一个非常具有隔离性的一个概念。处于不同DisplayContent的两个窗口在布局、显示顺序以及动画处理上不会产生任何耦合。因此,就这几个方面来说,DisplayContent就像一个孤岛,所有这些操作都可以在其内部独立执行。因此,这些本来属于整个WMS全局性的操作,变成了DisplayContent内部的操作了。
3.理解窗口的显示次序
在addWindow()函数的前半部分中,WMS为窗口创建了用于描述窗口状态的WindowState,接下来便会为新建的窗口确定显示次序。手机屏幕是以左上角为原点,向右为X轴方向,向下为Y轴方向的一个二维空间。为了方便管理窗口的显示次序,手机的屏幕被扩展为了一个三维的空间,即多定义了一个Z轴,其方向为垂直于屏幕表面指向屏幕外。多个窗口依照其前后顺序排布在这个虚拟的Z轴上,因此窗口的显示次序又被称为Z序(Z order)。在这一节中将深入探讨WMS确定窗口显示次序的过程以及其影响因素。
1.主序、子序和窗口类型
看一下WindowState的构造函数:
[-->WindowState.java::WindowState.WindowState()]
WindowState(WindowManagerService service, Sessions, IWindow c, WindowToken token, WindowState attachedWindow, int seq, WindowManager.LayoutParams a, int viewVisibility, final DisplayContent displayContent) { ...... // 为子窗口分配ZOrder if((mAttrs.type >= FIRST_SUB_WINDOW && mAttrs.type <= LAST_SUB_WINDOW)) { // 这里的mPolicy即是WindowManagerPolicy mBaseLayer= mPolicy.windowTypeToLayerLw(attachedWindow.mAttrs.type) * WindowManagerService.TYPE_LAYER_MULTIPLIER + WindowManagerService.TYPE_LAYER_OFFSET; mSubLayer= mPolicy.subWindowTypeToLayerLw(a.type); ...... } else {// 为普通窗口分配ZOrder mBaseLayer= mPolicy.windowTypeToLayerLw(a.type) * WindowManagerService.TYPE_LAYER_MULTIPLIER + WindowManagerService.TYPE_LAYER_OFFSET; mSubLayer= 0; ...... } ...... }
窗口的显示次序由两个成员字段描述:主序mBaseLayer和子序mSubLayer。主序用于描述窗口及其子窗口在所有窗口中的显示位置。而子序则描述了一个子窗口在其兄弟窗口中的显示位置。
- 主序越大,则窗口及其子窗口的显示位置相对于其他窗口的位置越靠前。
- 子序越大,则子窗口相对于其兄弟窗口的位置越靠前。对于父窗口而言,其主序取决于其类型,其子序则保持为0。而子窗口的主序与其父窗口一样,子序则取决于其类型。从上述代码可以看到,主序与子序的分配工作是由WindowManagerPolicy的两个成员函数windowTypeToLayerLw()和subWindowTypeToLayerLw()完成的。
下表列出了所有可能的窗口类型以及其主序与子序的值。
主序窗口类型 |
主序 |
TYPE_UNIVERSE_BACKGROUND |
11000 |
TYPE_PHONE |
31000 |
TYPE_RECENTS_OVERLAY |
51000 |
TYPE_TOAST |
61000 |
TYPE_DREAM |
81000 |
TYPE_INPUT_METHOD |
101000 |
TYPE_KEYGUARD |
121000 |
TYPE_STATUS_BAR_SUB_PANEL |
141000 |
TYPE_WALLPAPER |
21000 |
TYPE_SEARCH_BAR |
41000 |
TYPE_SYSTEM_DIALOG |
51000 |
TYPE_PRIORITY_PHONE |
71000 |
TYPE_SYSTEM_ALERT |
91000 |
TYPE_INPUT_METHOD_DIALOG |
111000 |
TYPE_KEYGUARD_DIALOG |
131000 |
应用窗口与未知类型的窗口 |
21000 |
FIRST_SUB_WINDOW和LAST_SUB_WINDOW之间的窗口。
子窗口类型 |
子序 |
TYPE_APPLICATION_PANEL |
1 |
TYPE_APPLICATION_ATTACHED_DIALOG |
1 |
TYPE_APPLICATION_MEDIA |
-2 |
TYPE_APPLICATION_MEDIA_OVERLAY |
-1 |
TYPE_APPLICATION_SUB_PANEL |
2 |
注意:表4-2中的MEDIA和MEDIA_OVERLAY的子序为负值,这表明它们的显示次序位于其父窗口的后面。 这两个类型的子窗口是SurfaceView控件创建的。SurfaceView被实例化后,会向WMS添加一个类型为MEDIA的子窗口,它的父窗口就是承载SurfaceView控件的窗口。这个子窗口的Surface将被用于视频回放、相机预览或游戏绘制。为了不让这个子窗口覆盖住所有的父窗口中承载的其他控件(如拍照按钮,播放器控制按钮等),它必须位于父窗口之后。
从表4-1所描述的主序与窗口类型的对应关系中可以看出,WALLPAPER类型的窗口的主序竟和APPLICATION类型的窗口主序相同,这看似有点不合常理,WALLPAPER不是应该显示在所有Acitivity之下吗?
其实WALLPAPER类型的窗口是一个很不安分的角色,需要在所有的APPLICATION窗口之间跳来跳去。这是因为,有的Activity指定了android:windowShowWallpaper为true,则表示窗口要求将用户当前壁纸作为其背景。对于WMS来说,最简单的办法就是将WALLPAPER窗口放置到紧邻拥有这个式样的窗口的下方。在这种需求下,为了保证主序决定窗口顺序的原则,WALLPAPER使用了与APPLICATION相同的主序。另外,输入法窗口也是一个很特殊的情况,输入法窗口会选择输入目标窗口,并将自己放置于其上。在本章中不讨论这两个特殊的例子,WALLPAPER的排序规则将在第7章中进行介绍,而输入法的排序则留给读者自行研究。
虽然知道了窗口的主序与子序是如何分配的,不过我们仍然存有疑问:如果有两个相同类型的窗口,那么它们的主序与子序岂不是完全相同?如何确定它们的显示顺序呢?
事实上,表4-1和表4-2中所描述的主序和子序仅仅是排序的依据之一,WMS需要根据当前所有同类型窗口的数量为每个窗口计算最终的显示次序。
2.通过主序与子序确定窗口的次序
回到WMS的addWindow()函数中,继续往下看:
[-->WindowManagerService.java::WindowManagerService.addWindow()]
public int addWindow(Session session, IWindowclient, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, InputChannel outInputChannel) { ...... synchronized(mWindowMap){ //在前面的代码中,WMS验证了添加窗口的令牌的有效性,并为新窗口创建了新的WindowState对象 // 新的WindowState对象在其构造函数中根据窗口类型初始化了其主序mBaseLayer和mSubLayer ...... // 接下来,将新的WindowState按照显示次序插入到当前DisplayContent的mWindows列表中 // 为了代码结构的清晰,不考虑输入法窗口和壁纸窗口的处理 if (type== TYPE_INPUT_METHOD) { ...... }else if (type == TYPE_INPUT_METHOD_DIALOG) { }else { // 将新的WindowState按显示次序插入到当前DisplayContent的mWindows列表中 addWindowToListInOrderLocked(win,true); if(type == TYPE_WALLPAPER) { ...... } } ...... // 根据窗口的排序结果,为DisplayContent的所有窗口分配最终的显示次序 assignLayersLocked(displayContent.getWindowList()); ...... } ...... return res; }
这里有两个关键点:
- addWindowToListInOrderLocked()将新建的WindowState按照一定的顺序插入到当前DisplayContent的mWindows列表中。在分析WMS的重要成员时提到过这个列表。它严格地按照显示顺序存储了所有窗口的WindowState。
- assignLayersLocked()将根据mWindows的存储顺序对所有的WindowState的主序和子序进行调整。
接下来分别分析一下这两个函数。
1.addWindowToListInOrderLocked()分析
addWindowToListInOrderLocked()的代码很长,不过其排序原则却比较清晰。这里直接给出其处理原则,感兴趣的读者可根据这些原则自行深究相关代码。
注意:再次强调一下,mWindows列表是按照主序与子序的升序进行排序的,所以显示靠前的窗口放在列表靠后的位置,而显示靠前的窗口,则位于列表的前面。也就是说,列表顺序与显示顺序是相反的。这点在阅读代码时要牢记,以免混淆。
在后面的叙述中,非特别强调,所谓的前后都是指显示顺序而不是在列表的存储顺序。
子窗口的排序规则:
子窗口的位置计算是相对父窗口的,并根据其子序进行排序。由于父窗口的子序为0,所以子序为负数的窗口会放置在父窗口的后面,而子序为正数的窗口会放置在父窗口的前面。如果新窗口与现有窗口子序相等,则正数子序的新窗口位于现有窗口的前面,负数子序的新窗口位于现有窗口的后面。
非子窗口的排序规则:
非子窗口的排序则是依据主序进行的,但是其规则较为复杂,分为应用窗口和非应用窗口两种情况。之所以要区别处理应用窗口是因为所有的应用窗口的初始主序都是21000,并且应用窗口的位置应该与它所属的应用的其他窗口放在一起。例如应用A显示于应用B的后方,当应用A因为某个动作打开一个新的窗口时,新窗口应该位于应用A其他窗口的前面,但是不得覆盖应用B的窗口。只依据主序进行排序是无法实现这个管理逻辑的,还需要依赖Activity的顺序。在WindowToken一节的讲解中,曾经简单分析了mAppTokens列表的性质,它所保存的AppWindowToken的顺序与AMS中ActivityRecord的顺序时刻保持一致。因此,AppWindowToken在mAppTokens的顺序就是Activity的顺序。
非应用窗口的排序规则:依照主序进行排序,主序高者排在前面,当现有窗口的主序与新窗口相同时,新窗口位于现有窗口的前面。
应用窗口的排序规则:如上所述,同一个应用的窗口的显示位置必须相邻。如果当前应用已有窗口在显示(当前应用的窗口存储在其WindowState.appWindowToken.windows中),新窗口将插入到其所属应用其他窗口的前面,但是保证STARTING_WINDOW永远位于最前方,BASE_APPLICATION永远位于最后方。如果新窗口是当前应用的第一个窗口,则参照其他应用的窗口顺序,将新窗口插入到位于前面的最后一个应用的最后一个窗口的后方,或者位于后面的第一个应用的最前一个窗口的前方。如果当前没有其他应用的窗口可以参照,则直接根据主序将新窗口插入到列表中。
窗口排序的总结如下:
- 子窗口依据子序相对于其父窗口进行排序。相同子序的窗体,正子序则越新越靠前,负子序则越新越靠后。
- 应用窗口参照本应用其他窗口或相邻应用的窗口进行排序。如果没有任何窗口可以参照,则根据主序进行排序。
- 非应用窗口根据主序进行排序。
经过addWindowToListInOrderLocked()函数的处理之后,当前DisplayContent的窗口列表被插入了一个新的窗口。然后等待assignLayersLocked()的进一步处理。
2.assignLayersLocked分析
assignLayersLocked()函数将根据每个窗口的主序以及它们在窗口列表中的位置重新计算最终的显示次序mLayer。
[-->WindowManagerService.java::WindowManagerService.assignLayersLocked()]
int N = windows.size(); int curBaseLayer = 0; // curLayer表示当前分配到的Layer序号 int curLayer = 0; int i; // 遍历列表中的所有的窗口,逐个分配显示次序 for (i=0; i<N; i++) { final WindowState w = windows.get(i); final WindowStateAnimator winAnimator = w.mWinAnimator; boolean layerChanged = false; int oldLayer = w.mLayer; if (w.mBaseLayer == curBaseLayer || w.mIsImWindow || (i > 0 &&w.mIsWallpaper)) { // 为具有相同主序的窗口在curLayer上增加一个偏移量,并将curLayer作为最终的显示次序 curLayer +=WINDOW_LAYER_MULTIPLIER; w.mLayer = curLayer; } else { // 此窗口拥有不同的主序,直接将主序作为其显示次序并更新curLayer curBaseLayer = curLayer =w.mBaseLayer; w.mLayer = curLayer; } // 如果现实次序发生了变化则进行标记 if (w.mLayer != oldLayer) { layerChanged = true; anyLayerChanged = true; } ...... // 在确定了最终的显示次序mLayer后,又计算了WindowStateAnimator另一个属性:mAnimLayer。 if (w.mTargetAppToken != null) { // 输入目标为Activity的输入法窗口,其mTargetAppToken是其输入目标所属的AppToken winAnimator.mAnimLayer = w.mLayer + w.mTargetAppToken.mAppAnimator.animLayerAdjustment; } elseif (w.mAppToken != null) { // 属于一个Activity的窗口 winAnimator.mAnimLayer = w.mLayer + w.mAppToken.mAppAnimator.animLayerAdjustment; } else { winAnimator.mAnimLayer = w.mLayer; } } ...... // 向当前DisplayContent的监听者通知显示次序的更新 if (anyLayerChanged) { scheduleNotifyWindowLayersChangedIfNeededLocked(getDefaultDisplayContentLocked()); } }
assignLayersLocked()的工作原理比较绕,简单来说,如果某个窗口在整个列表中拥有唯一的主序,则该主序就是其最终的显示次序。如果若干个窗口拥有相同的主序(注意经过addWindowToListInOrderLocked()函数的处理后,拥有相同主序的窗口都是相邻的),则第i个相同主序的窗口的显示次序为在主序的基础上增加i * WINDOW_LAYER_MULTIPLIER的偏移。
经过assignLayersLocked()之后,一个拥有9个窗口的系统的现实次序的信息如表4-3所示。
表4- 3 窗口最终的显示次序信息
|
窗口1 |
窗口2 |
窗口3 |
窗口4 |
窗口5 |
窗口6 |
窗口7 |
窗口8 |
窗口9 |
主序mBaseLayer |
11000 |
11000 |
21000 |
21000 |
21000 |
21000 |
71000 |
71000 |
101000 |
子序mSubLayer |
0 |
0 |
0 |
-1 |
0 |
0 |
0 |
0 |
0 |
显示次序mLayer |
11000 |
11005 |
21000 |
21005 |
21010 |
21015 |
71000 |
71005 |
101000 |
在确定了最终的显示次序mLayer后,又计算了WindowStateAnimator另一个属性:mAnimLayer。
对于绝大多数窗口而言,其对应的WindowStateAnimator的mAnimLayer就是mLayer。而当窗口附属为一个Activity时,mAnimLayer会加入一个来自AppWindowAnimator的矫正:animLayerAdjustment。
WindowStateAnimator和AppWindowAnimator是动画系统中的两员大将,它们负责渲染窗口动画以及最终的Surface显示次序的修改。回顾一下4.1.2中的WMS的组成结构图,WindowState属于窗口管理体系的类,因此其所保存的mLayer的意义偏向于窗口管理。WindowStateAnimator/AppWindowAnimator则是动画体系的类,其mAnimLayer的意义偏向于动画,而且由于动画系统维护着窗口的Surface,因此mAnimLayer是Surface的实际显示次序。
在没有动画的情况下,mAnimLayer与mLayer是相等的,而当窗口附属为一个Activity时,则会根据AppTokenAnimator的需要适当地增加一个矫正值。这个矫正值来自AppTokenAnimator所使用的Animation。当Animation要求动画对象的ZOrder必须位于其他对象之上时(Animation.getZAdjustment()的返回值为Animation.ZORDER_TOP),这个矫正是一个正数WindowManagerService.TYPE_LAYER_OFFSET(1000),这个矫正值很大,于是窗口在动画过程中会显示在其他同主序的窗口之上。相反,如果要求ZOrder必须位于其他对象之下时,矫正为-WindowManagerService.TYPE_LAYER_OFFSET(-1000),于是窗口会显示在其他同主序的窗口之下。在动画完结后,mAnimLayer会被重新赋值为WindowState.mLayer,使得窗口回到其应有的位置。
动画系统的工作原理将在4.5节详细探讨。
注意:矫正值为常数1000,也就出现一个隐藏的bug:当同主序的窗口的数量大于200时,APPLICATION窗口的mLayer值可能超过22000。此时,在对于mLayer值为21000的窗口应用矫正后,仍然无法保证动画窗口位于同主序的窗口之上。不过超过200个应用窗口的情况非常少见,而且仅在动画过程中才会出现bug,所以google貌似也懒得解决这个问题。
3.更新显示次序到Surface
再回到WMS的addWindow()函数中,发现再没有可能和显示次序相关的代码了。mAnimLayer是如何发挥自己的作用呢?不要着急,事实上,新建的窗口目前尚无Surface。回顾一下SimpleWindow例子,在执行session.relayout()后,WMS才为新窗口分配了一块Surface。也就是说,只有执行relayout()之后才会为新窗口的Surface设置新的显示次序。
为了不中断对显示次序的调查进展,就直接开门见山地告诉大家,设置显示次序到Surface的代码位于WindowStateAnimator. prepareSurfaceLocked()函数中,是通过Surface.setLayer()完成的。在4.5节会深入为大家揭开WMS动画子系统的面纱。
4.关于显示次序的小结
这一节讨论了窗口类型对窗口显示次序的影响。窗口根据自己的类型得出其主序及子序,然后addWindowToListInOrderLocked()根据主序、子序以及其所属的Activity的顺序,按照升序排列在DisplayContent的mWindows列表中。然后assignLayersLocked()为mWindows中的所有窗口分配最终的显示次序。之后,WMS的动画系统将最终的显示次序通过Surface.setLayer()设置进SurfaceFlinger。
4.WMS和动画和Choreographer
这里再强调一下,WMS不负责窗口的具体绘制,因此WMS动画系统仅会影响窗口的位置、显示尺寸与透明度,不会影响窗口上所绘制的内容。窗口内容的动画在窗口的客户端由ViewRootImpl驱动完成。
另外,动画系统中所改变的显示尺寸与布局过程中的Surface尺寸是两个不同的概念。
布局过程中的Surface尺寸是Surface的实际尺寸,这个尺寸决定了其GraphicBuffer的大小以及Canvas可以绘制的区域。而动画过程中的尺寸则是渲染尺寸,只是在最终输出的过程中将Surface的内容放大或缩小。
Choreographer
Choreographer.getInstence()是一个线程单例。