《深入理解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的client怎样申请、渲染并注销自己的窗体。
同一时候这也初步地反应了WMS的工作方式。
这个样例非常easy。仅仅有三个文件:
· 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 是client向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 "[email protected]"
注意app_process事实上就是大名鼎鼎的zygote。只是。仅仅有使用--zygote參数启动时它才会给改名为zygote[2],否则就像java –jar命令一样,运行指定类的main静态函数。
在手机中运行该脚本,其运行结果是一个灰色的方块不断地从屏幕左側移动到右側,如图4-1所看到的。
图 4-1 SampleWindow在手机中的运行效果
2.初识窗体的创建、绘制与销毁
SampleWindow的这段代码尽管简单,可是却非常好地提炼了一个窗体的创建、绘制以及销毁的过程。注意,本例没有使用不论什么 WMS以外的系统服务。也没有使用Android系统四大组件的框架,也就是说。假设你愿意,能够利用WMS实现自己的UI与应用程序框架。这样就能够衍生出一个新的平台了。
总结在client创建一个窗体的步骤:
· 获取IWindowSession和WMS实例。client能够通过IWindowSession向WMS发送请求。
· 创建并初始化WindowManager.LayoutParams。注意这里是WindowManager下的LayoutParams,它继承自ViewGroup.LayoutParams类,并扩展了一些窗体相关的属性。当中最重要的是type属性。这个属性描写叙述了窗体的类型,而窗体类型正是WMS对多个窗体进行ZOrder排序的依据。
· 向WMS加入一个窗体令牌(WindowToken)。本章兴许将分析窗体令牌的概念,眼下读者仅仅要知道。窗体令牌描写叙述了一个显示行为,而且WMS要求每一个窗体必须隶属于某一个显示令牌。
· 向WMS加入一个窗体。必须在LayoutParams中指明此窗体所隶属于的窗体令牌,否则在某些情况下加入操作会失败。
在SampleWindow中,不设置令牌也可成功?完毕加入操作。由于窗体的类型被设为TYPE_SYSTEM_ALERT,它是系统窗体的一种。
而对于系统窗体。WMS会自己主动为其创建显示令牌,故无需client担心。此话题将会在后文进行更具体的讨论。
· 向WMS申请对窗体进行又一次布局(relayout)。所谓的又一次布局,就是依据窗体新的属性去调整其Surface相关的属性。或者又一次创建一个Surface(比如窗体尺寸变化导致之前的Surface不满足要求)。向WMS加入一个窗体之后,其仅仅是将它在WMS中进行了注冊而已。仅仅有经过又一次布局之后,窗体才拥有WMS为其分配的画布。有了画布,窗体之后就能够随时进行绘制工作了。
而窗体的绘制步骤例如以下:
· 通过Surface.lock()函数获取能够在其上作画的Canvas实例。
· 使用Canvas实例进行作画。
· 通过Surface.unlockCanvasAndPost()函数提交绘制结果。
提示关于Surface的原理与使用方法,请參考《卷 I》第8章“深入理解Surface系统”。
这是对Surface作画的标准方法。
在client也能够通过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显示在屏幕上时,就是用户所看到的窗体了。client向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寻求窗体管理服务的client。
注意Session是进程唯一的。
· mRotation,仅仅是一个int型变量。
它保存了当前手机的旋转状态。
WMS定义的成员一定不止这些,可是它们是WMS每一种功能最核心的变量。读者在这里能够线对它们有一个感性的认识。在本章兴许的内容里将会具体分析它们在WMS的各种工作中所发挥的核心作用。
4.1.3初识WMS的小结
这一节通过SampleWindow的样例向读者介绍了WMS的client怎样使用窗体,然后通过WMS的诞生过程简单剖析了一下WMS的重要成员组成。以期通过本节的学习能够为兴许的学习打下基础。
从下一节開始,我们将会深入探讨WMS的工作原理。
4.2 WMS的窗体管理结构
经过上一节的介绍,读者应该对WMS的窗体管理有了一个感性的认识。从这一节开将深入WMS的内部去剖析其工作流程。
依据前述内容可知,SampleWindow加入窗体的函数是IWindowSession.add()。
IWindowSession是WMS与client交互的一个代理,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) {
// 首先检查权限,没有权限的client不能加入窗体
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出场!
依据client的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()的目的就是如此。它要求client必须拥有SYSTEM_ALERT_WINDOW或INTERNAL_SYSTEM_WINDOW权限才干创建系统类型的窗体。
2.向WMS声明WindowToken
既然应用组件在创建一个窗体时必须指定一个有效的WindowToken才行,那么WindowToken到底该怎样声明呢?
在SampleWindow应用中,使用wms.addWindowToken()函数声明mToken作为它的令牌,所以在加入窗体时,通过设置lp.token为mToken向WMS进行出示。从而获得WMS加入窗体的许可。
这说明。仅仅要是一个Binder对象(随便一个),都能够作为Token向WMS进行声明。对于WMS的client来说,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中。键值为client用于声明Token的Binder实例。与addWindow()函数中隐式地创建WindowToken不同。这里的WindowToken被声明为显式的。隐式与显式的差别在于,当隐式创建的WindowToken的最后一个窗体被移除后,此WindowToken会被一并从mTokenMap中移除。显式创建的WindowToken仅仅能通过removeWindowToken()显式地移除。
addWindowToken()这个函数告诉我们。WindowToken事实上有两层含义:
· 对于显示组件(client)而言的Token。是随意一个Binder的实例,对显示组件(client)来说仅仅是一个创建窗体的令牌,没有其它的含义。
· 对于WMS而言的WindowToken这是一个WindowToken类的实例,保存了相应于client一側的Token(Binder实例)。并以这个Token为键。存储于mTokenMap中。
client一側的Token是否已被声明,取决于其相应的WindowToken是否位于mTokenMap中。
注意在普通情况下,称显示组件(client)一側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在client是否和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将被用于视频回放、相机预览或游戏绘制。为了不让这个子窗体覆盖住全部的父窗体中承载的其它控件(如拍照button,播放器控制button等)。它必须位于父窗体之后。
从表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的介绍。