转载:https://blog.csdn.net/Innost/article/details/47660193
本章主要内容:
· 示例最原始最简单的窗口创建方法
· 研究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
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的学习之旅。
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 {
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中,并在其上进行绘制操作
// 自己实现,继承IWindow.Stub
MyWindow mWindow = new MyWindow();
//WindowManager.LayoutParams定义了窗口的布局属性,包括位置、尺寸以及窗口类型等
LayoutParams mLp = new LayoutParams();
Choreographer mChoreographer = null;
//InputHandler 用于从InputChannel接收按键事件做出响应
// 自己去实现,继承InputEventReceiver类
InputHandler mInputHandler = null;
boolean mContinueAnime = true;
public void 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(自己创建注册到WMS),任何一个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());
}
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()函数中接收窗口的输入事件
class InputHandler extends InputEventReceiver {
Looper mLooper = null;
public InputHandler(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)
将这两个文件放在frameworks/base/cmds/samplewindow/下,然后用make或mm命令进行编译。最终生成的结果是samplewindow.jar,文件位置在out/target/
提示读者可使用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,否则就像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端,WMS是Bp端(在例子中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的内部,通过其启动过程一窥它的构成。
俗话说,一个好汉三个帮!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创建的wmHandler新建一个WindowManagerService对象
// 此Handler运行在一个名为WindowManager的HandlerThread中
wmHandler.runWithScissors(new Runnable() {
@Override
public void run() {
holder[0]= new WindowManagerService(context, pm, dm, im,
uiHandler,haveInputMethods, showBootMsgs, onlyCore);
}
}, 0);
return holder[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= 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端(在WMS)。也就是说,mSessions这个列表保存了当前所有想向WMS寻求窗口管理服务的Bp客户端,。注意Session是进程唯一的。
· mRotation,只是一个int型变量。它保存了当前手机的旋转状态。
WMS定义的成员一定不止这些,但是它们是WMS每一种功能最核心的变量。读者在这里可以线对它们有一个感性的认识。在本章后续的内容里将会详细分析它们在WMS的各种工作中所发挥的核心作用。
这一节通过SampleWindow的例子向读者介绍了WMS的客户端如何使用窗口,然后通过WMS的诞生过程简单剖析了一下WMS的重要成员组成,以期通过本节的学习能够为后续的学习打下基础。
从下一节开始,我们将会深入探讨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,也就是手机屏幕 */
final DisplayContent 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,以便确定窗口将被显示到哪一个屏幕上。
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中,键值为客户端用于声明Token的Binder实例
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()]
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()]
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的顺序对窗口的顺序影响非常大。
从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的从属关系
如果说WindowToken按照窗口之间的逻辑关系将其分组,那么DisplayContent则根据窗口的显示位置将其分组。隶属于同一个DisplayContent的窗口将会被显示在同一个屏幕中。每一个DisplayContent都对应这一个唯一的ID,在添加窗口时可以通过指定这个ID决定其将被显示在那个屏幕中。
DisplayContent是一个非常具有隔离性的一个概念。处于不同DisplayContent的两个窗口在布局、显示顺序以及动画处理上不会产生任何耦合。因此,就这几个方面来说,DisplayContent就像一个孤岛,所有这些操作都可以在其内部独立执行。因此,这些本来属于整个WMS全局性的操作,变成了DisplayContent内部的操作了。
在addWindow()函数的前半部分中,WMS为窗口创建了用于描述窗口状态的WindowState,接下来便会为新建的窗口确定显示次序。手机屏幕是以左上角为原点,向右为X轴方向,向下为Y轴方向的一个二维空间。为了方便管理窗口的显示次序,手机的屏幕被扩展为了一个三维的空间,即多定义了一个Z轴,其方向为垂直于屏幕表面指向屏幕外。多个窗口依照其前后顺序排布在这个虚拟的Z轴上,因此窗口的显示次序又被称为Z序(Z order)。在这一节中将深入探讨WMS确定窗口显示次序的过程以及其影响因素。
看一下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需要根据当前所有同类型窗口的数量为每个窗口计算最终的现实次序。
回到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的所有窗口分配最终的显示次序
// 根据mWindows的存储顺序对所有的WindowState的主序和子序进行调整。
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 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貌似也懒得解决这个问题。 再回到WMS的addWindow()函数中,发现再没有可能和显示次序相关的代码了。mAnimLayer是如何发挥自己的作用呢?不要着急,事实上,新建的窗口目前尚无Surface。回顾一下SimpleWindow例子,在执行session.relayout()后,WMS才为新窗口分配了一块Surface。也就是说,只有执行relayout()之后才会为新窗口的Surface设置新的显示次序。 为了不中断对显示次序的调查进展,就直接开门见山地告诉大家,设置显示次序到Surface的代码位于WindowStateAnimator. prepareSurfaceLocked()函数中,是通过Surface.setLayer()完成的。在4.5节会深入为大家揭开WMS动画子系统的面纱。 这一节讨论了窗口类型对窗口显示次序的影响。窗口根据自己的类型得出其主序及子序,然后addWindowToListInOrderLocked()根据主序、子序以及其所属的Activity的顺序,按照升序排列在DisplayContent的mWindows列表中。然后assignLayersLocked()为mWindows中的所有窗口分配最终的显示次序。之后,WMS的动画系统将最终的显示次序通过Surface.setLayer()设置进SurfaceFlinger。 在本节中将讨论WMS如何对窗口进行布局。窗口布局是WMS的一项重要工作内 (LayoutParameters)为窗口重建 Surface(如果有必要),并将其放置在屏幕的指定位置。同 图 4-6 属性更新的总体过程 图4-6揭示了WMS布局三步走的流程。即将讨论的relayoutWindow()函数属于第一 首先我们要理解relayoutWindow()的参数的意义。WMS的relayoutWindow()的签名如下: 口 session:调用者所在进程的Session实例。 /*①接下来长长的一段代码用来根据用户传入的参数更新windowstate对象的相关属性。它们是: //③接下来更新窗口焦点、壁纸可见性以及屏幕旋转等 //焦点窗口的变化可能会引发输入法窗口的变化,所以需要对输入法进行更新 /*窗口有一个flag为FLAG_SHOW_WALLPAPER。意思就是此窗口要求使用系统当前的壁纸作为其 } /*回顾在4.3节所讨论的内容,一旦windowList中的次序发生变化后,需要调用assigntayersLocked() /*新的窗口被显示,或者被隐藏,意味着屏幕的旋转状态可能会发生变化。因为窗口所属的Activity //向AMS更新Configuration,因为屏幕可能发生旋转 return ......; } 口 将布局结果返回给relayoutWindow()的返回者。 接下来开始剖析WMS的布局过程的征途吧。当然是从performLayoutAndPlaceSurfacesLocked()开始。 performLayoutAndPlaceSurfacesLockedLoop(); 实现比较简单,很明显可以看出,此函数中有意义的事情都是在- } [WindowManagerService.java-->WindowManagerService.requestTraversallocked()] } 这个实现自WindowManagerFuncs接口的函数向外界提供了一个触发WMS立刻进 Java GC很类似。僵尸窗口是指其持有者已经异常退出或者处于隐藏状态却拥有Surface的窗口。 if(mForceRemoves!=null){ iWindowstate ws=mForceRemoves.get(i); } } //这次又调用了performLayoutAndPlaceSurfacesLockedInner()... } } } 这个函数仍不算复杂。主要的布局逻辑应该位于performLayoutAndPlaceSurfacesLockedlnner()中。 至于第二个条件,mLayoutRepeatCount<6,则表示最多连续重 接下来要继续深入WMS的布局过程。performLayoutAndPlaceSurfacesLockedlnner() [WindowManagerService.java--> 对布局结果进行检查,是否有必要重新对DisplayContent执行布局; 完成布局后的策略处理; 可以看出,这个函数的整体逻辑是比较清晰的。不过由于它的全能性,每一个步骤的内 [WindowManagerService.java-> //将所有处于退出状态的Tokens标记为不可见 for(i=mExitingAppTokens.size()-1;i>=0;i--){ /*②初始化mInnerFields中的状态变量。客户端可以通过窗口的LayoutParameters中的一些属性 final int defaultDw=defaultInfo.logicalwidth; if(mstrictModeFlash!=null){ //接下来的两个变量将在后续遇到时再作介绍。在这里,它们都被设置为假 对布局结果进行检查,是否有必要重新对Displaycontent执行布局; } 完成布局后的策略处理; 在布局的前期,主要工作有: 框。它们其实是两块Surface,而它们的显示次序分别为1000000和1000001,高于任 需要再次强调一下,Android4.2之后将支持多块屏幕输出(目前第二块屏幕是WiFi- /*虽然isDefaultDisplay仅仅是一个布尔变量,不过其作用很大。Default display //接下来是处理pendingLayoutchanges字段的代码。在真正开始窗口的布局前,需要先 //清空pendingLayoutchanges字段 } displayContent.pendingLayoutChanges l=mPolicy.finishPostLayoutPolicyLw(); //如果PhonewindowManager认为布局结果仍需进行调整,则重新再来一遍 //完成布局后的策略处理 } 接着上面的讨论,深入研究第一部分的布局循环来探究WMS是如何完成窗口布局的。 (1)初识performLayoutLockedlnner() displayContent.layoutNeeded = false; //更新窗口的布局版本号 } } //③接着对所有的非顶级窗口进行布局 //布局完成后,窗口的尺寸和位置可能发生变化,此时,需要输入系统更新窗口的状态 //④通知WindowManagerPolicy,本次布局已完成。可以对布局过程中所使用的资源进行清理 可以看出,窗口布局过程的规律性还是很强的。 (2)窗口布局的8个准绳 在PhoneWindowManager.beginLayoutLw()中究竟为布局准备了什么参数呢?简单来说, 这些参数描述了屏幕上的8个矩形区域,而这8个区域构成了PhoneWindowManager布局窗 中,状态栏与导航栏仍然是可见的,而System区域则认为它们是不可见的。 图 4-8 竖屏下布局参数所指示的位置 图 4-9 横屏下布局参数所指示的位置 一旦这8个矩形区域确定下来,PhoneWindowManager 就准备好对窗口进行布局了。 (3)窗口布局的4个参数 上一小节讨论了PhoneWindowManager的beginLayoutLw()所准备好的布局参数。接下来 /*接下来的4个矩形是重点。不要介意mTmp***Frame这4个变量。PhonewindowManager不希望 } } layoutWindowLw()首先计算了pf、df、cf和vf这4个矩形,然后将这4个矩形作为参数 //设置mDisplayFrame为df final Rect contentInsets=mContentInsets; 整理一下computFrameLw()的产出。 (5)关于窗口布局的小结 到此为止,performLayoutLockedlnner()函数的整个工作流程的分析便完成了。perform- 局变量的值为最新,以便在后续工作中根据这些布局变量的取值来放置各个窗口的Surface, 图4-10 performLayoutLockedlnner()的工作流程 上一节所探讨的内容主要围绕在窗口位置尺寸的布局计算上。经过perform- flag的作用也有限制,如果用户设置了密码、图形等安全解锁措施,则在用户输入解 ...... ...... } //③通过WMP的finishpostLayoutPolicyLw(),修改系统窗口及锁屏界面的可见性 } //如果PhonewindowManager认为布局结果仍需进行调整,则重新再来一遍 (1)beginPostLayoutPolicyLw()分析 beginPostLayoutPolicyLw()在PhoneWindowManager中的实现比预期的要简单很多,仅仅 相信读者应该能想到三个函数的作用了:beginXXX()用来初始化参数,applyXXX()用来 (2)applyPostLayoutPolicyLw()分析 [Phone WindowManager java-->PhoneWindowManager.applyPostLayoutPolicyLw()] } } //如果有窗口的类型为TYPE_KEYGUARD,表示正在显示锁屏界面 boolean applywindow = attrs.type>=FIRST_APPLICATTON_WINDOW //接下来的操作针对满屏的应用窗口 //处理满屏窗口的ELAG_DISMISS_KEYGUARD } } } 在遍历所有的窗口之后,beginPostLayoutPolicyLw()中遇到的状态变量都已被设置了合适 不同的地方仅仅在于带有这个flag的窗口的类型是否是TYPEKEYGUARD。 (3)finishPostLayoutPolicyLw()分析 接下来,finishPostLayoutPolicyLw()将会根据上述状态变量,调整系统窗口与锁屏界面的 3.处理 pendingLayoutChanges 窗口布局完成了,布局检查也完成了,pendingLayoutChanges也被设置为合适的值。 do{ //处理FINISH_LAYOUT_REDO_WALLPAPER。通过调adjustWallpaperwindowsLocked() &&((displaycontent.pendingLayoutChanges&WindowManagerPolicy.FINISH LAYOUT_ REDO_WALLPAPER)!=0) //处理FINISH_LAYOUT_REDO_CONEIG。调用updateorientationFromAppTokensLocked() } //处理FINISH_LAYOUT_REDO_LAYOUT。这个简单多了,仅仅是将layoutNeeded设置为true //调用performLayoutLockedInner() 进行布局 ...... } 4.DisplayContent的布局后处理 在布局过程几经周折离开布局循环后,窗口的位置与尺寸都已经确定。不过,确定的仅 [WindowManagerService.java->WindowManager. 顺带提一句,Dimming效果也是handleNotobscuredLocked()发起的。而屏幕亮 //关于壁纸的一些操作,在后续的章节中再作介绍 //接下来就是设置Surface的位置与尺寸了 if(w.mHasSurface){ 在此时此刻被更新。mShownFrane是由动画系统根据窗口动画的进度实时计算的。在 ...... /*将位置尺寸、ContentInsets或VisibleInsets在布局过程中发生变化的窗口添加到 } //完成布局后的策略处理 ........ DisplayContent的布局后处理的主要工作内容是: 完成布局后处理后,当前DisplayContent的布局就完成了,所有的窗口都已各就各位。 5.关于DisplayContent布局的小结 经历了漫长的代码分析,DisplayContent布局过程终于完结了。再次回顾一下布局过程中 在布局的最终阶段中,所有的DisplayContent都已基本完成布局(之所以说基本完成, 在本章前面的讨论中,曾经多次看到WindowStateAnimator的身影: 这里再强调一下,WMS不负责窗口的具体绘制,因此WMS动画系统仅会影响窗口 在正式开始WMS动画系统的探讨之前,有必要先了解一下Android动画的工作原理。 1.Animation类与Transform类 Android提供了平移(Translate)、缩放(Scale)、旋转(Rotate)以及透明度(Alpha)4种类 到真正开始执行动画的时间间隔,滞后时间往往用于两个动画的衔接。倘若让这些时间参数参 它的返回值实际上是一个余弦函数的图像。如图4-11 以最简单的AlphaAnimation为例,applyTransformation()的实现如下所示: t.setAlpha(alpha+((mToAlpha-alpha)*interpolatedTime)); 非常简单!这完全得益于Animation类优秀的设计。至此,Animation类的工作原理已经 2.动画驱动器Choreographer类 Choreographer类是Android4.2新增的一个API。此类的功能与Handler 3.使用Choreographer 和Animation实现动画 在学习Choreographer和Animation的使用方法后,就可以通过它们勾勒出Android动画 //一个Animation //将负责执行渲染的mAnimationRunnable抛给Choreographer //这个Runnable实现了如何渲染一帧 } } } } 可以看出,在Android4.2中实现动画的方法为: getTransformation()函数获取动画对象所需要进行的变换,然后根据变换对动画对象进行 虽然Cheoregrapher用来处理动画,但是其postCalback()以及postFrameCalback()函 学习Android动画的实现原理之后将以此为基础,研究WMS的动画子系统的框架。 } 负责处理动画帧的mAnimationRunnable的定义为: //将布局系统提供的参数“解压”到动画系统,这个话题随后再讨论 } } } 也就是说,WMS动画的渲染集中实现在WindowAnimator类的animateLocked()函数中。 1.WindowAnimator的组成 除了DimAnimator和DisplayContentsAnimator之外,其他的Animator都有一个名为 图4-12 Animator 的从属关系 2.WindowAnimator的animateLocked()分析 需要注意的是,WindowAnimator的animateLocked()函数并不是只在运行一个动画, 可以看出,影响窗口的最终显示位置的Animator是AppWindowAnimator、 1.WindowStateAnimator初探 2.开始窗口动画 (1)动画的选择与设置 (2)从布局系统到动画系统 3.WSA的Transformation 计算 在这段代码中,和我们讨论的内容相关的有以下两点: 接下来看一下WSA的stepAnimationLocked()是如何更新其Transformation的。 (2)stepAnimationLocked()分析 [WindowStateAnimator.java->WindowStateAnimator.stepAnimationLocked()] 4.WSA的动画渲染 经过stepAnimationsLocked()之后,WSA的状态已经准备好正式渲染动画帧了。回到 prepareSurfaceLocked()是在众多Animator类中WSA所特有的函数。其他Animator的 (1)mShownFrame、Surface 变换矩阵以及mShownAlpha的计算 prepareSurfaceLocked()函数的第一个工作就是通过调用computeShownFrameLocked()计 这个函数应用了很多的矩阵乘法操作,注意矩阵乘法是不满足交换律的。例如一个平 [WindowStateAnimator.java-->WindowStateAnimator.computeShownFrameLocked()] //获取窗口所在DisplayContent下的ScreenRotationAnimation (2)设置变换到Surface 看下prepareSurfaceLocked()的实现: 5.窗口的绘制状态与从新建到显示的过程 在布局系统与动画系统中曾多次看到窗口的绘制状态这个概念。窗口绘制状态的迁移体 动画系统在处理一帧动画时,同时保持着WMS.mWindowMap以及WMS.mAnimator两 1.从布局系统到动画系统 布局的最终阶段,也就是performLayoutAndPlaceSurfacesLocked()的最后位置,会 2.从动画系统到布局系统 在分析WindowAnimator的animateLocked()函数时,曾经说明过mBulkUpdateParams和 这一节以WindowAnimator与WindowStateAnimator为例对WMS的动画系统做了介绍。 [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的介绍。
4.3.3 更新显示次序到Surface
4.3.4 关于显示次序的小结
4.4窗口的布局
容,而且其过程非常复杂,所以这将是本章中必须啃也是最难啃的一块骨头。回顾一下
SampleWindow这个例子,将窗口添加到WMS后,需要执行IWindowSession.relayout()函
数后才能获得一块可供作画的Surface,并将其放置在SampleWindow例子所指定的位置
上。简单来说,IWindowSession.relayout()函数的作用就在于根据客户端提供的布局参数
IWindowSession.add()函数一样,IWindowSession.relayoutWindow()函数对应于WMS的
relayoutWindow()函数。
事实上,relayoutWindow()函数并不是本节的重点,它是用来引出WMS中最重要也是最
复杂的一个函数—performLayoutAndPlaceSurfacesLocked()。relayoutWindow()函数修改指定
窗口的布局参数,然后performLayoutAndPlaceSurfacesLocked()遍历所有窗口并对它们进行重
新布局。这种牵一发而可能动全身的做法看似效率低下,但是确实很有必要,因为多个窗口
之间的布局是相互影响的。
另外,通过这一节的学习与分析,读者将加深对WindowState和WindowToken这两个概
念的认识。
再正式开始本节的内容之前,先看一下WMS窗口属性更新的总体过程,如图4-6所示。
步,除此之外,屏幕旋转等操作也属于第一步的范畴。第二步的布局“子系统”这个说法其
实并不太准确,因为这个所谓的子系统只是以performLayoutAndPlaceSurfaceLocked()函数
为主入口的一组函数的集合。而动画子系统则是WMS中由Choreographer驱动的一系列的
WindowAnimator,这一部分将在后续内容中介绍。
接下来,就从relayoutWindow()开始,揭开WMS窗口布局管理的面纱吧。 4.4.1 从relayoutWindow()开始
[WindowManagerService.java->WindowManagerService.relayoutWindow()]
public int relayoutWindow(Session session,IWindow client,int seq,
WindowManager.LayoutParams attrs,int requestedwidth,
int requestedHeight,int viewVisibility,int flags,
Rect outFrame,Rect outContentInsets,
Rect outVisibleInsets,Configuration outConfig,Surface outSurface);
参数很多,从名字上可以看出有些参数是返回给调用者的。下面介绍它们的意义。
口 client:需要进行relayout的窗口。
口 seq:一个和状态栏/导航栏可见性相关的序列号,在第6章再进行探讨。
口 attrs:窗口的新布局属性。relayoutWindow()的主要目的就是根据attrs所提供的布局
参数重新布局一个窗口。客户端可以通过relayoutWindow()函数改变attrs中所定义的
几乎所有布局属性。但是唯独无法改变窗口类型。
口 requestedWidth与 requestedHeight:客户端所要求的窗口尺寸。在重新布局的过程中,
WMS会尽量将窗口的尺寸布局为客户端所要求的大小。
口 viewVisiblility:窗口的可见性。
口 flags:定义一些布局行为。
口 outFrame:由relayoutWindow()函数返回给调用者的一个Rect类型的实例。它保存了
窗口被重新布局后的位置与大小。
口 outContentInsets与outVisiblelnsets:这两个参数表示了窗口可以绘制内容的矩形边界
与可视矩形边界在四个方向上到mFrame的像素差。
口 outConfiguration:重新布局后,WMS为此窗口计算出的Configuration。
口 outSurface:用来接收WMS为此窗口分配的Surface。窗口的第一次relayout完成后就
可以通过它在窗口中进行绘图了。卷I的Surface系统一章中曾经介绍了这个参数的
具体细节。本节不再赘述。
[WindowManagerService.java-->WindowManagerService.relayoutWindow0]
public int relayoutWindow(Session session,IWindow client,int seq,
windowManager.LayoutParams attrs,int requestedwidth,
int requestedHeight,int viewVisibility,int flags,
Rect outFrame,Rect outContentInsets,
Rect outVisibleInsets,Configuration outConfig,Surface outSurface){
boolean toBeDisplayed=false;
boolean inTouchMode;
boolean configChanged;
boolean surfaceChanged=false;
boolean animating;
...... // 先做一些权限相关的检查
/*接下来的操作将在锁住mWindowMap的情况下完成。在WMS中,几乎所有的操作都是在锁住mWindowMap
的情况下完成的。在后面分析performLayoutAndPlaceSurfacelocked()函数时会发现,WMS对
窗口的操作复杂而漫长,为了防止线程间竟态发生,WMS统一使用mWindowMap对所有窗口操作进行同步*/
synchronized(mWindowMap){
//从mWindowMap中获取需要进行relayout的WindowState。relayoutWindow()将根据传入
//的参数更新WindowState的属性
Windowstate win =windowForClientLocked(session,client,false);
mRequestedwidth/Height(客户端要求的窗口尺寸)
mSystemUiVisibility(状态栏与导航栏的可见性)、mAlpha(窗口透明度)等
当然,保存了几乎所有窗口属性的mAttrs也被更新了*/
注意: relayoutWindow()基本上可以为窗口更新所有在LayoutParameter类中定义的属性,
但是窗口类型是一个例外。窗口类型必须在添加窗口时加以指定,并且不允许再做
更改。
//②根据Window的可见性更新或创建Surface及启动动画效果
//因为这部分内容和布局关系不大,所以简单带过
if(viewVisibility== View.VISIBLE &&
(win.mAppToken==null ll!win.mAppToken.clientHidden)){
/*对于处于可见状态的窗口主要有以下处理:
1.如果窗口没有Surface,则为其创建一块Surface
2.如果客户端改变了窗口的色彩格式(由LayoutParams.format指定)发生了变化则为其
重新创建一块指定格式的Surface
3.如果窗口尚未被显示,并且窗口的客户端已经完成了绘制,则为其启动一个淡入动画*/
}else{
/*对于处于非可见状态下的窗口,主要的处理如下:
1.标记Windowstate的mExiting属性为true
2.如果窗口目前正被显示,则为其启动一个淡出动画
3.释放客户端所持有的Surface对象,自此之后,客户端无法再更新窗口的内容*/
}
/*更新焦点窗口,因为窗口的显示与退出,或者窗口与焦点相关的flag发生了变化
(例如FLAG_NOT_FOCUSABLE),都会要求WMS重新计算焦点窗口。焦点相关的内容会在
第5章中详细讨论*/
if(focusMayChange){
}
if(imMayMove){
}
背景,一个典型的例子就是launcher和home。这要求Wallpaper窗口的显示次序紧临此窗口之
下。所以,在这里需要更新Wallpaper窗口在WindowList中的次序。其实现原理与上一节所分
析的addwindowToListInorderLocked()非常相近*/
if(wallpaperMayMove){
函数为所有窗口重新计算最终的mLayer,以确定其显示次序*/
if (assignLayers){
assignLayersLocked(win.getwindowList());
}
可能指定了必须工作在横屏状态或者竖屏状态。所以此时需要根据窗口的要求,调整屏幕的旋转状态。
本章并不打算介绍屏幕旋转的内容,屏幕旋转不过是对布局系统与动画系统的一个运用*/
configChanged=updateorientationFromAppTokensLocked(false);
//将窗口所在的DisplayContent标记为需要进行重新布局。这一个赋值看似简单,其实很重要。
//因为如果不进行这个标记,哪怕DisplayContent中的窗口变化得天翻地覆,也不会对其重新布局
win.mpisplayContent.layoutNeeded=true;
/*④神奇的performLayoutAndPlaceSurfacesLocked()登场!它将遍历所有Displaycontent
的所有窗口,为它们计算布局尺寸,并将布局尺寸设置给它们的Surface*/
performLayoutAndPlaceSurfacesLocked();
/*⑤返回布局结果。完成performLayoutAndPlaceSurfacesLocked()的调用之后,WMS便根据
调用参数的要求完成窗口的布局,现在是将布局结果返回给调用者的时侯了*/
outFrame.set(win.mCompatFrame);
outContentInsets.set(win.mContentInsets);
outVisibleInsets.set(win.mVisibleInsets);
}
if (configchanged){
sendNewConfiguration();
}
这段代码稍稍有点长,下面总结一下它的工作内容:
口 根据参数更新窗口的WindowState对象的相应属性,这些属性是后续对其进行布局的
依据。
口 处理窗口的显示与退出。这里主要是一些涉及Surface的创建/销毁,以及动画相关
的操作。这不是本节讨论的重点。
口 更新和窗口相关的其他机制,例如焦点、输入法、壁纸以及屏幕旋转等。
口 调用performLayoutAndPlaceSurfaceLocked()函数进行布局。
之所以使用relayoutWindow()作为布局的切入点,一是因为它是WMS提供的最常用的
API之一,二是它体现了WMS窗口相关操作的一个通用做法:
口 修改一些属性(窗口属性、屏幕属性、焦点属性等)。
口 标记相关的DisplayContent为relayoutNeeded。调用performLayoutAndPlaceSurfaceLoc
ked0进行全局布局。
口 对布局结果进行进一步的操作。
通过这个通用做法可以对performLayoutAndPlaceSurfaceLocked()函数拥有一个感性认识。
简单地说,窗口的属性可以分为两类:一类是布局控制属性,例如mRequestedWidth/Height、
mAtrs等,由客户端根据需要进行设置,用来向WMS表达它所期望的窗口布局;另一类是
布局结果属性,例如mFrame、mContentilnsets、mVisiblelnsets等,它们是经过布局过程计算
出来的,直接影响到窗口的实际布局与显示,客户端无法直接干预这些属性,只能被迫接受
这些布局结果。performLayoutAndPlaceSurfaceLocked()函数就是通过布局控制属性计算布局
结果属性这一过程的场所。
客户端只能被迫接受窗口的布局结果,是因为客户端调用relayoutWindow()时所要
求的尺寸和位置参数和最终的布局结果有出入。研究一下Android控件系统的核心类
ViewRootlmpl的实现就可以发现,它作为控件系统到窗口系统的桥梁,采用了协商式
的函数来绘制控件。在窗口显示之前,ViewRootlmpl首先对整个控件树进行尺寸测量,
得到一个理想尺寸,并将这个理想尺寸作为relayoutWindow()的requestedWidth/Height
参数交给WMS进行布局。布局之后,ViewRootlmpl会根据WMS的实际布局结果
(frame,contentlnsets,visiblelnsets)重新对控件树进行一次测量,得到最终尺寸,然后
进行绘制。
有兴趣的读者可以做一个试验,在relayoutWindow()函数最后修改outFrame的botom
为原来的1/2,看一下是什么样的结果。 4.4.2布局操作的外围代码分析
[WindowManagerService.java->
WindowManagerService.performLayoutAndPlaceSurfaceLocked()]
private final void performLayoutAndPlaceSurfacesLocked(){
int loopCount=6;
do{
mTraversalScheduled=false;
mH.removeMessages(H.DO TRAVERSAL);
loopCount--;
}while(mTraversalscheduled&&loopCount>0);
}
performLayoutAndPlaceSurfacesLockedLoop()中完成的。
虽然这个函数只有短短的7行,但是却包含了一个乍看上去有点不明所以的do-while循
环。它的循环条件是mTraversalScheduled为true并且loopCount大于0。为了搞清楚这个循
环的目的所在,首先要找到mTraversalScheduled变量的意义何在。原来,修改mTraversalScheduled变量的只
有WMS为外界提供的一个名为requestTraversal()的接口函数。其实现如下:
[WindowManagerService.java-->WindowManagerService.requestTraversal()]
public void requestTraversal(){
synchronized(mwindowMap){
requestTraversalLocked();
}
void requestTraversalLocked(){
if{!mTraversalscheduled){
//设置为true。也就是说,performLayoutAndPlaceSurfaceLocked()要多循环一次了
mTraversalScheduled = true;
//H.DO_TRAVESAL消息的处理就是调用performLayoutAndPlacesurfaceLocked()
mH.sendEmptyMessage(H.DO_TRAVERSAL);
}
行重新布局的手段。注意,requestTraversalLocked()是在被mWindowMap这个锁的保护
下调用的,这与performLayoutAndPlaceSurfaceLocked()是一样的。也就是说外部线程不
可能影响这个循环的判定条件。那就只剩下了一个可能:performLayoutAndPlaceSurface
sLockedLoop()函数中会调用requestTraversalLocked()。现在就到这个函数内部去求证一
下吧。
[WindowManagerService-->WindowManagerService.performLayoutAndPlaceSurfacesLockedLoop()]
private final void performLayoutAndPlaceSurfacesLockedLoop(){
//首先是省略掉的安全性检查。不过这个函数中有一个特殊要求需要注意:该函数禁止递归调用
/*在正式开始重新布局之前,先删除所谓的“僵尸窗口”,并释放它们所持有的Surface。该操作其实与
当Surface的操作因为内存原因而失败时,WMS会将所有僵尸窗口收集到mForceRemoves列表中,
然后在这个位置(也就是重新布局开始之前)统一清理。
关于这个过程的详细内容,读者可以参考reclaimSomeSurfacelMemorylocked()函数的实现与使用*/
boolean recoveringMemory=false;
try{
//标记此次布局已经完成内存回收了,此次布局再发生Surface操作异常时,不会再次尝试回收
recoveringMemory=true;
for(int i = 0;i
removeWindowInnerLocked(ws.mSession,ws);
mForceRemoves=null;
}catch (RuntimeException e){ }
try{
performLayoutAndPlaceSurfacesLockedInner(recoveringMemory);
mInLayout=false;
//不出所料,果然调用了requestTraversalLocked(),
//其调用条件是needsLayout()并且mLayoutRepeatcount<6
if(needsLayout()){
if(++mLayoutRepeatCount<6){
requestTraversalLocked();
}else{
mLayoutRepeatCount=0;
}else{
mLayoutRepeatCount=0;
}//通知监听者,窗口布局发生了变化。目前的监听者只有一个ViewServer
if(mWindowsChanged &&!mwindowChangeListeners.isEmpty()){
mH.removeMessages(H.REPORT WINDOWS CHANGE);
mH.sendMessage(mH.obtainMessage(H.REPORT_WINDOWS_CHANGE));
}catch (RuntimeException e){ }
需要注意的地方应该就是requestTraversalLocked()的两个调用条件了。
第一个条件是needsLayout()函数的返回值。还记得在分析relayoutWindow()时所提到
的DisplayContent.mLayoutNeeded字段吗?needsLayout()函数遍历所有DisplayContent并
检查它们的mLayoutNeeded字段的值。只要有一个DisplayContent需要重新布局,此函数
就会返回true。这说明一个问题,就像罗马不是一天建成的一样,布局也不是一遍就能完成的
新布局6次,这和performLayoutAndPlaceSurfaceLocked()中的循环
条件有所重复。这说明了一个更严重的问题,布局有可能重复6次
也无法完成,为了防止调用者过度等待,以致需要增加一个6次的
限制。
到这里,我们对布局的外围有了一个简单认识,如图4-7所示。4.4.3初探perfomLayoutAndPlaceSurfacesLockedinner0()
函数不只名字长,其实现更长,有近600行之多。
而且Android4.2已经通过提炼一部分代码到独立的函数中为其减肥。
为了防止在研究这个函数的过程中迷路,先向大家提供将这个函数提
炼后的伪代码:
WindowManagerService.performLayoutAndPlaceSurfacesLockedlnner()]
privatevoid performLayoutAndPlaceSurfacesLockedInner(){
布局前的预处理;
遍历所有DisplayContent{
遍历Displaycontent下的所有窗口{
对窗口进行布局;
}
对Displaycontent的布局后处理;
}
}
容都相当繁杂。这一点从其注释中可以看出:Something has changed!Let's make it correct
now!不用担心,我们将一步一个脚印地探索这个函数的实现。 4.4.4布局的前期处理
WindowManagerService.performLayoutAndPlaceSurfaceLockedlnner()]
private final void performLayoutAndPlaceSurfacesLockedInner (boolean recoveringMemory){
//①首先更新处于焦点状态的窗口
if(mFocusMayChange){
mFocusMayChange=false;
updateFocusedWindowLocked(UPDATE_FOCUS_WILL_PLACE_SURFACES,
false/*updateInputwindows*/);
}
for(i = mExitingTokens.size()-1;i>=0;i--){
mExitingTokens.get(i).hasVisible=false;
}
mExitingAppTokens.get(i).hasVisible=false;
}
或lag来指明屏幕的亮度、按键背光的亮度、是否保持屏幕唤醒状态以及输入事件的超时时间。这些
设置都可以在窗口的LayoutParams中找到与之对应的属性。
WMS使用mInnerFileds 集合了这些状态,在布局过程中,WMS会遍历所有窗口以查找这些设置,
并将这些设置保存到mInnerFileds中。在讨论DisplayContent的布局后处理时将作深入探讨*/
//mHoldScreen是一个Session类型的变量,保存了要求保持屏幕唤醒状态的窗口所在的进程
mInnerFields.mHoldScreen=null;
//取值范围从0到1的一个float类型变量,保存了完成布局后的屏幕亮度
mInnerFields.mScreenBrightness=-1;
//与mScreenBrightness一样的取值范围,保存了完成布局后的键盘背光亮度
mInnerFields.mButtonBrightness=-1;
//以毫秒为单位,表示了输入事件在此窗口上发生ANR的时间
mInnerFields.mUserActivityTimeout=-1;
//这个属性表示了当前DisplayContent显示内容的类别。在本章中不作讨论
mInnerFields.mpisplayHasContent=LayoutFields.DISPLAY_CONTENT_UNKNOWN;
//③递增布局序列号
mTransactionSequence++;
//获取手机屏幕的宽高尺寸,这个尺寸将用来布局水印和StrictMode的红色警告框
final Displaycontent defaultDisplay =getDefaultDisplaycontentLocked();
final DisplayInfo defaultInfo=defaultDisplay.getDisplayInfo();
final int defaultDh=defaultInfo.logicalHeight;
Surface.openTransaction();
try{
//④布局水印和StrictMode警告框
if(mWatermark!=nul1){
mWatermark.positionSurface(defaultDw,defaultDh);
}
mStrictModeFlash.positionSurface(defaultDw,defaultDh);
}
boolean focusDisplayed=false;
boolean updateA1lDrawn=false;
遍历所有Displaycontent{
遍历Displaycontent下的所有窗口{
对窗口进行布局;
}
对Displaycontent的布局后处理;
}catch (Exception e){
}finally{
Surface.closeTransaction();
}
}
口 如果有必要,计算新的焦点窗口。第一次见到updateFocusedWindowLocked()函数是
在relayoutWindow()中,如果客户端要求relayout的窗口的可见性发生变化,则重新
计算焦点窗口。
口 初始化mlnnerFields中的一部分字段,mInnerFields是LayoutFields类的一个实例。事
实上,LayoutFields类本身并没有什么实际意义,不过是把一些布局相关的状态变量
组合到一起而已。在后面使用时再对其进行说明。
口 递增布局序号mTransactionSequence。WMS每进行一次布局都会导致序号递增。
AppWindowToken中也保存了一个相应的布局序号,在布局的过程中,WMS通过对
比这两个序号的值以确定AppWindowToken的布局状态是否最新。
口 布局水印和StrictMode警告框。水印用以在屏幕上显示一段固定的信息,而
StrictMode警告框则在任何一个应用或服务发生违例操作时在屏幕上闪烁一个红色方
何一个窗口的显示次序,所以它们将显示在所有窗口之上。它们的布局非常简单:占
据整个显示画面。
布局的前期处理看起来没有什么复杂的。接下来开始对每个DisplayContent进行布局。 4.4.5布局DisplayContent
Display设备)。而一个Display Conent则用来描述一块屏幕。在-
performLayoutAndPlaceSurfacesLocked()函数的后续代码中,将遍历系统中所有的DisplayContent,
并分别对它们各自所拥有的窗口进行布局。这一部分内容是
performLayoutAndPlaceSurfacesLockedlnner()函数的核心所在。
[WindowManagerService.,java-->
WindowManagerService.performLayoutAndPlaceSurfaceLockedlnner()]
private final void performLayoutAndPlaceSurfacesLockedInner(boolean recoveringMemory)
//布局的前期处理
try{
DisplaycontentsIterator iterator=new DisplaycontentsIterator();
/*遍历所有的Displaycontent。注意,WMS使用了一个内部类DisplayContentsIterator对
mDisplaycontents列表进行遍历*/
while(iterator.hasNext()){
//首先,提取displaycontent的属性,用于后续的布局操作。
//比较重要的有windows、dw/dh、innerDw/innerph、ispefaultDisplay等
final DisplayContent displayContent=iterator.next();
//windows列表中保存了displayContent所拥有的所有窗口
WindowList windows=displaycontent.getwindowList();
DisplayInfo displayInfo=displaycontent.getDisplayInfo();
final int displayId=displayContent.getDisplayId();
/*dw/dh存储了当前Displaycontent所描述的显示屏的逻辑尺寸。注意这时逻辑尺寸与
物理尺寸是有区别的。例如当前DisplayContent正在模拟一个低分辨率的显示屏时,则获取
为模拟显示屏的尺寸,而非实际物理尺寸*/
final int dw=displayInfo.logicalwidth;
final int dh=displayInfo.logicalleight;
/*innerDw/innerph描述了当前显示屏用于显示应用程序的区域尺寸,就是逻辑尺寸减去系统
装饰的尺寸。所谓的系统装饰包括状态栏、导航栏等*/
final int innerDw=displayInfo.appwidth;
final int innerDh=displayInfo.appHeight;
是指屏幕的Displaycontent。与其他屏幕不同的是,手机屏幕拥有状态栏、导航条,并
应输入事件,而其他的屏幕则没有这些特性。因此在布局的过程中,必须对此加以区分*/
final boolean isDefaultDisplay=(displayId ==Display.DEFAULT_DISPLAY);
/*①接下来是一个do-while循环。是否似曾相识?读者可能想到了,Displaycontent的
布局也是需要进行多次尝试的。循环的条件就是DisplayContent的pendingLayoutchanges
段值不为0。完成一次布局后会检查布局结果,并通过设置pendingLayoutChanges决定是
否需要重新布局*/
int repeats=0;
do{
repeats++;
//限制尝试次数在6次以内
if(repeats>6){
displaycontent.layoutNeeded=false;
break;
}
//根据pendingLayoutChanges的值做相应的处理。在这里暂且忽略这些代码,等
//清楚pendingLayoutchanges的取值之后再做分析
/*接下来调用performLayoutLockedInner()函数。注意其名称,与我们正在
分析的函数相比少了AndPlaceSurface字样。它完成了对当前DisplaContent的
所有窗口的布局工作,也将是后续分析的重点。注意,当repeat次数大于4后,将
不再执行此函数*/
if(repeats<4){
performLayoutLockedInner (displaycontent,repeats ==1,
false/*updateInputwindows*/);
}
displaycontent.pendingLayoutChanges=0;
//由PhonewindowManager检查布局,并将结果保存到pendingLayoutchanges中
if(isDefaultDisplay){
mPolicy.beginPostLayoutPolicyLw(dw,dh);
for(i=windows.size()-1;i>=0;i--){
Windowstate w=windows.get(i);
if(w.mHasSurface){
mPolicy.applyPostLayoutPolicyLw(w,w.mAttrs);
}
}
}while (displaycontent.pendingLayoutchanges !=0);
//②对Displaycontent的布局后处理,这部分稍候再作者虑
}//对Displaycontent的布局到此结束
}catch (Exception e){
}finally{
Surface.closeTransaction();
}
整理一下目前的思路。布局DisplayContent整体上分为两个部分:
口 布局循环。这个do-while循环的主要工作是对DisplayContent所拥有的窗口进行布
局,其工作侧重于PerformLayout。
口 布局后处理。后处理用于根据布局结果设置Surface参数,应用一些动画效果等,其
工作侧重于PlaceSurfaces。 1.深入理解窗口布局的原理
仔细归纳一下其循环体,可以发现其明显分为三个阶段:
口 pendingLayoutChanges处理阶段。这个阶段先不讨论。当学习了结果检查阶段后再回
头探讨这个阶段。
口 布局阶段。主要内容就是performLayoutLockedlnner()函数。这个函数将对
DisplayContent下的所有窗口进行布局。
口 结果检查阶段。在所有窗口的布局完成后,通过一些状态量的检查,决定是否重做这
三个阶段的工作。其检查内容主要是状态栏、导航栏可见性是否与顶层窗口的属性冲
突,是否需要解除锁屏状态等。
这个循环不断地对DisplayContent中的所有窗口进行布局操作,并处理pending-
LayoutChanges,直到WMP的finishPostLayoutLw()认为当前布局不需要再做任何改动。
如果在循环4次之后仍无法满足finishPostLayoutLw()的要求,则不再尝试对窗口进行布
局,仅处理pendingLayoutChanges。如果6次尝试之后仍然无法满足要求,则放弃继续
尝试。
接下来先深入学习布局阶段的工作原理。通过这一节的学习,希望读者能够了解WMS
如何计算每一个窗口的位置与尺寸。
我们首先来看performLayoutLockedlnner()是如何对窗口完成布局的。参考其代码:
[WindowManagerService.java-->WindowManagerService.performlayoutLockedlnnerO]
private final void performLayoutLockedInner(final Displaycontent displaycontent,
boolean initial,boolean updateInputMindows){
//只有displaycontent的layoutNeeded为真时才会进行布局
if( ! displayContent.layoutNeeded){
return;
}
//获取displayContent所拥有的所有Window
Windowlist windows=displaycontent.getWindowlist();
//确定是否是手机屏幕
boolean isDefaultDisplay=displaycontent.isDefaultDisplay;
//获取当前displaycontent的尺寸信息
DisplayInfo displayInfo-displaycontent.getDisplayInfo();
final int dw=displayInfo.1ogicalwidth;
final int dh=displayInfo.logicalleight;
//①通知WindowPolicyManager,使其为即将开始的布局操作做准备
mPolicy.beginLayoutLw(isDefaultDisplay,dw,dh,mRotation);
//递增mLayoutSeg
int seq-mLayoutSeq+1;
if (seq<0)seq=0;
mLayoutSeq =seq;
//这个局部变量用来指示第一个非顶级窗口在列表中所处的位置。用来节省一部分查找时间
int topAttached=-1;
//②对所有的顶级窗口进行布局
for(i=N-1;i>=0;i--){
final Windowstate win=windows.get(i);
//为了节省篇幅,这里省略了判断条件。其判断目的是节省布局的时间开销,使一些不可见的窗口不参与布局
if(....…){
if(lwin.mLayoutAttached){//在本次循环中仅布局顶级窗口
//调用窗口的prelayout()函数,使其做好被布局的准备
win.prelayout();
//调用WindowManagerPolicy的layoutWindowLw()函数对窗口进行布局
mPolicy.layoutwindowtw(win,win.mattra,nul1);
win.mLayoutSeq=seq;
}else{
//如果当前窗口不是顶级窗口,则记录下其位置。目的是在后面对非顶级窗口进行布局时
//直接从这个位置开始,从而省略一部分时间
if (topAttached<0)topAttached=i;
}
for(i=topAttached;i>=0;i--){
//这部分内容与顶级窗口布局几乎完全一致,读者可自行对比研究
}
mInputMonitor.setUpdateInputwindowsNeededLw();
if (updateInputwindows){
mInputMonitor.updateInputwindowsLw(false/*force*/);
}
mPolicy.finishLayoutLw();
}
口 在进行布局前,首先通过执行WindowManagerPolicy.beginLayoutLw()通知WMP为即
将开始的布局进行准备。这个准备过程就是通过屏幕的尺寸、状态栏/导航栏的可见
性、屏幕旋转状态等因素来计算布局所使用的参数。
口 首先对所有顶级窗口进行布局。这个布局过程也分为两步,分别为WindowState.
prelayout()和WindowManagerPolicy.layoutWindowLw()。其工作就是使用上一步所计
算出的布局参数计算出窗口的位置属性,并保存在WindowState中。
口 之后对所有子窗口进行布局,其布局过程与顶级窗口一致。之所以要将顶级窗口与子
窗口分开进行布局,是因为子窗口的布局依赖于其父窗口的布局结果。
口 最后,执行WindowManagerPolicy.finishLayoutLw)。告知WMP本次布局已完成,可
以清理布局过程中所使用过的资源。
可以看出,在窗口布局过程中,WindowManagerPolicy发挥了近乎决定性的作用。在
Android4.2中,WindowManagerPolicy 是由PhoneWindowManager实现的,所以上述的关键函
数将以PhoneWindowManager的实现为基础进行讨论。
接下来将深入研究这三个阶段的工作原理。
口的准绳。由于beginLayoutLw()函数内容相对琐碎,因此下面直接给描述这8个矩形区域
的参数的意义以及其计算依据。建议感兴趣的读者在读完本节后可以仔细研究一下这个函数,
以加深理解。
口 mUnrestrictedScreenLeft/Top/Width/Height:描述了整个屏幕的逻辑显示区域,
也就是(0,0-dw,dh)。
口 mRestrictedScreenLeft/Top/Width/Height:描述了屏幕中导航栏之后的区域。这个区
域会受到导航栏的可见性影响。当导航栏不可见时,这块区域与Unrestricted区域等价。
UnrestrictedScreen区域与Restricted区域在窗口布局的过程中常常被用作布局容器。布局
容器可以用来应用窗口的对齐方式,或者对窗口的尺寸及位置加以限制。
口 mStableFullscreenLeft/Top/Right/Bottom:描述了整个屏幕的逻辑显示区域,和
Unrestricted区域等价。
口 mStableLeft/Top/Right/Botom:描述了屏幕排除状态栏和导航栏之后的区域。这个区
域并不受状态栏与导航栏的可见性影响。即便状态栏或导航栏被隐藏,Stable区域仍
然为排除状态栏与导航栏之后的区域。这也是Stable这个名称的由来。
StableFullscreen区域和Stable区域并不直接参与窗口的布局过程,而是为WMS的客户
端提供一个不受状态栏/导航条的可见性所影响的显示区域大小及位置。
口 mDockLeft/Top/Right/Bottom:Dock区域用来描述可用来放置停靠窗口的区域。所谓
的停靠窗口是指显示在屏幕某一侧的半屏窗口,例如输入法窗口。Dock区域最主要
的用途就是用来作为输入法窗口的布局容器。
口 mContentLeft/Top/Right/Bottom:描述屏幕中排除了状态栏、导航栏以及输入法(如
果显示)后的屏幕区域。与Restricted区域相比,Content区域额外地受到了状态栏
及输入法的影响。当没有状态栏及输入法显示时,Content区域与Restricted区域是
等价的。
口 mCurLeft/Top/Right/Bottom:与Content区域一样,它也描述了屏幕中排除了状态栏、
导航栏以及输入法(如果显示的话)后的屏幕区域。在大部分情况下,Cur区域与
Content区域是相同的。但是这两个区域的计算依据不同。
Content区域和Cur区域 与 其他区域 有个不同的地方:它们的区域并不是在begin-
LayoutLw()中被最终确立下来。因为它们受输入法窗口的尺寸影响,所以必须在输入法窗口
完成布局后,在offsetlnputMethodWindowLw()函数中得到最终的位置。而在此之前,它们与
Dock区域是相同的。
口 mSystemLeft/Top/Right/Botom:与Dock区域的绝大多数情况是相同的,但它们有一
个很微妙的不同。状态栏与导航栏的可见性发生变化时有一个淡入淡出的动画效果,
这两个区域在这个淡入淡出的过程中是不一致的。Dock认为在淡入淡出的动画过程
图4-8与图4-9较为直观地描述了布局参数的具体位置。
读者可以通过执行adb shell dumpsys window命令来查看这8个区域的当前取值。它们
显示在WINDOWMANAGER POLICY STATE一栏中。
讨论如何通过这些布局参数确定一个窗口的位置和尺寸。
如前所述,布局窗口使用了两个函数,分别是WindowState.prelayout()和WindowManager-
Policy.layoutWindowLw()。prelayout()函数初始化了一个尺寸放大系数,用于在兼容模式下显
示窗口,在正常情况下,此放大系数保持为1,本章不准备讨论这个问题,所以,直接看一
下PhoneWindowManager的layoutWindowLw()的实现:
[Phone WindowManager.java->PhoneWindowManager.layoutWindowLw()]
public void layoutwindowLw(Windowstate win,WindowManager.LayoutParams attrs,Windowstate attached){
//状态栏与导航栏不再被布局。事实上,这两个窗口是由beginLayoutLw()函数在计算布局参数时完成
//布局的,这一点很容易理解,因为一些布局参数是依赖于状态栏和导航栏的尺寸的
if (win==mstatusBar ll win==mNavigationBar){
return;
}
在布局窗口的过程中频繁创建和销毁这4个矩形,故而提供了4个成员变量用作缓存。当完成后续的
计算之后,四个矩形的内容都会被更新*/
final Rect pf=mTmpParentFrame;
final Rect df=mTmpDisplayFrame;
final Rect cf=mTmpContentFrame;
final Rect vf=mTmpVisibleFrame;
//一大段代码,用来计算pf、df、cf和vf
//将计算好的4个矩形交给Windowstate,由窗口自己计算自己的布局结果
win.computeFrameLw(pf,df,cf,vf);
//下面这段代码与当前窗口的布局操作本身没有什么关系。如上一节所述,当完成输入法窗口的布局后
//需要更新Content区域与Cur区城。这里就是完成这个工作的地方
if (attrs.type==TYPE_INPUT_METHOD && win.isVisibleOrBehindKeyguardLw()
&&!win.getGivenInsetspendingLw())(
setLastInputMethodwindowLw(null,null);
offsetInputMethodwindowLw(win);
调用WindowState的computFrameLw()函数完成布局,这4个矩形便成为窗口布局的4个关
键参数。
口 ParentFrame(pf):描述了放置窗口的容器的位置与尺寸。通过LayoutParam为窗口指
定的布局参数如x、y、width、height及gravity等都是相对于pf进行计算的。也就是说,
pf对窗口的位置与尺寸计算的影响在4个矩形中起到了决定作用。
口 DisplayFrame(df):df用来限制窗口的最终位置。当窗口通过ParentFrame完成位置与
尺寸的计算后,需要通过df再进行一次校正,要求窗口必须完全位于df之内。
口 ContentFrame(cf):cf不会直接影响窗口布局位置与尺寸,但是它影响了窗口内容的绘
制。cf表示当前屏幕上排除所有系统窗口(状态栏、导航栏以及输入法)后所留下的
矩形区域。
口 VisibleFrame(vf):和cf一样,vf也不会直接影响窗口布局的位置与尺寸,而是会影
响窗口内容的绘制。vf表示在当前屏幕上,完全不被任何系统窗口所遮挡的一块矩形
区域。
那么,这4个参数与前面一节所提到的8个准绳是什么关系呢?简单分析一下
layoutWindowLw()计算这4个参数的代码便可知道,这4个参数是layoutWindowLw()根据
需要从8个准绳当中选出来的。例如,根据窗口的类型、flag的不同,pf可能是Restricted、
Unrestricted、Dock 或Content区域。df则在绝大部分情况下与pf的取值保持着一致。cf则根
据LayoutParams.softInputMode以及lag的取值不同而可能是Content、Dock或Restricted区
域。vf则根据LayoutParams.softInputMode的取值选择与cf保持一致,或者选择Cur区域。另
外,对于子窗口,其4个参数的取值依赖于其父窗口。
由于需要考虑到的情况非常多,因此layoutWindowLw()花了很大篇幅计算这4个参数。
虽然繁琐却并不复杂,所以这里就不贴出来了,读者可以根据自己的实际需要去探索这段
代码。
了解computeFrameLw()的参数之后,接下来就要研究computeFrameLw()的工作原理,
以及经过这么复杂的流程,对一个窗口进行布局后的产出是什么。
(4)窗口布局的产出物
参考computeFrameLw()的代码:
[WindowState.java-->WindowState.computeFrameLw()]
public void computeFrameLw(Rect pf,Rect df,Rect cf,Rect vf){
mHaveFrame=true;
//首先,设置mContainingFrame为pf
final Rect container=mContainingFrame;
container.set(pf);
final Rect display=mpisplayFrame;
display.set(df);
//计算container的宽高
final int pw=container.right-container.left;
final int ph=container.bottom-container.top;
int w,h;
/*接下来计算窗口的尺寸,并保存到w和h中。代码比较简单,这里就不贴了,其基本原则是:如果将
LayoutParams.width或height指定为MATCHPARENT,则w和h为pf的宽和高。否则,如果窗口
拥有FLAG_SCALED,则w和h为LayoutParams.width与height。倘若未指定FLAG_SCALED,w和
h为客户端调用relayoutwindow函数所传的参数mRequestedwidth/Height*/
//将cf保存到mContentFrame
final Rect content=mContentFrame;
content.set(cf);
//将vf保存到mvisibleFrame
final Rect visible=mVisibleFrame;
visible.set(vf);
//Windowstate的mFrame成员变量表示窗口的当前位置与尺寸
final Rect frame=mFrame;
//调用Gravity.apply()函数根据gravity、pf、w、h、x、y计算窗口的mFrame。,
//也就是说 pf 对窗口的位置起到决定性作用
Gravity.apply(mAttrs.gravity,w,h,container,
(int)(x+mAttrs.horizontalMargin *pw),
(int)(y+ mAttrs.verticalMargin * ph),frame);
//调用Gravity.applyDisplay() 函数根据gravity、df来修正mFrame。其原则为,mFrame必须
//完全位于df之内。经过修正后的mFrame就是窗口的最终位置与尺寸
Gravity.applyDisplay(mAttrs.gravity,df,frame);
//根据计算出的mFrame调整mContentFrame和mVisibleFrame。窗口最终的mVisibleFrame和
//mContentFrame是系统当前的cf或vf与窗口的mFrame的交集
if (content.left< frame.left) content.left=frame.left;
if(content.topif(content.right>frame.right) content.right=frame.right;
if(content.bottom>frame.bottom) content.bottom=frame.bottom;
if(visible.left< frame.left) visible.left=frame.left;
if(visible.topif(visible.right>frame.right) visible.right=frame.right;
if(visible.bottom>frame.bottom) visible.bottom=frame.bottom;
//计算mContentInsets。这个成员变量用来指示mContentFrame到mFrame的4条边界的距离
contentInsets.left=content.left-frame.left;
contentInsets.top=content.top-frame.top;
contentInsets.right=frame.right-content.right;
contentInsets.bottom=frame.bottom-content.bottom;
//计算mVisibleInsets。这个成员变量用来指示mVisibleFrame到mFrame的4条边界的距离
final Rect visibleInsets=mVisibleInsets;
visibleInsets.left=visible.left-frame.left;
visibleInsets.top=visible.top-frame.top;
visibleInsets.right=frame.right-visible.right;
visibleInsets.bottom=frame.bottom-visible.bottom;
}
口 mFrame:描述了窗口的位置与尺寸。
口 mContainerFrame与mParentFrame:这两个矩形是相等的,保存了pf参数。
口 mDisplayFrame:保存了df参数。
口 mContentFrame与mContentlnsets:mContentFrame表示了在当前窗口中可以用来显示
内容的区域,由cf与mFrame相交得出。而mContentlnsets则表示了mContentFrame
与mFrame的4条边界之间的距离。relayoutWindow()函数有一个输出参数
outConentInsets,即 mContentInsets。
口 mVisibleFrame与mVisiblelnsets:mVisibleFrame表示在当前窗口中不被系统窗口所遮
挡的区域,由vf与mFrame相交得出。mVisiblelnsets 同mContentInsets一样,表示
mVisibleFrame与mFrame的4条边界之间的距离。relayoutWindow()函数的另一个输
出参数outVisiblelnsets就是mVisiblelnsets。客户端可以通过outVisiblelnsets的取值,
适当地滚动自己的显示,以确保一些重要信息不被遮挡。
至此,单个窗口的布局过程便完成了。在经历布局之后,窗口的位置尺寸、内容区域的
位置尺寸、可视区域的位置尺寸都得到了更新。这些更新将会影响到窗口Surface的位置尺
寸,并且还会以回调的方式通知窗口的客户端,进而影响窗口内容的绘制。
回到performLayoutLockedlnner()函数,在DisplayContent下的所有窗口都已完成布局之
后,则调用WindowManagerPolicy.finishLayoutLw()函数,要求WMP释放布局过程中所使用
的资源,或者做一些策略处理。不过WMP的实现者PhoneWindowManager在这个函数中并
没有做任何事情。
LayoutLockedinner()对给定的DisplayContent下的所有窗口进行布局,使窗口的mFrame等布
应用一些显示效果,以及影响客户端显示内容的绘制。performLayoutLockedlnner()函数的主
要工作流程如图4-10所示。
窗口布局的演算过程为:根据DisplayContent的属性,以及状态栏、导航栏及输入法窗口
的状态,确定8个布局区域。然后根据每个窗口的状态,从8个布局区域中选出4个布局参
数pf、df、cf以及vf(可以重复选择)。之后,窗口根据这4个参数计算出布局结果并保存在
WindowState中。 2.检查窗口布局结果
LayoutLockedlnner()之后,窗口已经各就各位。然而,一些窗口的flag对布局系统还有更多
的要求,例如:
口 FLAG_FORCE_NOT_FULLSCREEN,拥有这个flag的窗口被显示时,必须同时显示
状态栏等系统窗口。
口 FLAG_FULLSCREEN,拥有这个flag的满屏窗口被显示时,必须隐藏状态栏等系统
窗口。注意,这个flag与FLAG_FORCE_NOT_FULLSCREEN是冲突的。如果两个
窗口分别指定了这两个flag,那么FLAG FORCE NOT_FULLSCREEN的优先级要
高。FLAGFULLSCREEN将被忽略。
口 SYSTEM_UI_FLAG_FULLSCREEN,这是通过view.setSystemUIVisibility()设置的一
个flag。其作用与FLAG_FULLSCREEN一样。
口 FLAG_SHOW_WHEN_LOCKED,在锁屏情况下,拥有这个flag的窗口被显示时,隐
藏锁屏界面,以使窗口得以呈现给用户。当窗口关闭时,自动恢复锁屏。由于这个
flag无视用户设置的加密解锁,因此出于安全考虑,这个flag仅对能够充满屏幕(不
包括状态栏等系统窗口)的窗口起作用。
口 FLAG_DISMISS_KEYGUARD,与FLAG_SHOW_WHEN_LOCKED类似,拥有这个
flag的窗口被显示时,将会退出锁屏状态,而且窗口关闭时不会再恢复锁屏。不过此
锁密码或图形之前,此窗口不会被显示。这个fiag仅对能够充满屏幕(不包括状态栏
等系统窗口)的窗口起作用。
由于这些flag影响了系统窗口、锁屏界面的可见性,也就会影响窗口的布局过程。然
而让人头痛的是,这些flag的生效条件是要求窗口能够覆盖整个屏幕,因此窗口的布
局也影响了这些flag的有效性。这两方面互相依赖又互相影响,因此,WMS引入了
pendingLayoutChanges机制。
这个机制的原理是:首先依据当前的系统窗口、锁屏界面的有效性进行布局,然后基于
这个布局来生效上述flag,并检查系统窗口、锁屏界面的可见性是否发生了变化。如果可见性
发生变化,则通过pendingLayoutChanges变量标记下需要额外完成何种工作。在完成这些工
作之后,重新进行布局,再进行检查,如此反复,直到系统窗口与锁屏界面的可见性不再发
生变化为止。
接下来将分析pendingLayoutChanges的工作机制。回顾一下
performLayoutAndPlaceSurfacesLockedlnner()函数中的那个布局循环。
[WindowManagerService.java->
WindowManagerService.performLayoutAndPlaceSurfacesLockedlnner()]
do{
//处理pendingLayoutChanges
//调用performLayoutLockedInner() 进行布局
//清空pendingLayoutChanges字段
displayContent.pendingLayoutChanges = 0;
//布局检查仅发生在手机主屏幕上,因为只有手机主屏幕才有系统窗口及锁屏界面
if(isDefaultDisplay){
//①首先调用WMP的beginPoatLayoutPolicytw()函数要求WMP初始化检查所需的状态变量
mPolicy.beginPostLayoutPolicyLw(dw,dh);
//按照窗口的 Z序自上而下进行遍历
for(i=windows.size()-1;i>=0;i--){
Windowstate w=windows.get(i);
if(w.mHasSurface){
/*②检查窗口MP的applyPoBtLayoutPolicytw()不会改变窗口的任何属性,而是
记录可能影响到系统窗口或锁屏界面可见性的窗口*/
mPolicy.applyPostLayoutPolicyLw(w,w.mAttrs);
}
displayContent.pendingLayoutChanges l=mPolicy.finishPostLayoutPolicyLw();
}while(displayContent.pendingLayoutChanges!=0);
很显然,我们的调查重点在于WMP的三个函数上。
初始化了一些状态变量。而且方便将布局结果检查过程中所需要的参数一网打尽。
[Phone WindowManager.java->PhoneWindowManager.beginPostLayoutPolicyLw()]
public void beginPostLayoutPolicyLw(int displaywidth,int displayHeight){
//Windowstate类型的变量,保存显示在最上方的满屏窗口
mTopFullscreenOpaquewindowstate=null;
//指定是否需要强制显示状态栏等系统窗口
mForceStatusBar=false;
//指定是否需要在锁屏状态下显示状态栏等系统窗口
mForceStatusBarFromKeyguard=false;
//指定是否需要隐藏锁屏界面
mHideLockScreen=false;
//指示是否允许在屏幕点亮的情况下自动进行锁屏
mAllowLockscreenwhenon=false;
//指示是否需要解除锁屏
mDismissKeyguard-DISMISS_KEYGUARD_NONE;
//指示当前是否正在显示锁屏界面
mShowingLockscreen=false;
//指示是否正在显示屏保
mShowingDream=false;
}
设置上述参数,而finishXXX()则用来根据上述参数执行相应的动作。
public void applyPostLayoutPolicyLw(Windowstate win, WindowManager.LayoutParams attrs){
/*整个函数被以下判断条件包裹着。注意此函数的调用顺序是沿着Z序自上而下对每个窗口进行调用
因此,可以得出一个结论:
仅那些位于第一个可见的满屏窗口(含)之上的可见窗口才能影响系统窗口与锁屏界面的可见性*/
if(mTopFullscreenOpaqueWindowstate ==null&&
win.isVisibleorBehindKeyguardLw()&&!win.isGoneForLayoutLw()){
//只要有窗口拥有FLAG_FORCE_NOT_FULLSCREEN的flag,必然要求强制显示系统窗口
if((attrs.flags &FLAG_FORCE_NOT_FULLSCREEN)!=0){
if(attrs.type==TYPE_KEYGUARD){
mForceStatusBarFromKeyguard = true;
}else{
mForceStatusBar = true;
if (attrs.type==TYPE_KEYGUARD){
mShowingLockscreen=true;
}
&& attrs.type <=LAST_APPLICATION_ WINDOW;
.........
if(applywindow
&&attrs.x==0 s& attrs.y==0
&& attrs.width ==WindowManager.LayoutParams.MATCH_PARENT
&& attrs.height=-WindowManager.LayoutParams.MATCH_PARENT){
//将这个满屏的应用窗口记录到mTopFullscreenOpaquewindowstate中。
//之后对此函数的的调用将不再做任何事情
mTopFullscreenOpaquewindowstate=win;
//如果这个满屏窗口拥有FLAG_SHOW_WHEN_LOCKED,则标记mHideLockScreen为真
if((attrs.flags & FLAG_ SHOW_ WHEN_LOCKED)!=0){
mHideLockScreen=true;
mForceStatusBarFromKeyguard=false;
}
if((attrs.flags & FLAG_DISMISS_KEYGUARD)!=0
&& mDismissKeyguard==DISMISS_KEYGUARD_NONE){
//如果此窗口已经解锁了锁屏界面,设置mDismissKeyguard为CONTINUE,
//要求维持解锁状态。否则设置为START,要求执行解锁动作
mDismlsaKeyguard =mWinDismissingkeyguard==win ?
DISMISS_KEYGUARD_CONTINUE:DISMISS_KEYGUARD_START;
mWinDismissingKeyguard=win;
mForceStatusBarFromKeyguard=false;
}
的值。对我们的分析目标来说,比较重要的状态变量有:
口 mTopFullscreenOpaqueWindowState,类型是WindowState,保存了第一个满屏窗口。
这个满屏窗口的FLAG_FULLSCREEN将会导致系统窗口被隐藏。
口 mForceStatusBar 和mForceStatusBarFromKeyguard,这两个状态变量都会 要求强制显
示系统窗口,对应于FLAG_FORCE_NOT_FULLSCREEN。
口 mHideLockScreen,要求隐藏锁屏界面,对应于FLAG SHOW_ WHEN_LOCKED。从代
码中可以看出,仅当第一个满屏显示的应用窗口拥有这个flag时才会被置为true。
口 mDismissKeyguard,表示是否执行解锁。这是一个int型的变量,取值为DISMISS
KEYGUARD_NONE/START/CONTINUE三种。NONE表示不会解锁,START表示执
行解锁动作,而CONTINUE则表示维持当前状态。
可见性,因为逻辑非常简单直接,这里就不贴代码了。
需要提醒的是,FLAG_FULL_SCREEN和 SYSTEM_UI_FLAG_FULLSCREEN这两个flag
并没有相应的状态变量与之对应。在finishPostLayoutPolicyLw()函数中,当mForceStatusBar
及mForceStatusBarFromKeyguard都为false时,会从mTopFullscreenOpaqueWindowState以及
mLastSystemUIVisiblity中查找这两个flag,并尝试隐藏系统窗口。
finishPostLayoutPolicyLw()的另外一个工作就是确定pendingLayoutChanges的值。当系统
窗口的可见性发生变化时,仅会影响窗口的尺寸,因此在这种情况下pendingLayoutChanges的
值为FINISH_LAYOUT_REDO_LAYOUT。要求performLayoutLockedlnner()函数在新的系统窗
口可见性下重新对所有窗口进行布局。
而当锁屏窗口的可见性发生变化时,情形就比较复杂了,因为当锁屏窗口被隐藏时,显
示给用户的窗口所要求的屏幕旋转方向与锁屏窗口可能不一致,这就需要WMS重新计算屏
幕方向。另外,显示给用户的窗口可能要求系统壁纸作为其背景,所以WMS还需要重新调
整壁纸显示次序,而且无论如何,重新对所有窗口进行布局也是难免的。因此,在锁屏窗
口的可见性发生变化后,pendingLayoutChanges的值为FINISH_LAYOUT_REDO_LAYOUT |
FINISH_LAYOUT_REDO_ WALLPAPER | FINISH_ LAYOUT_REDO_ CONFIG。
当然,如果两者的可见性都没有发生变化,pendingLayoutChanges的值为0。
接下来回过头再看performLayoutAndPlaceSurfacesLockedlnner()的那个布局循环中处理
pendingLayoutChanges的代码:
[WindowManagerService.java-->
WindowManagerService.performLayoutAndPlaceSurfacesLockedlnner()]
//函数调整壁纸窗口的属性,如果有必要,还会重新分配窗口之间的显示层级。
if(isDefaultDisplay
&&((adjustwallpaperwindowsLocked() & ADJUST_WALLPAPER_LAYERS_CHANGED)!=0)){
assignLayersLocked(windows);
displayContent.layoutNeeded=true;
}
//函数根据最顶层的应用窗口的要求更新屏幕的旋转方向
if(isDefaultDisplay && (displayContent.pendingLayoutChanges
&WindowManagerPolicy.FINISH_LAYOUT_REDO_CONFIG)!=0){
if(updateorientationFromAppTokensLocked(true)){
displaycontent.layoutNeeded=true;
mH.sendEmptyMessage(H.SEND_NEW_CONFIGURATION);
}
//目的是强制performLayoutLockedInner()对所有窗口进行布局。不过,如果重复次数大于4,
//将不会再尝试布局
if((displaycontent.pendingLayoutChanges
&WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT)!=0){
displaycontent.layoutNeeded=true;
}
//清空pendingLayoutchanges字段
displaycontent.pendingLayoutchanges=0;
if(isDefaultpisplay){
...…
displayContent.pendingLayoutChanges l= mPolicy.finishPostLayoutPolicyLw();
}while(displaycontent.pendingLayoutChanges !=0);
至此本节分析了DisplayContent布局过程中的布局循环部分的工作原理。
仅是每个窗口的尺寸和位置信息,窗口Surface尚未更新。也就是说,到目前为止,用户还没
有看到窗口被放置到布局过程中所指定的位置。
在接下来的布局后处理中,performLayoutAndPlaceSurfacesLockedlnner()将会为布局好
的窗口设置其Surface大小与尺寸,并附加一些动画效果,例如弹出对话框后的变暗效果
(Dimming)等。另外,在布局的前期处理中初始化了mlnnerFileds对象所保存的一些手机状
态,现在是时候更新它们了。看一下DisplayContent的布局后处理的代码:
performLayoutAndPlaceSurfaceLockedInner()]
private final void performLayoutAndPlaceSurfacesLockedInner (boolean recoveringMemory) {
//布局的前期处理
...
try{
DisplaycontentsIterator iterator=new DisplayContentsIterator();
while(iterator.hasNext()){
do{
//布局循环
}while(displaycontent.pendingLayoutChanges !=0);
/*离开布局循环后,所有窗口的布局信息都已计算完毕。接下来的主要工作是需要设置窗口
Surface的位置与尺寸*/
//初始化3个mInnerFields中重要的状态变量
/*mObscured 在后续的窗口遍历过程中指示当前所处理的窗口是否处于被完全遮挡状态。
所谓的完全遮挡是指窗口位于一个非透明的的满屏窗口之下。注意满屏与全屏的区别,全屏是
指窗口将隐藏状态栏与导航栏,而满屏是指窗口将充满除状态栏与导航栏之外的所有区域。几
乎所有Activity的窗口都是满屏的*/
mInnerFields.mobscured=false;
/*mDimming 表示是否有窗口要求使用Dimming(变暗)效果。例如,当一个AlertDialog弹出
时,其后的所有窗口都会变暗。如果一个窗口启用了透明效果或不是满屏的,则可以通过向
LayoutParameters.flag追加FLAG_DIM_BIHIND以达到此效果*/
mInnerFields.mDimming=false;
//mSysWin是另外一种遮挡状态,表示在遍历过程中,当前的窗口是否被系统窗口所遮挡
//注意,被系统窗口所遮挡不要求系统窗口非透明且满屏
mInnerFields.msyswin=false;
//自顶向下遍历所有窗口。注意windows列表同时描述了窗口的显示顺序
final int N=windows.size();
for(i=N-1;i>=0;i--){
Windowstate w=windows.get(i);
final boolean obscuredChanged=w.mobscured!=mInnerFields.mobscured;
//更新窗口的遮挡状态。窗口的遮挡状态在一定程度上也描述了窗口的可见性
w.mobscured=mInnerFields.mobscured;
/*如果当前窗口未被遮挡,则调用handleNotobscuredLocked函数。
在4.4.4节中介绍mInnerFileds时提到了这个函数。在这个函数中WMS将从窗口
的LayoutParams里获取有关Dimming、屏幕亮度、键盘背光等属性的设置,然后将
这些设置保存到mInnerFields中。与此同时,此函数还检查窗口的尺寸以及类型,
以此更新当前的遮挡状态。当然,可能有多个窗口都指定上述设置,谁说了算呢?WMS
的解决办法是:位于上层的窗口的设置拥有更高的优先级。不过,如果窗口被完全遮
挡,或被系统窗口所遮挡,这些设置将被忽略。
度、键盘背光等状态的设置则是在布局的最终阶段完成的*/
if(!mInnerFields.mobscured){
handleNotobscuredLocked(w,currentTime,innerDw,innerph);
}
......
final WindowstateAnimator winAnimator=w.mwinAnimator;
/*w.shouldAnimateMove()表示窗口的位置是否发生变化(也就是mFrame.left
与mFrame.top发生变化)。WMS通过一个定义的动画anim.window_move_from
decor来完成窗口Surface的移动。WMS做了一个非常聪明的小动作:这个动画是一个
TranslateAnimation,并且动画的移动量为100%p,也就是移动量为容器尺寸。WMS
将mFrame的位置变化量作为动画的容器尺寸,从而达到不改变动画自身的属性,实现移动
到任意位置的动画效果*/
if (w.mHasSurface && w.shouldAnimateMove()){
Animation a=Animationutils.loadAnimation(mContext, com.android.internal.R.anim.window_move_from_decor);
winAnimator.setAnimation(a);
winAnimator.mAnimDw=w.mLastFrame.left-w.mFrame.left;
winAnimator.mAnimph=w.mLastFrame.top-w.mFrame.top;
try{
w.mClient.moved(w.mFrame.left,w.mFrame.top);
}catch (RemoteException e){ }
}
/*调用WindowStateAnimator的commitFinishDrawingLocked()。如果
当前窗口的客户端已经完成对Surface的绘制,并尚未被显示,则向动画系统
提交绘制完毕的通知,在动画的下一帧处理时,窗口将会被显示出来。WMS为窗口
的绘制状态实现了一个状态机,用来与客户端同步Surface的绘制状态。关于这
个状态机的详细内容在动画系统中再深入探讨*/
final boolean committed = winAnimator.commitFinishDrawingLocked(currentTime);
/*更新窗口的Surface的位置与尺寸。
注意,WMS并没有为Surface的尺寸变化提供动画效果。这是因为Surface尺寸
的变化将会导致GraphicBuffer的尺寸变化,这就需要重新进行内存分配。
而且对客户端来说,Surface尺寸变化还意味着重绘的必要性。窗口尺寸变化
的开销是很大的。因此,WMS并没有对尺寸变化使用动画效果。另外,此函数同
时也设置了窗口的位置,而且并不是窗口的mFrame所指定的位置而是mShownFrame。
在布局的过程中并没有计算过这个矩形。所以窗口的实际位置并没有
4.5节中将深入介绍窗口动画的工作原理 */
winAnimator.setSurfaceBoundariesLocked(recoveringMemory);
/*接下来,如果窗口属于一个AppWindowToken,则计算此AppWindowToken已绘
制的窗口数量。在AppWindowToken下的所有窗口都已完成绘制后,AppWindow-
Token的allDrawn状态会被置为true。allDrawn的目的是让Activity的所有
窗口能够同时显示给用户。属于AppwindowToken的窗口经过commitFinishDrawing-
Locked()的处理而变为READ_TO_SHOW状态后必须等待allDrawn为true后才会
显示出来,而其他窗口则没有这个限制*/
}
mResizingwindow列表中。在完成布局时,列表中窗口的客户端将会收到resized()
回调通知*/
updateResizingwindows(w);
}//遍历所有窗口的循环结束
//如果handleNotobscuredLocked()函数没有将mDimming状态设置为true,则表示当前
//没有任何窗口有启用Dimming效果的要求,于是要求WindowAnimator停止Dimming效果
if(!mInnerFields.mDimming && mAnimator.isDimmingLocked(displayId)){
stopDimmingLocked(displayId);
}//对Displaycontent的布局到此结束
}catch (Exception e){
}finally{
Surface.closeTransaction();
}
}
口 设置窗口的遮挡状态。
口 从窗口的LayoutParams中提取关于屏幕亮度、键盘亮度、输入超时等设置。
口 发起或取消Dimming效果。
口 设置窗口Surface的位置与尺寸。其中,位置的变化是有动画效果的,而WMS出于
性能考虑,尺寸的变化则没有动画效果。
口 如果窗口的客户端已经完成对Surface的绘制工作,则显示这个窗口。
的两大阶段:
口 以窗口布局计算为主要任务的布局循环。布局循环以performLayoutLockedlnner()函
数为核心,根据屏幕尺寸以及状态栏、导航栏、输入法窗口等系统窗口确定了作为窗
口布局准绳的8个矩形,再从8个矩形中选择4个矩形作为窗口布局的直接参数,并
以这4个参数计算出窗口的最终位置。
口 根据布局计算结果设置Surface的位置与尺寸,并更新一些由窗口指定的系统属性为
目的布局后处理。
DisplayContent的布局其实就是窗口的布局,而窗口的布局中又以窗口的布局计算为核
心。这部分内容需要读者尽可能深刻地理解。 4.4.6布局的最终阶段
是因为如果布局循环的次数大于6次,有可能DisplayContent pendingLayoutChanges仍然不
为0)。
布局的最终阶段的工作相对繁杂。包括设置屏幕亮度、背光亮度、保持屏幕唤醒等(基
于布局后处理时设置的mlnnerFields中的相关状态),通知窗口客户端其布局发生变化等。在
这里不再一—说明。
其中比较重要的是,在DisplayContent的布局循环中有可能没能将pendingLayoutChanges
清零,此时需要设置DisplayContent的layoutNeeded为true,由布局外围循环重新进行一遍完
整布局。
另外,在设置窗口的位置时使用了动画,并且Dimming效果也是基于动画实现的,因此
在布局的最后,需要通过调用updateLayoutToAnimationLocked()启动动画系统。 4.5WMS的动画系统
在perpareSurfaceLocked()中设置Surface的显示次序。在relayoutWindow()中创建及销毁Surface,
在performLayoutAndPlaceSurfacesLockedlnner()中设置Surface的尺寸与位置,等等。
WindowStateAnimator在Surface的操作过程中发挥了极大的作用,而且布局过程中还有一些
和动画系统相关的重要的内容尚未解释,例如mShownFrame的计算、窗口绘制的状态机的原
理等,这一节将深入探讨WMS的动画系统。
的位置、显示尺寸与透明度,不会影响窗口上所绘制的内容。
窗口内容的动画在窗口的客户端由ViewRootlmpl驱动完成。
另外,动画系统中所改变的显示尺寸与布局过程中的Surface尺寸是两个不同的概念。
布局过程中的Surface尺寸是Surface的实际尺寸,这个尺寸决定了其GraphicBuffer的
大小以及Canvas可以绘制的区域。而动画过程中的尺寸则是渲染尺寸,只是在最终
输出的过程中将Surface的内容放大或缩小。 4.5.1Android 动画原理简介
型的动画。这些动画分别由TranslateAnimation类、ScaleAnimation类、RotateAnimation类以
及AlphaAnimation类实现,它们都是Animation类的子类。
直观上,很多人可能都会认为Animation及其子类在开始动画后会在某一个线程中以
一定的频率不断地操作动画目标的相关属性,从而实现动画效果。其实不然,Android中的
Animation类的功能非常轻量级。在给定了初始状态、结束状态、启动时间与持续时间后,该
类可以为使用者计算其动画目标在任意时刻的变换(Transformation),这是Animation类唯一
的用途。
Transformation类描述了一个变换。它包含了两个分量:透明度及一个二维变换矩阵。同
变换矩阵的运算一样,多个Transformation也可以进行类似于矩阵的先乘、后乘等叠加操作。
其计算方法为,透明度分量相乘,变换矩阵分量进行相应的先乘、后乘。
二维变换矩阵可以在二维空间中对变换目标实现平移、旋转、缩放与切变等效果。将
两个矩阵相乘可以组合其各自的变换效果。关于其变换的原理与运算,有很多图形学
相关的书籍与文章可以参考,本书不再赘述。
本节将探讨一下Animation类是如何计算Transformation的。使用
Animation类的getTranformation()函数获取Transformation,这个函数根据给定的时间戳,计算动画目标所需要进
行的变换。另外,其返回值如果为true,则表示动画尚未完结,调用者需要继续处理下一帧
动画。
public boolean getTransformation(long currentTime,Transformation outTransformation)
在真正的变换计算开始之前,Animation 首先将curentTime进行一个“标准化”操作。因
为动画的时间参数有起始时间、滞后时间、持续时间等。其中滞后时间是指从起始时间开始,
与到变换计算中无疑会大大增加计算的复杂度。“标准化”正是为了解决这个问题。
normalizedTime=((float)(currentTime-(mStartTime+startoffset)))/(float)duration;
normalizedTime 就是currentTime经过标准化后的时间,我们称为标准时间。标准时间隐
藏了上述几个时间参数,后续的计算只需关心标准时间相对于0、1的取值即可,大大减少了
复杂度。
接下来,Animation考虑的是时间线效果的实现,例如,调用者也许希望实现一个先加
速后减速的移动效果,于是Animation 需要对刚刚计算出的标准时间再做进一步加工,如下
所示:
final float interpolatedTime=mInterpolator.getInterpolation(normalizedTime);
mInterpolator是一个实现了Iterpolater接口的对象。Interpolator接口定义了一个
getinterpolation()函数,用于根据一个现有的标准时间计算其对应的插值时间(或者说应用了
时间线效果后的时间)。其子类众多,分别实现了各种各样的时间线效果。调用者可以通过
Animation 类的setlnterpolator()函数设置所期望的效果。以AccelerateDeceleratelnterpolator这
个插值类的实现来说明一下插值器是如何工作的。其getlnterpolation()函数的实现为:
[AccelerateDeceleratelnterpolatorjava->AccelerateDeceleratelnterpolator.getinterpolation0]
public float getInterpolation(float input){
return(float)(Math.cos((input+1)*Math.PI)/2.0f)+0.5f;
所示。当标准时间正常地匀速流逝时,插值时间则经历了
一个先加速后减速的流逝过程。后续的变换计算直接使用
插值时间即可“免费”地获得这个平滑、圆润的效果。
最后,Animation类使用插值时间调用由子类实现的
applyTransformation()函数,计算当前的变换:
applyTransformation(interpolatedTime,outTransformation);
计算结果将被保存至调用者所提供的Transformation 对象中。
[AlphaAnimation java-->AlphaAnimation.apply Transformation0]
protected void applyTransformation(float interpolatedTime,Transformation t){
final float alpha=mFromAlpha;
}
非常清楚了。既然Animation类仅仅根据时间戳计算变换,那么是谁对动画一帧一帧地进行渲
染呢?对,就是本章开始时的例子SampleWindow中所使用的Choreographer类。
的post()函数非常类似。区别就在于,Choreographer类处理回调的时机为屏幕的垂直同步
(VSync)事件到来之时,其处理回调的过程被当作渲染下一帧的工作的一部分。
Choreographer为驱动动画提供了以下函数:
口 postCallback(int callbackType,Runnable action,Object token)
在下一次VSync时执行action参数所指定的操作。callbackType的取值为CALLBACK_
INPUT、CALLBACK_ANIMATION和 CALLBACK_TRAVERSAL,表示action所指定的
回调的工作内容分别为处理输入事件、处理动画、进行布局。当一次VSync事件到来时,
Choreographer将优先执行所有INPUT类型的回调,然后执行ANIMATION,最后才执行
TRAVERSAL。这不正是游戏的主循环的工作流程吗?token参数对回调的执行不会产生影
响。只是当取消一次回调时,除提供回调对象之外,必须提供相同的Token。
口 postCallbackDelayed(int callback Type,Runnable action,Object token,long delayMillis)
其作用与Handler的postDelayed()函数一致。
口 postFrameCallback(FrameCallback callback)
与postCallpack()的本质功能没有太大区别。
不过其回调类型被强制为CALLBACK_ANIMATION,而且FrameCallback接口的定义的
函数为:doFrame(long frameTimeNanos),参数是一个纳秒级的时间戳。因此这个函数天生就
是为处理动画帧所设计的。
口 postFrameCallback(FrameCallback callback,int timeDelayed)
同上,只是增加了一个延迟时间。
的工作骨架了。下面是一个演示类,这个类向外界提供一个名为startAnimation()的函数,用
于立刻开始运行参数所指定的动画。
public class SampleAnimation{
//Choreographer是线程唯一的。通过getInstance()获取当前线程上的实例
Choreographer mchoreographer=Choreographer.getInstance();
Animation mAnimation=null;
//保存需要运行的动画,并通过scheduleNextFrame准备执行第一帧
public synchronized void startAnime(Animation anim){
mAnimation=anim;
scheduleNextFrame();
}
private void scheduleNextFrame(){
mChoreographer.postcallback(Choreographer.CALLBACK_ANIMATION
,mAnimationRunnable ,null);
}
Runnable mAnimationRunnable=new Runnable(){
public void run(){
synchronized(SampleAnimation.this){
//从这里正式开始渲染动画的一帧
if(mAnimation!=null){
//获取当前时间
long time=SystemClock.uptimeMillis();
//新建一个Transformation用以保存Animation的变换计算结果
Transformation transform=new Transformation();
//计算出Transformation,返回值more表示动画是否需要继续执行
boolean more=mAnimation.getTransformation(time,transform);
//使用Animation计算出的Transformation进行渲染
PERFORM_RENDER WITH_TRANSFORMATION(transform);
//如果mAnimation表示动画尚未结束,则向Choreograhper申请处理下一帧
//否则清空mAnimation并不再向Choreographer发送请求
if(more)
scheduleNextFrame();
else
mAnimation=null;
};
通过Cheoregrapher发送一个Runnable以处理一帧动画。在处理动画时,使用Animation.
渲染。所谓渲染,可以是绘制对象或者改变对象的属性。如果动画需要继续,则继续向
Cheoregrapher发送下一帧的处理请求。
数并不会自动重复调用传入的Runnalbe或FrameCalback对象,而是仅仅调用一次。
因此,当调用者处理完一帧后,如果还需要处理下一帧,必须重新调用上述函数,否
则动画将会停止。
上述的代码是Android4.2中最基本的动画实现原理,所有动画都是在这个骨架之上实现
的,包括复杂的WMS。 4.5.2WMS的动画系统框架
在WMS中,有一个同上一节 SampleAnimation.scheduleNextFrame()功能一样的函数,用
于启动窗口动画。其定义如下:
[WindowManagerService.java->WindowManagerService.scheduleAnimationLocked()]
void scheduleAnimationLocked(){
//mLayoutToAnim与其类名一样,用于保存来自布局子系统传递给动画系统的参数
//在后续内容中将讨论mLayoutToAnim成员
final LayoutToAnimatorParams layoutToAnim=mLayoutToAnim;
//如果动画帧已经被发送给Choreographer,则没有必要重复发送
if(!layoutToAnim.mAnimationscheduled){
layoutToAnim.mAnimationscheduled=true;
//处理动画帧的Runnable是mAnimator.mAnimationRunnable。
//mAnimator在分析WMS构成时介绍过,它是一个WindowAnimator类型的对象,是WMS所有动画的管理者
mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION,mAnimator.mAnimationRunnable,null);
}
[WindowAnimator.java-->WindowAnimator.mAnimationRunnable]
mAnimationRunnable=new Runnable(){
@override
public void run(){
//注意,动画帧处理过程是一个双锁!首先锁住mWindowMap,然后是mAnimator。因此无论如
//如何都要避免以相反的顺序使用这两个锁。例如,当动画系统中有函数访问布局系统时,要尤为慎重
synchronized(mService.mwindowMap){
synchronized(WindowAnimator.this){
copyLayoutToAnimParamsLocked();
//渲染一帧动画
animateLocked();
};
在深入讨论这个函数之前,先了解一下WindowAnimator以及其组成部分。 4.5.3WindowAnimator分析
本章4.2节曾经讨论了WMS的窗口管理结构是由以下三种成员组成的:DisplayContent、
WindowToken和WindowState,分别对应屏幕、显示组件和窗口本身。其实动画系统中也有
与之对应的组成结构。它们分别是以屏幕为动画目标的DisplayContentAnimator、以Activity
为动画目标的AppWindowAnimator,以及以窗口为动画目标的WindowStateAnimator。
而WindowAnimator则是协调它们工作的管理者。另外,WMS管理的不仅仅有窗口动
画,还有一些特效动画,如Dimming、屏幕旋转等。所以与之对应的还有DimAnimator、
ScreenRotateAnimation 这两种Animator。
这些Animator的作用如下:
口 DisplayContentAnimator:Android 4.2新增的Animator,仅仅用于存储位于其屏幕上
的其他类型的Animator。例如ScreenRotateAnimation、DimAnimator以及WindowStateAnimator。
口 AppWindowAnimator:用于对一个AppWindowToken进行动画处理。由
AppWindowAnimator 计算得出的Tranformation将被应用在此Token所拥有的所有窗口上。
AppWindowToken的动画主要是用来表现Activity的进入与退出。
口 WindowStateAnimator:用于对一个窗口进行动画处理。它计算得出的Transformation
将与AppWindowAnimator、ScreenRotateAnimation以及父窗口的WindowStateAnimator
三者的Transformation一并应用到窗口的Surface上。所以窗口的动画
其实就是Surface的动画。
口 ScreenRotateAnimation:用于处理转屏动画。由它所计算出的Transformation将被应
用在其所属屏幕的所有窗口之上。
口 DimAnimator:用于实现Dimming效果。这个Animator的动画对象不是窗口,而是
一块黑色的Surface。当需要Dimming效果时,DimAnimator会将这块Surface
以一定的透明度衬于需要Dimming效果的窗口之下。之所以使用一个Animator来完
成这个工作,是为了提供舒服的淡入淡出效果。
stepAnimationLocked(int timestamp)函数。这个函数顾名思义,将其状态迁移到由时间戳
timestamp所指定的一帧上。完成stepAnimationLocked()的调用之后,Animator便更新了绘制
当前帧时其动画目标所需的Transformation。
这些Animator的从属关系如图4-12所示。
接下来,看一下WindowAnimator是如何处理一帧动画并协调上述5种Animator的工作
的。从WindowAnimator.animateLocked()函数开始:
[WindowAnimator.java-->WindowAnimator.animateLocked()]
private void animatelocked(){
if(!mInitialized){
return;
}
//同上一节的SampleAnimation例子一样,WindowAnimator需要获取当前帧的时间戳
mCurrentTime=SystemClock.uptimeMillis();
//记录处理当前帧之前的动画状态到wasAnimating中。
//wasAnimating可以用来确定动画是刚刚开始第一帧或是最后一帧。可以以此进行一些初始化或清理工作
boolean wasAnimating=mAnimating;
mAnimating=false;
//开启一个Surface的Transaction
Surface.openTransaction();
Surface.setAnimationTransaction();
try{
/*①首先处理AppWindowAnimator的动画。这个函数会调用AppWindowAnimator的
stepAnimationlocked()函数以更新AppWindowAnimator的Transformation,
这个变换将会在后面影响窗口的Surface的位置*/
updateAppWindowsLocked();
//接下来会遍历所有的DisplayContentsAnimator,并处理其上的旋转动画与窗口动画
final int numDisplays=mDisplayContentsAnimators.size();
for(inti=0;i< numDisplays;i++){
final int displayId=mpisplayContentsAnimators.keyAt(i);
DisplayContentsAnimator displayAnimator= mDisplayContentsAnimators.valueAt(i);
//②处理屏幕旋转动画
final ScreenRotationAnimation screenRotationAnimation=
displayAnimator.mScreenRotationAnimation;
if(screenRotationAnimation!=null && screenRotationAnimation.isAnimating()){
//更新旋转动画的Transformation,它一样会在后面影响窗口的Surface的位置
if(screenRotationAnimation.stepAnimationLocked(mCurrentTime)){
mAnimating=true;
}else{
/*当旋转完成后,AMS会向Activity发送新的Configuration。这个动作的起点
就是这里了。stepAnimationLocked()的返回值为false时,表示这是动画的最
后一帧。此时通过向mBulkUpdateParams添加SET_UPDATE_ROTATION棋标,以此
通知布局系统旋转动画完毕,需要通知AMS进行更新*/
mBulkUpdateParams l=SET_UPDATE_ROTATION;
screenRotationAnimation.kill();
//由此可见,当屏幕没有处于旋转过程中时,
//DisplayContentsAnimator的mScreenRotationAnimation是null
displayAnimator.mScreenRotationAnimation=null;
}
}
/*③接下来处理windowstatennimator的动画。这个处理分为两部分,
普通窗口的动画与壁纸动画。此函数更新了WindowStateAnimator的Transformation的值。同时,在这一步中
也会检查窗口绘制状态机的状态,如果窗口在Displaycontent的布局后处理中完成了
commitFinishDrawingLocked()的提交,那么窗口的状态会在这一步中变为IS_DRAWN,随后
prepareSurfaceLocked()会将窗口显示出来*/
performAnimationsLocked (displayId);
final WinAnimatorList winAnimatorlist = displayAnimator.mwinAnimators;
final int N=winAnimatorList.size();
for(int j=0;j < N;j++){
/*④渲染动画
WindowStateAnimator的prepareSurfaceLocked()将集合AppWindowAnimator、
ScreenRotationAnimator以及WindowStateAnimator的Transformation到一
起,修改Surface的layer、matrix、alpha等属性,从而实现窗口动画的渲染*/
winAnimatorList.get(j).prepareSurfaceLocked(true);
}
}
//至此窗口动画的一帧渲染完成
......
//⑤渲染Dimming和屏幕旋转效果
for(int i=0;i < numDisplays ;i++){
......
//首先是屏幕旋转动画
if(screenRotationAnimation!=null){
screenRotationAnimation.updateSurfacesInTransaction();
}
//处理Dimming动画。前面提过,Dimming效果是由一块黑色的Surface完成的
//Dimming动画的处理分为以下两个步骤
final DimAnimator.Parameters dimParams=displayAnimator.mDimParams;
final DimAnimator dimAnimator=displayAnimator.mDimAnimator;
//更新Diming参数,包括Dimming的尺寸、Dimming的颜色深度以及Dimming的目标等
// 所谓Dimming的目标是指Dimmingsurface将要衬于其下的那个窗口
if(dimAnimator!=null && dimParams!=null){
dimAnimator.updateParameters(mContext.getResources(),dimParams, mCurrentTime);
}
//然后根据当前的时间戳,更改Dimming Surface的透明度,实现Dimming的淡入淡出效果
if(dimAnimator!=null && dimAnimator.mpimshown){
mAnimating l=dimAnimator.updateSurface(isDimmingLocked (displayId),
mcurrentTime,!mservice.okToDisplay());
}
}
}finally{
Surface.closerransaction();
}
//至此,动画的一帧已经渲染完成
......
/*⑥如果有必要,向布局系统请求一次布局
在动画的过程中,有时需要布局系统“搭把手”,例如前面所见到的,在屏幕旋转动画完成后需要布
局系统更新Configuration到AMS。为了实现从动画系统到布局系统的请求传递,WindowAnimator定义
了以下两个成员以在动画渲染的过程中收集所有对布局系统的请求,并在动画帧渲染结束后,将其通过
updateAnimToLayoutLocked()一次性发送给布局系统*/
if(mBulkUpdateParams!=0 || mPendingLayoutChanges.size()>0){
updateAnimroLayoutLocked();
}
//⑦如果仍有动画处于运行状态,通过WMS安排下一帧的处理
if(mAnimating){
synchronized(mService.mLayoutToAnim){
mService.scheduleAnimationLocked();
}
}else if (wasAnimating){
//wasAnimating为true而mAnimating为false表示所有动画在这一帧停止
//此时需要令布局系统重新进行一次布局。其实,updateAnimTolayoutLocked()就已经
//向WMS请求一次重新布局了,所以这里略显多余
mService.requestTraversalLocked();
}
}
这个函数相对较长,不过结构非常清晰:
口 首先通过updateAppWindowsLocked()计算AppWindowToken动画在当前时间所要求
的变换。
口 针对每一个DisplayContentAnimator,计算其屏幕旋转动画在当前时间所要求的
变换。
口 针对每一个DisplayContent下的每一个窗口,计算其自身动画在当前时间所要求的
变换。
口 针对每一个DisplayContent下的每一个窗口,将上述3个变换同时应用到窗口的
Surface上,实现窗口动画帧的渲染。
口 渲染效果动画,包括屏幕旋转以及Dimming效果。
口 如果动画渲染的过程中,需要布局系统有相应的动作,则将请求放置在
mBulkUpdateParams与mPendingLayoutChanges两个成员中,动画渲染完毕后统一通
过updateAnimToLayoutLocked()将动作请求发送给布局系统。
口 如果动画仍需进行,通过WMS.scheduleAnimationLocked()安排下一帧的处理。
而是一批保存在各个Animator中的动画。
ScreenRotateAnimationAnimator以及WindowStateAnimator。限于篇幅,本节将仅探讨三个
Animator中最重要的WindowStateAnimator的工作原理。剩下的两个Animator读者可以参照
WindowStateAnimator自行研究,因为它们的本质是一致的,而且也比WindowStateAnimator
简单。 4.5.4深入理解窗口动画
研究窗口动画离不开WindowStateAnimator(以下简称WSA)。WSA在WindowState的构
造函数中随之一起被创建,二者互持双方的引用。WindowState保存了窗口管理方面的属性,
例如窗口位置、尺寸、主序、子序、父窗口的引用等,而WSA则保存了窗口的Surface的属
性。前者侧重于窗口管理,后者则侧重于窗口显示。WSA的几个重要成员变量如下:
口 mSurface 保存了窗口的Surface,同时也是WSA的动画目标。
口 mWin 保存了WSA所对应的窗口。
口 mAnimation 是由WMS设置给WSA的一个Animation类的对象,保存了WMS要求
窗口执行的动画。当窗口没有运行动画时,mAnimation对象保持为空。
口 mAnimDw和mAnimDh 描述了窗口动画时的“容器尺寸”。容器尺寸一般是
DisplayInfo.appWidth/Height,也就是屏幕上排除状态栏/导航栏等系统窗口之后供应
用程序显示的区域尺寸。在为Animation的属性设置带有“p”后缀时,水平方向与
垂直方向的参考值就是这两个变量。另外,读者可以回顾一下在布局后的处理过程中
使用动画移动窗口位置的情况,当时将这两个值分别设置成窗口位置变化量,而动画
的移动距离都是100%p。
口 mAnimLayer,对除Wallpaper与IME之外的窗口来说,其值等于WindowState的
mLayer,也就是窗口最终显示次序。在prepareSurfaceLocked()中,它将作为Surface
的layer被设置到显示系统。
口 mAlpha 和 mShownAlpha 都表示窗口的Surface的透明度。它们两个的区别是,
mAlpha是由窗口的LayoutParams.alpha指定的,也就是客户端要求的透明度。而
mShownAlpha是mAlpha在混合了WindowStateAnimator、AppWindowAnimator、
ScreenRotateAnimation以及父窗口的WindowStateAnimator的Transformation中的
alpha分量后的透明度,也就是实际透明度。
口 mDsDx、mDtDx、mDsDy和mDtDy 这4个变量自己本身没有什么意义,它们组合起
来就是Surface的变换矩阵。它们是4个Animator的Transformation变换矩阵组合之
后(相乘)的缩放与旋转分量。通过Surface.setMatrix()方法可以改变Surface最终显
示的角度与缩放尺寸。
二维变换矩阵是3×3的矩阵,其左上角的2×2子阵负责缩放与旋转。
口 mDrawState 保存了窗口的绘制状态。从窗口最初的创建,到最终得以显示到屏幕
上,共经历了NO_SURFACE、DRAW_PENDING、COMMIT_DRAW_PENDING,
READTO_SHOW和HAS_DRAWN 5个状态。后面将深入探讨这5个状态的迁移 过程。
接下来将从开始动画及渲染一帧动画两个方面学习WSA的工作原理。
在WMS的relayoutWindow()函数中,当窗口由不可见变为可见时,执行了下面一条
语句:
winAnimator.applyEnterAnimationLocked();WSA.applyEnterAnimationLocked()将会为当前窗口开始一个淡入动画,将窗口显示出来。
它调用了WSA中更为通用的启动窗口动画的函数applyAnimationLocked()。
[WindowStateAnimator.java-->WindowStateAnimator.applyAnimationLocked()]
boolean applyAnimationLocked(int transit,boolean isEntrance){
//transit参数用来指定动画的意图,WSA将通过动画的意图寻找合适的动画资源
......
if(mService.okToDisplay()){
/*首先尝试由WindowManagerPolicy获取合适的动画资源。WindowManagerPolicy的
selectAnimationLw()为状态栏、导航栏等系统窗口分配了一些特殊的动画*/
int anim=mPolicy.selectAnimationLw(mwin,transit);
int attr=-1;
Animation a=null;
if(anim!=0){
//如果WMP为当前窗口指定了动画资源,则选择此动画
a=anim!=-1?Animationutils.loadAnimation(mcontext,anim):null;
}else{
//否则根据transit选择WSA默认的动画资源
switch(transit){
......
}
if(attr>=0){
a=mService.loadAnimation(mWin.mattrs,attr);
}
}
if(a!=null){
//保存选取的Animation对象
setAnimation(a);
}
}else{
//如果屏幕尚未准备好,则不进行任何动画
clearAnimation();
}
return mAnimation!=null;
}
setAnimation()的实现为:
[WindowStateAnimator.java->WindowStateAnimator.setAnimation()]
public void setAnimation(Animation anim){
mAnimating=false;
mLocalAnimating=false;
//保存选择的anim到mAnimation
mAnimation=anim;
......
//初始化WSA的Transformation
mTransformation.clear();
/*标记以下变量为true,表示在stepAnimation()时需要将WSA自身的Transformation参与计算
当WSA本身没有动画时,完全可以保持mrransformation为单位变换状态,并在stepAnimation()
中不加判断地让其参与计算,但是会降低效率*/
mHasLocalTransformation=true;
}
可以看出,WSA并没有给WMS非常大的自由去选择任意动画。WMS需要通过一个
transit参数向WSA提出动画的意图,WMP和WSA再根据此意图选择一个既有动画保存在
mAnimation中。
判断一个窗口是否正在运行动画的方法是判断其对应的WSA的mAnimation成员是否为null。
仅仅如此尚不能让窗口动起来,因为WindowAnimator可能正处于空闲状态。即
便WindowAnimator正在不停地工作,此WSA并没有位于DisplayContentsAnimator的
mWindowAnimators列表中,因此其动画一样不能得到渲染。那怎么办呢?
重新回到WMS的relayoutWindow()函数,随后它调用了performLayoutAndPlaceSurfacesLocked()
函数,发起了一次重新布局。在布局的最终阶段,布局系统调用
updateLayoutToAnimationLocked()将包括WSA在内的所有Animator传递给了动画系统。
[WindowManagerService.java-->WindowManagerService.updatelayoutToAnimationLocked()]
void updateLayoutToAnimationLocked(){
//mLayoutToAnim 是布局系统向动画系统传递信息的桥梁
final LayoutToAnimatorParams layoutToAnim = mLayoutToAnim;
synchronized(layoutToAnim){
//WinAnimatorList其实就是ArrayList
SparseArray allWinAnimatorLists= layoutToAnim.mwinAnimatorLists;
//注意,每次布局系统向动画系统传递Animator时都会先将列表清空
allWinAnimatorLists.clear();
//遍历所有的Displaycontent
DisplayContentsIterator iterator=new DisplayContentsIterator();
while(iterator.hasNext()){
//获取Displaycontent下的所有窗口列表windows
for(inti=0;i < N ;i++){
final WindowstateAnimator winAnimator =windows.get(i).mWinAnimator;
/*如果WSA拥有Surface,就将其添加到Animator列表中,为什么只要有Surface
就会将其添加到列表中,而不需要判断WSA是否有动画在进行?因为WSA的地位不同,
WSA即便自己没有动画,但它肩负着将AppWindowAnimator和ScreenRoatationAnimator
的变换应用到窗口的Surface中的任务*/
if(winAnimator.mSurface!=null){
winAnimatorList.add(winAnimator);
}
}
//保存Window列表
allwinAnimatorLists.put(displaycontent.getDisplayId(),winAnimatorList);
}
//接下来还要向mLayoutToAnim添加壁纸动画相关的参数和AppWindowAnimator
......
//一切准备完毕后,安排渲染下一帧
scheduleAnimationLocked();
}
}
在完成布局后,布局系统会将所有布局后的窗口的WSA保存至mLayoutToAnim对象中,
然后安排动画系统开始处理下一帧动画的渲染。mLayoutToAnim就像一辆列车,载满了WSA
驶向动画系统。
再次来到动画系统处理动画帧的入口mAnimationRunnable.run()函数处,第一个调用的函
数就是copyLayoutToAnimParamsLocked()。这个函数将位于mLayoutToAnim中的WSA接下
车,再保存到Display ContentsAnimator的mWinAnimators列表中。
[WindowAnimator.java->WindowAnimator.copyLayoutToAnimParamsLocked()]
private void copyLayoutToAnimParamsLocked(){
final LayoutToAnimatorParams layoutToAnim=mService.mLayoutToAnim;
synchronized(layoutToAnim){
//从mLayoutToAnim中取出与Wallpaper及Dimming效果相关的参数
......
final int numDisplays = mpisplayContentsAnimators.size();
for(inti=0;i< numpisplays;i++){
......
displayAnimator.mwinAnimators.clear();
//从mLayoutroAnim中取出所有保存的WSA,并悉数添加到DisplaycontentsAnimator中
final WinAnimatorList winAnimators= layoutToAnim.mWinAnimatorLists.get(displayId);
if (winAnimators!=null){
displayAnimator.mwinAnimators.addAll(winAnimators);
}
}
//取出AppWindowAnimator并放到mAppAnimators列表中
......
}
}
经过一番从布局系统到动画系统的辗转,WSA终于出现在DispalyContentsAnimator的
mWinAnimators列表中。在接下来的animateLocked()中,WSA将会参与其中。
回顾一下WindowAnimator.animateLocked(),在完成AppWindowAnimator和ScreenRotationAnimator
的Transformation 计算后,通过执行performAnimationsLocked()函数计算WSA的Transformation。
(1)performAnimationsLocked分析
[WindowAnimatorjava-->WindowAnimator.performAnimationsLocked()]
private void performAnimationsLocked(final int displayrd){
//处理当前Displaycontent所拥有的所有WSA
updateWindowsLocked(displayId);
//处理Wallpaper的动画,暂不讨论
updateWallpaperLocked(displayId);
}
继续updateWindowsLocked()的代码:
[WindowAnimator.java->WindowAnimator.updateWindowsLocked()]
private void updatewindowsLocked(final int displayId){
final WinAnimatorList winAnimatorList=
getDisplayContentsAnimatorLocked(displayId).mwinAnimators;
......
for(int i = winAnimatorList.size() - 1;i >= 0;i --){
WindowstateAnimator winAnimator=winAnimatorList.get(i);
Windowstate win = winAnimator.mwin;
final int flags=winAnimator.mAttrFlags;
if(winAnimator.mSurface!=nul1){
......
//计算WSA的变换
final boolean nowAnimating=winAnimator.stepAnimationtocked (mCurrentTime);
/*接下来处理force hiding相关的逻辑。Force hiding 是指当WMP认为某些窗口具有force
hiding的特性时,所有位于其下的窗口都应被隐藏*/
......
}
/*处理WSA的绘制状态。当WSA的状态为READY_TOSHOW时,执行WSA.performshowLocked()
将状态切换为HAS_DRAWN,拥有HAS_DRAWN状态的窗口在WSA. prepareSurfaceLocked()函数中被显示出来*/
final AppWindowToken atoken=win.mAppToken;
if(winAnimator.mDrawstate==WindowstateAnimator.READY_TO_SHOW){
if (atoken==null ll atoken.allDrawn){
if(winAnimator.performShowLocked()){
mPendingLayoutchanges.put(displayId,WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM);
}
}
}
/*更新AppWindowAnimator的thumbnailLayer。在遍历所有窗口的过程中,寻找一个最靠前的
窗口的显示次序作为其窗口所属的AppWindowToken的缩略图的显示次序*/
final AppWindowAnimator appAnimator=winAnimator.mAppAnimator;
if (appAnimator!=null && appAnimator.thumbnail!=null){
......
}
}
......
}
口 通过调用WSA的stepAnimationLocked()更新其所维护的mTransformation。这个
Transformation将在后面WSA.prepareSurfaceLocked()的调用中影响Surface的最终位置。
口 如果窗口的绘制状态为READY_TO_SHOW,则通过调用performShowLocked()
函数将绘制状态改为HAS_DRAWN。拥有HAS_DRAWN状态的窗口在WSA.
prepareSurfaceLocked()函数中被显示出来。先记住这个操作,在后续的小节中将整理
窗口的绘制状态的迁移以及其意义。
boolean stepAnimationLocked(long currentTime){
mWasAnimating=mAnimating;
if(mService.okToDisplay()){
if(mWin.isDrawnw()&& mAnimation!=null){
// mAnimation不为null,表示WSA拥有自己的动画,因此需要计算其Transformation
mHasTransformation=true;
mHasLocalTransformation =true;
// 如果此时mLocalAnimating为false,表示这是WSA的第一帧动画,需要初始化其Animation
if(!mLocalAnimating){
// 注意initialzied的参数,动画对象的尺寸为窗口的mFrame的尺寸
// 而容器尺寸为mAnimDw和mAnimDh
mAnimation.initialize(mWin.mFrame.width(),mwin.mFrame.height(),mAnimDw,mAnimph);
// mAnimDw与mAnimph有可能在 布局后处理中被改成窗口的移动量,因此最好在其
// 完成mAnimation初始化后将其还原回屏幕上应用程序区的尺寸
final DisplayInfo displayInfo=mwin.mDisplayContent.getDisplayInfo();
mAnimDw=displayInfo.appwidth;
mAnimDh=displayInfo.appHeight;
// currentTime就是动画的起始时间
mAnimation.setstartTime(currentTime);
mLocalAnimating = true;
mAnimating = true;
}
if((mAnimation!=null)&& mLocalAnimating){
// 执行stepAnimation()函数更新mTransformation
if(stepAnimation(currentTime)){
// 注意,如果动画在持续,在这里就返回了
return true;
}
}
}
/*如果WSA没有动画或动画已经结束,则标记下述状态变量为false。表示在计算Surface最终的
变换时,WSA的mTransformation不参与计算*/
mHasLocalTransformation = false;
}
//当执行到这里时,WSA的动画已经播放完毕,接下来便是一些动画的清理动作
//将mLocalAnimating设置为false,这样当下次开始动画时,就可以对Animation进行初始化了
mLocalAnimating=false;
/*回顾4.3节最后关于mAnimLayer的讨论,在assignLayersLocked()中mAnimLayer可能会被增加
一个来自AppWindowAnimator的矫正。现在,动画结束了,应该撤销这个矫正让窗口回到其本来的位置,
也就是mLayer所指定的位置*/
mAnimLayer=mwin.mLayer;
//返回false表示动画已结束
return false;
}
stepAnimationLocked()将较多的经历花费在第一帧时动画的初始化与完成最后一帧后的
清理工作上。在动画持续的过程中,其工作还是很简单的,就是调用WSA.stepAnimation()函
数以更新Transformation,注意没有locked()。
[WindowStateAnimator.java-->WindowStateAnimator.stepAnimation()]
private boolean stepAnimation(long currentTime){
if((mAnimation==null) || !mLocalAnimating){
return false;
}
mTransformation.clear();
final boolean more = mAnimation.getTransformation (currentTime,mrransformation);
return more;
}
很简单,不是吗?更新Transformation 调用Animation.getTransformation()函数即可。其
原理在4.1节便已经介绍过。
WindowAnimator的animateLocked()函数,在完成performAnimationsLocked()之后,再次
遍历DisplayContent下的所有WSA,并分别执行它们的prepareSurfaceLocked()。
prepareSurfaceLocked()将完成动画帧的渲染动作。
Transformation通过WSA的这个函数影响窗口动画的渲染。
算mShownFrame、Surface的变换矩阵以及mShownAlpha(Surface透明度)的计算。我们可
以将其称为渲染参数。
Android提供的动画工具类可以实现平移、旋转、缩放以及透明度4种动画。因此在窗
口动画渲染过程中,需要提取这4种分量,并以此设置Surface的相关属性,从而实现动画帧
的渲染。其中mShownFrame提取了窗口平移分量,Surface变换矩阵提取了旋转与缩放分量,
mShownAlpha自然提取了透明度分量。注意mShownFrame仅仅提取了窗口平移分量,也就
是说只有其left和top是变换后的结果,而其宽度与高度则与mFrame保持一致。
上述三个渲染参数的计算过程繁杂却不失规律性:将AppWindowAnimator、Screen-
RotationAnimation、父窗口的WSA以及窗口自身的WSA的4个Transformation组合在一起,
然后从组合后的矩阵中提取平移分量交给mShownFrame,将旋转与缩放子阵作为Surface的
变换矩阵,再将4个Transformation的透明度分量相乘作为mShownAlpha。
下面分析computeShownFrameLocked()的实现。
移矩阵T乘以一个缩放矩阵S的结果与S乘以T的结果是不一样的,因此矩阵乘法分
为左乘和右乘两种,要格外注意操作顺序。在computeShownFrameLocked()中只使用
了右乘。
void computeShownFrameLocked(){
final boolean selfTransformation = mHaslocalTransformation;
/*获取父窗口、AppWindowAnimator的Transformation。如果窗口没有父窗口或不属于
AppWindowToken,亦或二者不处于动画过程中,则对应的Transformation为null*/
Transformation attachedTransformation=....…
Transformation appTransformation=....
//暂不考虑壁纸动画相关的内容
......
final int displayId=mwin.getDisplayId();
final ScreenRotationAnimation screenRotationAnimation =
mAnimator.getScreenRotationAnimationLocked(displayId);
final boolean screenAnimation=....
/*目前,computeshownFrameLocked()收集了4种Animator的变换,如果只要有一种Animator
正在运行中,就需要整合这些Transformation进行mShownFrame的计算*/
if(selfTransformation || attachedTransformation!=null
|| appTransformation!=null || screenAnimation){
//窗口的mFrame
final Rect frame=mWin.mFrame;
//tmpFloats用于保存tmpMatrix的9个矩阵元素
final float tmpFloats[] = mService.mTmpFloats;
//二维变换矩阵,用于存储Transformation矩阵分量的组合结果
final Matrix tmpMatrix = mwin.mTmpMatrix;
//①首先将窗口缩放添加到结果矩阵中。postscale()函数用于右乘一个缩放矩阵
tmpMatrix.postScale(mwin.mGlobalscale,mwin.mGlobalscale);
//②将WSA自身的Transformation添加到结果矩阵中。postconcat()用于右乘任意矩阵
if(selfTransformation){
tmpMatrix.postConcat(mTransformation.getMatrix());
}
//③将窗口的实际位置添加到结果矩阵中。postTranslate()函数用于右乘一个平移矩阵
tmpMatrix.postTranslate(frame.left +mWin.mxOffset,
frame.top+mwin.mYoffset);
//④将父窗口的动画变换添加到结果矩阵中
if (attachedTransformation!=nul1){
tmpMatrix.postConcat(attachedTransformation.getMatrix());
}
//⑤将AppWindowAnimator的变换添加到结果矩阵中
if(appTransformation!=null){
tmpMatrix.postconcat(appTransformation.getMatrix());
}
/*⑥将UniverseBackground的变换添加到结果矩阵中。UniverseBackground是类型为TYPE
UNIVERS_BACKGROUND的窗口,由SystemUI实现。虽然它与其他窗口并没有什么实际的从
属关系,但是Android将其作为所有真实窗口的容器,或者如其名字所说,它是Android显示
世界的字宙。将这个变换添加到结果矩阵的意义是:既然容器移动了,内部的所有窗口都要跟
着移动*/
//⑦将screenRotationAnimation的变换加入结果矩阵中
if(screenAnimation){
tmpMatrix.postConcat(screenRotationAnimation.getEnterTransformation().getMatrix());
}
//⑧将放大镜效果的变换加入结果矩阵中
MagnificationSpec spec=mWin.getWindowMagnificationSpecLocked();
if (spec!=null s& !spec.isNop()){
tmpMatrix.postScale(spec.mScale,spec.mscale);
tmpMatrix.postTranslate(spec.moffsetX,spec.moffsetY);
}
/*结果矩阵已经计算完成,提取它们的缩放旋转子阵作为Surface的变换矩阵,
并将平移分量保存到mshownFrame中*/
mHaveMatrix=true;
//将结果矩阵的元素放到tmpFloats中
tmpMatrix.getValues(tmpFloats);
//保存Surface的变换矩阵
mDsDx=tmpFloats[Matrix.MSCALE_X];
mDtDx=tmpFloats[Matrix.MSKEW_Y];
mDsDy=tmpFloats[Matrix.MSKEW_X];
mDtDy=tmpFloats[Matrix.MSCALE_Y];
//计算mShownFrame,注意只有left和top分量取自变换结果,width和height同mErame
float x=tmpFloats[Matrix.MTRANS X];
float y=tmpFloats[Matrix.MTRANS_Y];
int w=frame.width();
int h=frame.height();
mWin.mshownFrame.set(x,y,x+w,y+h);
/*接下来计算mShownAlpha。注意,由于对支持透明绘制的Surface的透明度是通过软件
实现的而不是硬件加速,因此为了保证动画的流畅度,正在做平移/缩放/旋转动画的支持透
明绘制的Surface将不做透明度的变换,以省去通过软件渲染透明度动画的时间进而保证其
他动画的流畅。在framework/base/core/res/res/values/config.xml 中将config
sf_limitedAlpha设置为false可以取消这个限制*/
mShownAlpha=mAlpha;
if(!mservice.mLimitedAlphaCompositing
|| (!PixelFormat.formatHasAlpha(mwin.mattrs.format)
|| (mWin.isIdentityMatrix(mDsDx,mDtDx,mDsDy,mDtDy)
&& x== frame.left && y==frame.top))){
// 透明度是通过乘法混合的
if(selfTransformation){
mShownAlpha *=mTransformation.getAlpha();
}
if(attachedTransformation!=null){
mShownAlpha *=attachedTransformation.getAlpha();
}
if(appTransformation!=null){
mShownAlpha *=appTransformation.getAlpha();
}
if(mAnimator.mUniverseBackground!=null){
mShownAlpha *= mAnimator.mUniverseBackground.muniverseTransform.getAlpha();
}
if(screenAnimation){
mShownAlpha*= screenRotationAnimation.getEnterTransformation().getAlpha();
}
}else{
}
return;
}
/*当没有与窗口相关的动画在运行时,仍然要设置UniversBackground以及放大镜效果,其
原理一样,这里不再赞述*/
......
}
注意变换的顺序,它们是不能轻易改动的,因为矩阵乘法不满足交换律。为什么Android
要采用这个顺序呢?将参与变换的对象按照顺序列出来:动画窗口,动画窗口的父窗口,动画
窗口所属的Activity,UniverseBackground,屏幕旋转。不难发现规律,它们是按照从属关系由
小到大排列的。因为Android希望父窗体的动画应用于所有子对象(右乘,即追加效果,是在左边矩阵现有效果基础上计算、追加效果)。例如,当使用上述变
换顺序,子窗口的变换矩阵为A而父窗口的变换矩阵为B时,子窗口的最终变换为AB,父
窗口的最终变换仍然是B。当子窗口的变换固定,无论父窗口的变换B为任何值,子窗口相
对于父窗口的变换固定为A,也就是说父窗口做任何动画,子窗口都会如影随形地相对于父
窗口保持静止。因此,倘若读者需要增加一个ActivityGroundAnimator,那么这个变换应插入
AppWindowAnimator与UniverseBackground之间。
至此,mShownFrame、Surface的变换矩阵与透明度计算完毕。接下来将它们设置给
Surface。
[WindowStateAnimator.java-->WindowStateAnimator.prepareSurfaceLocked()]
public void prepareSurfaceLocked(final boolean recoveringMemory){
final Windowstate w = mWin;
//计算渲染参数
computeShownFrameLocked();
/*还是setSurfaceBoundariesLocked(),将计算好的mShownFrame设置为Surface的位置,
完成平移操作。注意,尺寸仍为mFrame的尺寸。动画所要求的缩放交换将通过变换矩阵完成*/
setSurfaceBoundariesLocked(recoveringMemory);
if(...){
......
}else if (mLastLayer!=mAnimLayer//如果Surface参数发生了变化
|| mLastAlpha!=mshownAlpha || mLastDsDx!=mDsDx || mastDtDx!=mDtDx
|| mLastDsDy!=mDsDy || mastDtDy!=mDtDy || w.mLastHScale!=w.mHScale
|| w.mLastVScale!=w.mVScale || mLastHidden){
if (mSurface!=null){
try{
//设置透明度
mSurfaceAlpha=mshownAlpha;
mSurface.setAlpha(mshownAlpha);
//设置窗口的显示次序
mSurfaceLayer=mAnimLayer;
mSurface.setLayer(mAnimLayer);
//设置Surface的变换矩阵,缩放与旋转变换
mSurface.setMatrix(
mDsDx*w.mHScale,mDtDx*w.mVScale,
mDsDy*w.mHScale,mptDy*w.mVScale);
//如果窗口的绘制状态是HAS_DRAWN,并且尚未显示,则显示Surface
//showsurfaceRobustlyLocked()将通过Surface.show()将窗口显示出来
if(mLastHidden && mDrawstate==HAS_DRAWN){
if(showsurfaceRobustlyLocked()){..}else {....... }
}
}catch(RuntimeException e){
}
}
}
......
}
很简单,上文提到的三个渲染参数都被设置到了Surface。用户可以看到,在这一帧中,
窗口的位置发生了变化!
现了窗口从最初的创建到显示在屏幕的过程。这一小节将介绍这个状态的一些细节。
第一个状态:NO_SURFACE。窗口的绘制状态保存在WSA.mDrawState中。当一个窗口
刚刚被WMS的addWindow)函数创建时,WSA在WindowState的构造函数中被一并创建。
此时,窗口的绘制状态为NO_SURFACE,因为在relayoutWindow()之前,窗口是没有Surface
的,当然也不可能显示出来。
第二个状态:DRAW_PENDING。随后,客户端调用了relayoutWindow(),此时WMS通
过WSA的createSurfaceLocked()为窗口创建了一块Surface。此时窗口的绘制状态被设置为
DRAW_PENDING。也就是说,窗口正拥有一块空白的Surface,此时需要客户端在Surface上
作画。由于Surface仍是空白状态,因此此时仍不能让窗口显示出来。
第三个状态:COMMIT_DRAW_PENDING。回顾SampleWindow的例子,在通过Canvas
完成在Surface上的绘制之后,调用IWindowSession.finishDrawing()函数,通知WMS客户端
已经完成在Surface上的绘制。此时,窗口的绘制状态便成为COMMIT_DRAW_PENDING,
意思是窗口的绘制已经完成,正在等待由布局系统进行提交,窗口距离显示在屏幕上已经进
了一步。
第四个状态:READY_TO_SHOW。之后工作就比较多了,WMS会继续调用
performLayoutAndPlaceSurfacesLocked()启动一次重新布局。正如在布局后处理中所看到的,会调用
WSA的commitFinishDrawingLocked()。如果窗口的状态为COMMIT_DRAW_PENDING时,
窗口的状态会再迁移到READ_TO_SHOW,此时窗口距离最终显示已经很接近了。READ
TO_SHOW表示窗口可以随时被显示,但是为什么不直接将其显示出来呢?因为窗口可能
属于某一个AppWindowToken,Android希望当AppWindowToken所有的窗口都已准备好后
再将它们一并显示出来。当然,如果窗口不属于AppWindowToken,或者AppWindowToken
下的所有窗口都已准备好显示或已经显示,commitFinishDrawingLocked()会立刻调用
performShowLocked(),进行绘制状态的下一步迁移。
第五个状态:HAS_DRAWN。performShowLocked()在布局系统中被commitFinishDrawingLocked()
调用,也可能在动画系统中被WindowAnimator的updateWindowsLocked()在
处理动画帧的过程中调用。performShowLocked()会将窗口的绘制状态进一步迁移为最终状态
HAS_DRAWN。拥有这个状态的窗口距离显示已经无限接近了。
最后,在WSA的prepareSurfaceLocked()中,处于HAS_DRAWN状态却未被显示的窗口
通过showSurfaceRobustlyLocked()完成最终显示。
值得一提的是,当窗口的旋转方向发生变化后,窗口的状态会被重置为DRAW_PENDING,
表示窗口必须重新绘制自己的内容。完成之后,再一步一步按照上述状态迁移将新的内容显示出来。
4.5.5交替运行的布局系统与动画系统
个锁。而布局系统则保持着wMS.mWindowMap锁,因此,布局系统的布局过程与动画帧处
理过程是互斥的。然而,动画帧处理和布局过程可能是交替的。
执行一个名为updateLayoutToAnimLocked()函数。这点在“开始窗口动画”中已经介绍
过。此函数会刷新动画系统需要在下一帧执行的Animator列表。因此在上一帧还在运行的
Animator,经过一次布局后,下一帧可能就忽然不见了,同时,新的Animator有可能被加
入列表中并参与下一帧的绘制过程。这个Animator列表被保存在WMS.mLayoutToAnim中。
updateLayoutToAnimLocked()立刻向WindowAnimator发出处理下一帧动画的命令。在开始
处理下一帧动画时,WindowAnimator会将Animator列表从mLayoutToAnim取出,并逐个
处理。
注意,updateLayoutToAnimLocked()和发送处理下一帧动画的命令是无条件的。也就是说,
只要进行了一次重新布局,必然会“惊扰”动画系统。
mPendingLayoutChanges 成员变量以及updateAnimToLayoutLocked()函数。
在动画的过程中,某些动画的开始或结束,或者某些操作对布局系统来说有着特别
的意义,需要布局系统对此做出反应。此时可以将需要布局系统做出变化的要求或状态存
入mPendingLayoutChanges以及mBulkUpdateParams中。mPendingLayoutChanges保存了动
画系统要求重新布局DisplayContent时所要做的更改,也就是pendingLayoutChanges,而
mBulkUpdateParams主要收集了要求修改保存在mlnnerFileds的状态的请求。了解布局系统的
工作原理后,读者对这两个概念应该很熟悉了。
在处理完一帧后,上述的两个变量便完成了信息的收集工作。在animateLockd()函数的末
尾处会调用updateAnimToLayoutLocked(),这将它们保存在WindowaAnimator.mAnimToLayout
中并传递给布局系统。布局系统解析这两个变量收集的请求,并检查这些请求是否需要进行
重新布局。如果需要,则调用performLayoutAndPlaceSurfacesLocked()开始布局。
因此,窗口布局与动画帧处理是交替运行的。但是最终一定会以布局系统解析
mBulkUpdateParams与mPendingLayoutChanges时发现无须重新布局为交替运行的终点。 4.5.6动画系统总结
WindowAnimator是一个强大的驱动器,在它的控制下,多种类型的Animator有条不紊
地完成各自的渲染工作。
在WindowAnimator之下有各种类型的Animator,分别掌管不同类型对象的动画。它们
是WindowStateAnimator、AppWindowAnimator、ScreenRotationAnimation、DimAnimator以及
一个名不副实的DisplayContentsAnimator。除DimAnimator与DisplayContentAnimator以外,
其他类型的Animator都有一个stepAnimationLocked0函数用以计算当前时间下动画对象所需
的变换。
虽然受到WindowAnimator的管理,但WindowStateAnimator的重要性却非常重要,因为
它不仅要进行动画变换的计算,还要管理窗口的Surface,并且所有其他类型的Animator的变
换都要汇集到WindowStateAnimator中完成窗口最终变换的计算。在prepareSurfaceLocked0
中,WindowStateAnimator完成所有相关Animator的变换的组合过程,并将变换结果设置到
Surface中。
剩余的AppWindowAnimator、ScreenRotationAnimation 以及DimAnimator等Animator的
工作原理与WindowStateAnimator类似,而且更加简单,读者在经过本节学习后应该可以较快
地完成对它们的研究。 4.6本章小结
漫长的WMS之旅就此告一段落。这一章首先讨论了WMS的窗口管理结构,然后详细
地分析了WMS的布局和动画两个系统的工作原理。这三部分构成了WMS完成所有工作的基
础。类似于屏幕旋转等功能性的内容在本章中并没有探讨,因为这些内容不过是在此基础之
上的应用,完成本章的学习之后,扩展到WMS的其他内容也就不难了,所以这部分便留给
读者研究吧。
另外,在分析的过程中,本章忽略了和壁纸及输入事件相关内容,它们将在第5章以及
第6章进行分析。另外,状态栏的实现在WMS中也有一部分内容值得进一步探讨。因此在
后面的几章中,仍会再回到WMS中来,重拾被本章忽略的有价值的内容。