《深入理解Android 卷III》即将发布,作者是张大伟。此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分。在一个特别讲究颜值的时代,本书分析了Android 4.2中WindowManagerService、ViewRoot、Input系统、StatusBar、Wallpaper等重要“颜值绘制/处理”模块
第4章 深入理解WindowManagerService(节选)
本章主要内容:
· 示例最原始最简单的窗口创建方法
· 研究WMS的窗口管理结构
· 探讨WMS布局系统的工作原理
· 研究WMS动画系统的工作原理
本章涉及的源代码文件名及位置:
· SystemServer.java
frameworks/base/services/java/com/android/server/SystemServer.java
· WindowManagerService.java
frameworks/base/services/java/com/android/server/wm/WindowManagerService.java
· ActivityStack.java
frameworks/base/services/java/com/android/server/am/ActivityStack.java
· WindowState.java
frameworks/base/services/java/com/android/server/wm/WindowState.java
· PhoneWindowManager.java
frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
· AccelerateDecelerateInterpolator.java
frameworks/base/core/java/android/view/animation/AccelerateDecelerateInterpolator.java
· Animation.java
frameworks/base/core/java/android/view/animation/Animation.java
· AlphaAnimation.java
frameworks/base/core/java/android/view/animation/AlphaAnimation.java
· WindowAnimator.java
frameworks/base/services/java/com/android/server/wm/WindowAnimator.java
· WindowStateAnimator.java
frameworks/base/services/java/com/android/server/wm/WindowStateAnimator.java
4.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),根据布局系统计算的窗口位置与层次渲染窗口动画。
为了让读者对WMS的功能以及工作方式有一个初步地认识,并见识一下WMS的强大,本节将从一个简单而神奇的例子开始WMS的学习之旅。
4.1.1 一个从命令行启动的动画窗口
1.SampleWindow的实现
在这一节里将编写一个最简单的Java程序SampleWindow,仅使用WMS的接口创建并渲染一个动画窗口。此程序将抛开Activity、Wallpaper等UI架构的复杂性,直接了当地揭示WMS的客户端如何申请、渲染并注销自己的窗口。同时这也初步地反应了WMS的工作方式。
这个例子很简单,只有三个文件:
· SampleWindow.java 主程序源代码。
· Android.mk 编译脚本。
· sw.sh 启动器。
分别看一下这三个文件的实现:
[-->SampleWindow.java::SampleWindow]
package understanding.wms.samplewindow;
......
public class SampleWindow {
publicstatic 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中将详细介绍它们的作用以及计算原理
RectmInsets = new Rect();
RectmFrame = new Rect();
RectmVisibleInsets = new Rect();
Configuration mConfig = new Configuration();
// 窗口的Surface,在此Surface上进行的绘制都将在此窗口上显示出来
SurfacemSurface = new Surface();
// 用于在窗口上进行绘图的画刷
PaintmPaint = new Paint();
// 添加窗口所需的令牌,在4.2节将会对其进行介绍
IBindermToken = new Binder();
// 一个窗口对象,本例演示了如何将此窗口添加到WMS中,并在其上进行绘制操作
MyWindowmWindow = new MyWindow();
//WindowManager.LayoutParams定义了窗口的布局属性,包括位置、尺寸以及窗口类型等
LayoutParams mLp = new LayoutParams();
Choreographer mChoreographer = null;
//InputHandler 用于从InputChannel接收按键事件做出响应
InputHandler mInputHandler = null;
booleanmContinueAnime = true;
publicvoid Run() throws Exception{
Looper.prepare();
// 获取WMS服务
IWindowManager wms = IWindowManager.Stub.asInterface(
ServiceManager.getService(Context.WINDOW_SERVICE));
// 通过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);
}
publicvoid 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,希望当输入事件发生在此窗口之外时,其他窗口也可以接受输入事件
mLp.flags = mLp.flags | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
}
publicvoid 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()) {
thrownew RuntimeException("Failed creating Surface.");
}
// 基于WMS返回的InputChannel创建一个Handler,用于监听输入事件
//mInputHandler一旦被创建,就已经在监听输入事件了
mInputHandler= new InputHandler(mInputChannel, Looper.myLooper());
}
publicvoid uninstallWindow(IWindowManager wms) throws Exception {
// 从WMS处卸载窗口
mSession.remove(mWindow);
// 从WMS处移除之前添加的Token
wms.removeWindowToken(mToken);
}
publicvoid scheduleNextFrame() {
// 要求在显示系统刷新下一帧时回调mFrameRender,注意,只回调一次
mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION
, mFrameRender, null);
}
// 这个Runnable对象用以在窗口上描绘一帧
publicRunnable mFrameRender = new Runnable() {
@Override
publicvoid 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()函数中接收窗口的输入事件
classInputHandler extends InputEventReceiver {
Looper mLooper = null;
publicInputHandler(InputChannel inputChannel, Looper looper) {
super(inputChannel,looper);
mLooper= looper;
}
@Override
publicvoid 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。
classMyWindow 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所示。
图 4-1 SampleWindow在手机中的运行效果
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 Android显示系统的三个层次
在图4-2中:
· 第一个层次是UI框架层,其工作为在Surface上绘制UI元素以及响应输入事件。
· 第二个层次为WMS,其主要工作在于管理Surface的分配、层级顺序等。
· 第三层为SurfaceFlinger,负责将多个Surface混合并输出。
经过这个例子的介绍,相信大家对WMS的功能有了一个初步的了解。接下来,我们要进入WMS的内部,通过其启动过程一窥它的构成。
4.1.2WMS的构成
俗话说,一个好汉三个帮!WMS的强大是由很多重要的成员互相协调工作而实现的。了解WMS的构成将会为我们深入探索WMS打下良好的基础,进而分析它的启动过程,这是再合适不过了。
1.WMS的诞生
和其他的系统服务一样,WMS的启动位于SystemServer.java中ServerThread类的run()函数内。
[-->SystemServer.java::ServerThread.run()]
Public void run() {
......
WindowManagerService wm = null;
......
try {
......
// ①创建WMS实例
/* 通过WindowManagerService的静态函数main()创建WindowManagerService的实例。
注意main()函数的两个参数wmHandler和uiHandler。这两个Handler分别运行于由
ServerThread所创建的两个名为“WindowManager”和“UI”的两个HandlerThread中 */
wm =WindowManagerService.main(context, power, display, inputManager,
uiHandler,wmHandler,
factoryTest !=SystemServer.FACTORY_TEST_LOW_LEVEL,
!firstBoot, onlyCore);
// 添加到ServiceManager中去
ServiceManager.addService(Context.WINDOW_SERVICE,wm);
......
catch(RuntimeException e) {
......
}
......
try {
//②初始化显示信息
wm.displayReady();
} catch(Throwable e) {......}
......
try {
// ③通知WMS,系统的初始化工作完成
wm.systemReady();
} catch(Throwable e) {......}
......
}
由此可以看出,WMS的创建分为三个阶段:
· 创建WMS的实例。
· 初始化显示信息。
· 处理systemReady通知。
接下来,将通过以上三个阶段分析WMS从无到有的过程。
看一下WMS的main()函数的实现:
[-->WindowManagerService.java::WindowManagerSrevice.main()]
public static WindowManagerService main(finalContext context,
finalPowerManagerService pm, final DisplayManagerService dm,
finalInputManagerService im,
finalHandler uiHandler, final Handler wmHandler,
finalboolean haveInputMethods, final boolean showBootMsgs,
finalboolean onlyCore) {
finalWindowManagerService[] holder = new WindowManagerService[1];
// 通过由SystemServer为WMS创建的Handler新建一个WindowManagerService对象
// 此Handler运行在一个名为WindowManager的HandlerThread中
wmHandler.runWithScissors(newRunnable() {
@Override
publicvoid run() {
holder[0]= new WindowManagerService(context, pm, dm, im,
uiHandler,haveInputMethods, showBootMsgs, onlyCore);
}
}, 0);
returnholder[0];
}
注意Handler类在Android 4.2中新增了一个API:runWithScissors()。这个函数将会在Handler所在的线程中执行传入的Runnable对象,同时阻塞调用线程的执行,直到Runnable对象的run()函数执行完毕。
WindowManagerService.main()函数在ServerThread专为WMS创建的线程“WindowManager”上创建了一个WindowManagerService的新实例。WMS中所有需要的Looper对象,例如Handler、Choreographer等,将会运行在“WindowManager”线程中。
接下来看一下其构造函数,看一下WMS定义了哪些重要的组件。
[-->WindowManagerService.java::WindowManagerService.WindowManagerService()]
private WindowManagerService(Context context,PowerManagerService pm,
DisplayManagerService displayManager, InputManagerService inputManager,
Handler uiHandler,
booleanhaveInputMethods, boolean showBootMsgs, boolean onlyCore)
......
mDisplayManager=
(DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
mDisplayManager.registerDisplayListener(this,null);
Display[]displays = mDisplayManager.getDisplays();
/* 初始化DisplayContent列表。DisplayContent是Android4.2为支持多屏幕输出所引入的一个
概念。一个DisplayContent指代一块屏幕,屏幕可以是手机自身的屏幕,也可以是基于Wi-FiDisplay
技术的虚拟屏幕[3]*/
for(Display display : displays) {
createDisplayContentLocked(display);
}
.....
/* 保存InputManagerService。输入事件最终要分发给具有焦点的窗口,而WMS是窗口管理者,
所以WMS是输入系统中的重要一环。关于输入系统的内容将在第5章中深入探讨*/
mInputManager= inputManager;
// 这个看起来其貌不扬的mAnimator,事实上具有非常重要的作用。它管理着所有窗口的动画
mAnimator= new WindowAnimator(this, context, mPolicy);
// 在“UI“线程中将对另一个重要成员mPolicy,也就是WindowManagerPolicy进行初始化
initPolicy(uiHandler);
// 将自己加入到Watchdog中
Watchdog.getInstance().addMonitor(this);
......
}
第二步,displayReady()函数的调用主要是初始化显示尺寸的信息。其内容比较琐碎,这里就先不介绍了。不过值得注意的一点是,再displayReady()完成后,WMS会要求ActivityManagerService进行第一次Configuration的更新。
第三步,在systemReady()函数中,WMS本身将不会再做任何操作了,直接调用mPolicy的systemReady()函数。
2.WMS的重要成员
总结一下在WMS的启动过程中所创建的重要成员,参考图4-3。
图 4-3 WMS的重要成员
以下是对图4-3中重要成员的简单介绍:
· mInputManager,InputManagerService(输入系统服务)的实例。用于管理每个窗口的输入事件通道(InputChannel)以及向通道上派发事件。关于输入系统的详细内容将在本书第5章详细探讨。
· mChoreographer,Choreographer的实例,在SampleWindow的例子中已经见过了。Choreographer的意思是编舞指导。它拥有从显示子系统获取VSYNC同步事件的能力,从而可以在合适的时机通知渲染动作,避免在渲染的过程中因为发生屏幕重绘而导致的画面撕裂。从这个意义上来讲,Choreographer的确是指导Android翩翩起舞的大师。WMS使用Choreographer负责驱动所有的窗口动画、屏幕旋转动画、墙纸动画的渲染。
· mAnimator,WindowAnimator的实例。它是所有窗口动画的总管(窗口动画是一个WindowStateAnimator的对象)。在Choreographer的驱动下,逐个渲染所有的动画。
· mPolicy,WindowPolicyManager的一个实现。目前它只有PhoneWindowManager一个实现类。mPolicy定义了很多窗口相关的策略,可以说是WMS的首席顾问!每当WMS要做什么事情的时候,都需要向这个顾问请教应当如何做。例如,告诉WMS某一个类型的Window的ZOrder的值是多少,帮助WMS矫正不合理的窗口属性,会为WMS监听屏幕旋转的状态,还会预处理一些系统按键事件(例如HOME、BACK键等的默认行为就是在这里实现的),等等。所以,mPolicy可谓是WMS中最重要的一个成员了。
· mDisplayContents,一个DisplayContent类型的列表。Android4.2支持基于Wi-fi Display的多屏幕输出,而一个DisplayContent描述了一块可以绘制窗口的屏幕。每个DisplayContent都用一个整型变量作为其ID,其中手机默认屏幕的ID由Display.DEFAULT_DISPLAY常量指定。DisplayContent的管理是由DisplayManagerService完成的,在本章不会去探讨DisplayContent的实现细节,而是关注DisplayContent对窗口管理与布局的影响。
下面的几个成员的初始化并没有出现在构造函数中,不过它们的重要性一点也不亚于上面几个。
· mTokenMap,一个HashMap,保存了所有的显示令牌(类型为WindowToken),用于窗口管理。在SampleWindow例子中曾经提到过,一个窗口必须隶属于某一个显示令牌。在那个例子中所添加的令牌就被放进了这个HashMap中。从这个成员中还衍生出几个辅助的显示令牌的子集,例如mAppTokens保存了所有属于Activity的显示令牌(WindowToken的子类AppWindowToken),mExitingTokens则保存了正在退出过程中的显示令牌等。其中mAppTokens列表是有序的,它与AMS中的mHistory列表的顺序保持一致,反映了系统中Activity的顺序。
· mWindowMap,也是一个HashMap,保存了所有窗口的状态信息(类型为WindowState),用于窗口管理。在SampleWindow例子中,使用IWindowSession.add()所添加的窗口的状态将会被保存在mWindowMap中。与mTokenMap一样,mWindowMap一样有衍生出的子集。例如mPendingRemove保存了那些退出动画播放完成并即将被移除的窗口,mLosingFocus则保存了那些失去了输入焦点的窗口。在DisplayContent中,也有一个windows列表,这个列表存储了显示在此DisplayContent中的窗口,并且它是有序的。窗口在这个列表中的位置决定了其最终显示时的Z序。
· mSessions,一个List,元素类型为Session。Session其实是SampleWindow例子中的IWindowSession的Bn端。也就是说,mSessions这个列表保存了当前所有想向WMS寻求窗口管理服务的客户端。注意Session是进程唯一的。
· mRotation,只是一个int型变量。它保存了当前手机的旋转状态。
WMS定义的成员一定不止这些,但是它们是WMS每一种功能最核心的变量。读者在这里可以线对它们有一个感性的认识。在本章后续的内容里将会详细分析它们在WMS的各种工作中所发挥的核心作用。
4.1.3 初识WMS的小结
这一节通过SampleWindow的例子向读者介绍了WMS的客户端如何使用窗口,然后通过WMS的诞生过程简单剖析了一下WMS的重要成员组成,以期通过本节的学习能够为后续的学习打下基础。
从下一节开始,我们将会深入探讨WMS的工作原理。
4.2 WMS的窗口管理结构
经过上一节的介绍,读者应该对WMS的窗口管理有了一个感性的认识。从这一节开将深入WMS的内部去剖析其工作流程。
根据前述内容可知,SampleWindow添加窗口的函数是IWindowSession.add()。IWindowSession是WMS与客户端交互的一个代理,add则直接调用到了WMS的addWindow()函数。我们将从这个函数开始WMS之旅。本小节只讨论它的前半部分。
注意由于篇幅所限,本章不准备讨论removeWindow的实现。
[-->WindowManagerService.java::WindowManagerService.addWindow()Part1]
public int addWindow(Session session, IWindowclient, 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则表示了窗口所隶属的对象
// 对于子窗口来说,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;
}
}
booleanaddToken = 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
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,以便确定窗口将被显示到哪一个屏幕上。
4.2.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对象而已。
为了验证这一点,来看一下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和InputMethod的Token
Wallpaper的Token声明在WallpaperManagerService中。参考以下代码:
[-->WallpaperManagerService.java::WallpaperManagerService.bindWallpaperComponentLocked()]
BooleanbindWallpaperComponentLocked(......) {
......
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()]
booleandetachWallpaperLocked(WallpaperData wallpaper){
......
mIWindowManager.removeWindowToken(wallpaper.connection.mToken);
......
}
再此之后,如果这个被detach的Wallpaper想再要创建窗口便不再可能了。
WallpaperManagerService使用WindowToken对一个特定的Wallpaper做出了如下限制:
· Wallpaper只能创建TYPE_WALLPAPER类型的窗口。
· Wallpaper显示的生命周期由WallpaperManagerService牢牢地控制着。仅有当前的Wallpaper才能创建窗口并显示内容。其他的Wallpaper由于没有有效的Token,而无法创建窗口。
InputMethod的Token的来源与Wallpaper类似,其声明位于InputMethodManagerService的startInputInnerLocked()函数中,取消声明的位置在InputmethodManagerService的unbindCurrentMethodLocked()函数。InputMethodManagerService通过Token限制着每一个InputMethod的窗口类型以及显示生命周期。
(2) Activity的Token
Activity的Token的使用方式与Wallpaper和InputMethod类似,但是其包含更多的内容。毕竟,对于Activity,无论是其组成还是操作都比Wallpaper以及InputMethod复杂得多。对此,WMS专为Activity实现了一个WindowToken的子类:AppWindowToken。
既然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。
· keyDispatchingTimeout(),窗口没能按时完成输入事件的处理。这个回调将会导致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的顺序对窗口的顺序影响非常大。
4.2.2 理解WindowState
从WindowManagerService.addWindow()函数的实现中可以看出,当向WMS添加一个窗口时,WMS会为其创建一个WindowState。WindowState表示一个窗口的所有属性,所以它是WMS中事实上的窗口。这些属性将在后面遇到时再做介绍。
类似于WindowToken,WindowState在显示组件一侧也有个对应的类型: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()进行声明);而mWindowToken的键值一定是IWindow的Bp端。
关于WindowState的更多细节将在后面的讲述中进行介绍。不过经过上面的分析,不难得到WindowToken和WindowState之间的关系,参考图4-4。
图 4-4 WindowToken与WindowState的关系
更具体一些,以一个正在回放视频并弹出两个对话框的Activity为例,WindowToken与WindowState的意义如图4-5所示。
图 4-5WindowState与WindowToken的从属关系
4.2.3理解DisplayContent
如果说WindowToken按照窗口之间的逻辑关系将其分组,那么DisplayContent则根据窗口的显示位置将其分组。隶属于同一个DisplayContent的窗口将会被显示在同一个屏幕中。每一个DisplayContent都对应这一个唯一的ID,在添加窗口时可以通过指定这个ID决定其将被显示在那个屏幕中。
DisplayContent是一个非常具有隔离性的一个概念。处于不同DisplayContent的两个窗口在布局、显示顺序以及动画处理上不会产生任何耦合。因此,就这几个方面来说,DisplayContent就像一个孤岛,所有这些操作都可以在其内部独立执行。因此,这些本来属于整个WMS全局性的操作,变成了DisplayContent内部的操作了。
4.3 理解窗口的显示次序
在addWindow()函数的前半部分中,WMS为窗口创建了用于描述窗口状态的WindowState,接下来便会为新建的窗口确定显示次序。手机屏幕是以左上角为原点,向右为X轴方向,向下为Y轴方向的一个二维空间。为了方便管理窗口的显示次序,手机的屏幕被扩展为了一个三维的空间,即多定义了一个Z轴,其方向为垂直于屏幕表面指向屏幕外。多个窗口依照其前后顺序排布在这个虚拟的Z轴上,因此窗口的显示次序又被称为Z序(Z order)。在这一节中将深入探讨WMS确定窗口显示次序的过程以及其影响因素。
4.3.1 主序、子序和窗口类型
看一下WindowState的构造函数:
[-->WindowState.java::WindowState.WindowState()]
WindowState(WindowManagerService service, Sessions, IWindow c, WindowToken token,
WindowState attachedWindow, int seq, WindowManager.LayoutParams a,
intviewVisibility, 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()完成的。
表4-1与表4-2列出了所有可能的窗口类型以及其主序与子序的值。
表 4-1 窗口的主序
窗口类型 | 主序 | 窗口类型 | 主序 |
TYPE_UNIVERSE_BACKGROUND | 11000 | TYPE_WALLPAPER | 21000 |
TYPE_PHONE | 31000 | TYPE_SEARCH_BAR | 41000 |
TYPE_RECENTS_OVERLAY | 51000 | TYPE_SYSTEM_DIALOG | 51000 |
TYPE_TOAST | 61000 | TYPE_PRIORITY_PHONE | 71000 |
TYPE_DREAM | 81000 | TYPE_SYSTEM_ALERT | 91000 |
TYPE_INPUT_METHOD | 101000 | TYPE_INPUT_METHOD_DIALOG | 111000 |
TYPE_KEYGUARD | 121000 | TYPE_KEYGUARD_DIALOG | 131000 |
TYPE_STATUS_BAR_SUB_PANEL | 141000 | 应用窗口与未知类型的窗口 | 21000 |
表 4-2 窗口的子序
子窗口类型 | 子序 |
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需要根据当前所有同类型窗口的数量为每个窗口计算最终的现实次序。
4.3.2 通过主序与子序确定窗口的次序
回到WMS的addWindow()函数中,继续往下看:
[-->WindowManagerService.java::WindowManagerService.addWindow()]
public int addWindow(Session session, IWindowclient, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
RectoutContentInsets, 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());
......
}
......
returnres;
}
这里有两个关键点:
· 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()]
privatefinal void assignLayersLocked(WindowList windows) {
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;
}
......
}
......
// 向当前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。如下所示:
[-->WindowManagerService.java::assignLayersLocked()]
finalWindowStateAnimator winAnimator = w.mWinAnimator;
......
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;
}
......
对于绝大多数窗口而言,其对应的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貌似也懒得解决这个问题。
4.3.3 更新显示次序到Surface
再回到WMS的addWindow()函数中,发现再没有可能和显示次序相关的代码了。mAnimLayer是如何发挥自己的作用呢?不要着急,事实上,新建的窗口目前尚无Surface。回顾一下SimpleWindow例子,在执行session.relayout()后,WMS才为新窗口分配了一块Surface。也就是说,只有执行relayout()之后才会为新窗口的Surface设置新的显示次序。
为了不中断对显示次序的调查进展,就直接开门见山地告诉大家,设置显示次序到Surface的代码位于WindowStateAnimator. prepareSurfaceLocked()函数中,是通过Surface.setLayer()完成的。在4.5节会深入为大家揭开WMS动画子系统的面纱。
4.3.4 关于显示次序的小结
这一节讨论了窗口类型对窗口显示次序的影响。窗口根据自己的类型得出其主序及子序,然后addWindowToListInOrderLocked()根据主序、子序以及其所属的Activity的顺序,按照升序排列在DisplayContent的mWindows列表中。然后assignLayersLocked()为mWindows中的所有窗口分配最终的显示次序。之后,WMS的动画系统将最终的显示次序通过Surface.setLayer()设置进SurfaceFlinger。
[1]关于Choreographer,请参考邓凡平的博客《Android Project Butter分析》(http://blog.csdn.net/innost/article/details/8272867)。
[2]读者可阅读《深入理解Android 卷I》第4章“深入理解Zygote”来了解和zygote相关的知识
[3]关于Wi-Fi Display的详细信息,请读者参考http://blog.csdn.net/innost/article/details/8474683的介绍。