跟着Innost理解下WindowsManagerService

WMS为所有窗口分配Surface,掌管Surface的显示顺序(Z-order)以及位置尺寸,控制窗口动画,并且还是输入系统的一重要的中转站。

窗口(Window):Android系统中的窗口是屏幕上的一块用于绘制各种UI元素并可以响应应用户输入的一个矩形区域。从原理上来讲,窗口的概念是独自占有一个Surface实例的显示区域。例如Dialog、Activity的界面、壁纸、状态栏以及Toast等都是窗口。

Surface:是一块画布,应用可以随心所欲地通过Canvas或者OpenGL在其上作画。

SurfaceFlinger:将多块Surface的内容按照特定的顺序(Z-order)进行混合并输出到FrameBuffer,从而将Android“漂亮的脸蛋”显示给用户。

1. WMS的工作方式和构成

通过具体实例揭示WMS的客户端如何申请、渲染并注销自己的窗口。同时介绍这个过程中涉及的重要成员。

1.1 客户端对窗口创建、绘制与销毁的过程

抛开Activity、Wallpaper等UI架构以及WMS以外的系统服务。

1.1.1 客户端创建一个窗口的步骤:
· 获取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为其分配的画布。有了画布,窗口之后就可以随时进行绘制工作了。

1.1.2 窗口的绘制过程如下:
· 通过Surface.lock()函数获取可以在其上作画的Canvas实例。
· 使用Canvas实例进行作画。
· 通过Surface.unlockCanvasAndPost()函数提交绘制结果。

1.2 Android显示系统的三个层次

· 第一个层次是UI框架层,其工作为在Surface上绘制UI元素以及响应输入事件。
· 第二个层次为WMS,其主要工作在于管理Surface的分配、层级顺序等。
· 第三层为SurfaceFlinger,负责将多个Surface混合并输出。

1.3 WMS 内部的中要成员

· 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对窗口管理与布局的影响。
跟着Innost理解下WindowsManagerService_第1张图片
下面的几个成员的初始化并没有出现在构造函数中,不过它们的重要性一点也不亚于上面几个。
· mTokenMap,一个HashMap,保存了所有的显示令牌(类型为WindowToken),用于窗口管理。在SampleWindow例子中曾经提到过,一个窗口必须隶属于某一个显示令牌。在那个例子中所添加的令牌就被放进了这个HashMap中。从这个成员中还衍生出几个辅助的显示令牌的子集,例如mAppTokens保存了所有属于Activity的显示令牌(WindowToken的子类AppWindowToken),mExitingTokens则保存了正在退出过程中的显示令牌等。其中mAppTokens列表是有序的,它与AMS中的mHistory列表的顺序保持一致,反映了系统中Activity的顺序。
· mWindowMap,也是一个HashMap,保存了所有窗口的状态信息(类型为WindowState),用于窗口管理。在SampleWindow例子中,使用IWindowSession.add()所添加的窗口的状态将会被保存在mWindowMap中。与mTokenMap一样,mWindowMap一样有衍生出的子集。例如mPendingRemove保存了那些退出动画播放完成并即将被移除的窗口,mLosingFocus则保存了那些失去了输入焦点的窗口。在DisplayContent中,也有一个windows列表,这个列表存储了显示在此DisplayContent中的窗口,并且它是有序的。窗口在这个列表中的位置决定了其最终显示时的Z序。
· mSessions,一个List,元素类型为Session。Session其实是SampleWindow例子中的IWindowSession的Bn端。也就是说,mSessions这个列表保存了当前所有想向WMS寻求窗口管理服务的客户端。注意Session是进程唯一的。
· mRotation,只是一个int型变量。它保存了当前手机的旋转状态。

2. 理解WMS内部流程

WMS的addWindow()展示了三个重要的概念,分别是WindowToken、WindowState以及DisplayContent。

它们之间的关系:除子窗口外,添加任何一个窗口都必须指明其所属的WindowToken;窗口在WMS中通过一个WindowState实例进行管理和保管。同时必须在窗口中指明其所属的DisplayContent,以便确定窗口将被显示到哪一个屏幕上。

2.1 WindowToken

2.1.1 WindowToken的意义

· 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.1.2 WindowToken的声明
使用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。但是为了叙述方便,在没有歧义的前提下不会过分仔细地区分这两个概念。

2.1.2.1 Wallpaper和InputMethod的Token
WallpaperManagerService使用WindowToken对一个特定的Wallpaper做出了如下限制:
· Wallpaper只能创建TYPE_WALLPAPER类型的窗口。
· Wallpaper显示的生命周期由WallpaperManagerService牢牢地控制着。仅有当前的Wallpaper才能创建窗口并显示内容。其他的Wallpaper由于没有有效的Token,而无法创建窗口。
InputMethod的Token的来源与Wallpaper类似,其声明位于InputMethodManagerService的startInputInnerLocked()函数中,取消声明的位置在InputmethodManagerService的unbindCurrentMethodLocked()函数。InputMethodManagerService通过Token限制着每一个InputMethod的窗口类型以及显示生命周期。

2.1.2.2 Activity的Token
Activity的Token的使用方式与Wallpaper和InputMethod类似,但是其包含更多的内容。毕竟,对于Activity,无论是其组成还是操作都比Wallpaper以及InputMethod复杂得多。对此,WMS专为Activity实现了一个WindowToken的子类:AppWindowToken。

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的顺序对窗口的顺序影响非常大。

2.2 WindowState

从WindowManagerService.addWindow()函数的实现中可以看出,当向WMS添加一个窗口时,WMS会为其创建一个WindowState。WindowState表示一个窗口的所有属性,所以它是WMS中事实上的窗口。这些属性将在后面遇到时再做介绍。
类似于WindowToken,WindowState在显示组件一侧也有个对应的类型:IWindow.Stub。IWindow.Stub提供了很多与窗口管理相关通知的回调,例如尺寸变化、焦点变化等。
另外,从WindowManagerService.addWindow()函数中看到新的WindowState被保存到mWindowMap中,键值为IWindow的Bp端。mWindowMap是整个系统所有窗口的一个全集。
说明对比一下mTokenMap和mWindowMap。这两个HashMap维护了WMS中最重要的两类数据:WindowToken及WindowState。它们的键都是IBinder,区别是: mTokenMap的键值可能是IAppWindowToken的Bp端(使用addAppToken()进行声明),或者是其他任意一个Binder的Bp端(使用addWindowToken()进行声明);而mWindowToken的键值一定是IWindow的Bp端。

以一个正在回放视频并弹出两个对话框的Activity为例,WindowToken与WindowState的意义如图。

2.3 DisplayContent

隶属于同一个DisplayContent的窗口将会被显示在同一个屏幕中。每一个DisplayContent都对应这一个唯一的ID,在添加窗口时可以通过指定这个ID决定其将被显示在那个屏幕中。
DisplayContent是一个非常具有隔离性的一个概念。处于不同DisplayContent的两个窗口在布局、显示顺序以及动画处理上不会产生任何耦合。因此,就这几个方面来说,DisplayContent就像一个孤岛,所有这些操作都可以在其内部独立执行。因此,这些本来属于整个WMS全局性的操作,变成了DisplayContent内部的操作了。

3. 窗口的显示次序

手机屏幕是以左上角为原点,向右为X轴方向,向下为Y轴方向的一个二维空间。为了方便管理窗口的显示次序,手机的屏幕被扩展为了一个三维的空间,即多定义了一个Z轴,其方向为垂直于屏幕表面指向屏幕外。多个窗口依照其前后顺序排布在这个虚拟的Z轴上,因此窗口的显示次序又被称为Z序(Z order)。

3.1 主序、子序和窗口类型

窗口的显示次序由两个成员字段描述:主序mBaseLayer和子序mSubLayer。主序用于描述窗口及其子窗口在所有窗口中的显示位置。而子序则描述了一个子窗口在其兄弟窗口中的显示位置。
· 主序越大,则窗口及其子窗口的显示位置相对于其他窗口的位置越靠前。
· 子序越大,则子窗口相对于其兄弟窗口的位置越靠前。对于父窗口而言,其主序取决于其类型,其子序则保持为0。而子窗口的主序与其父窗口一样,子序则取决于其类型。从上述代码可以看到,主序与子序的分配工作是由WindowManagerPolicy的两个成员函数windowTypeToLayerLw()和subWindowTypeToLayerLw()完成的。

3.2 通过主序与子序确定窗口的次序

3.3 更新显示次序到Surface

你可能感兴趣的:(android)