WMS是android系统的核心服务之一,它在android的显示功能中扮演着极为重要的角色。一般来说,WMS具有以下四个重要的功能:
- 窗口管理:负责响应进程的添加、移除窗口、启动窗口的业务,以及管理窗口的坐标、层级、尺寸、令牌等属性。
- 窗口动画:负责处理窗口切换时的动画效果。
- 输入中转:负责派发系统按键、触摸事件给合适的窗口去处理,以及处理部分输入法的交互逻辑。
- Surface管理:为所有window分配合适的surface,并将排序后的surface交给SurfaceFlinger做进一步的显示工作。
这篇文章,我们将针对WMS的窗口管理这一职能,简单地分析一下其中存在的要点。
什么是窗口?
既然是窗口管理,我们首先要弄明白的是,在Android 系统中,什么是窗口?
窗口是一个抽象的概念,我们一般认为,窗口是承载由一个或多个组件及布局组合形成的视图树的容器。在这里要注意三点,一是一个或多个组件及布局,android里的组件和布局有哪些呢,TextView,Button,LinearLayout等等;二是视图树,ViewTree,为什么我们不认为ViewGroup是一个窗口,因为它和它的子View被一起归纳为View Tree;三是容器,Activity和Dialog算不算窗口?从代码的层面上来说不算,因为Activity、Dialog与Window类属于包含与被包含的关系;但从用户和开发者的视角,我们可以将它看作窗口,它们都对外表现出了窗口的层级,坐标,权限等特性,在实际分析问题时基本可以和窗口等同处理。
我们看一下Activity的视图结构:
一个Activity包含一个窗口,即图中的PhoneWindow。
android用Window
这个抽象类来表示窗口,PhoneWindow
是Window
的唯一实现。
窗口的分类
为了更方便地定义窗口的权限和层级关系,Android系统将窗口划分为三种类型:
如上图所示,三种类型分别是 应用窗口、子窗口以及系统窗口。这里需要讲一下Type,它代表了不同的窗口类型。一般而言,Type的值越高,代表窗口越靠近用户,意味着窗口层级的越高。高层级的窗口会覆盖底层级窗口的视图。
WindowState
WindowState在窗口管理中扮演者十分重要的一个角色。当进程向WMS添加一个窗口时,WMS会为其创建一个WindowState。WindowState表示一个窗口的所有属性,可以说它是WMS中事实上的窗口。
可能有读者会问了WindowState
是事实上的窗口,那PhoneWindow
呢?
实际上,PhoneWindow
这个类与WindowState
最大的不同在于,PhoneWindow
是客户端的事实上的窗口,而WindowState
是服务端的。
class WindowState extends WindowContainer<WindowState> implements WindowManagerPolicy.WindowState {
WindowToken mToken;
AppWindowToken mAppToken;
final int mBaseLayer;
final int mSubLayer;
}
WindowState
有4个重要的属性:WindowToken
、AppWindowToken
、mBaseLayer
、mSubLyer
。
我们先来认识一下Token。
WindowToken有以下两点作用:
-
WindowToken将属于同一个应用组件的窗口组织在了一起。所谓的应用组件可以是Activity、InputMethod、Wallpaper以及Dream。在WMS对窗口的管理过程中,用WindowToken指代一个应用组件。例如在进行窗口ZOrder排序时,属于同一个WindowToken的窗口会被安排在一起,而且在其中定义的一些属性将会影响所有属于此WindowToken的窗口。这些都表明了属于同一个WindowToken的窗口之间的紧密联系。一般来说,子窗口和父窗口共用同一个Token。
-
WindowToken具有令牌的作用,是对应用组件的行为进行规范管理的一个手段。WindowToken由应用组件或其管理者负责向WMS声明并持有。应用组件在需要新的窗口时,必须提供WindowToken以表明自己的身份,并且窗口的类型必须与所持有的WindowToken的类型一致。
AppWindowToken是WindowToken的子类,AppWindowToken是保证Activity与Window一致性的桥梁,也是判断Window是否从属Activity的重要方式,通常作为Activity、Dialog类型窗口的Token。而WindowToken一般作为PopUpWindow、Toast类型窗口的Token。
AppWindowToken是作为保持Activity与Window一致性的桥梁,也是判断Window是否从属Activity的重要方式,这句话怎么理解,它是怎么保持一致性的?
Activity与Window的关联
为了更好地理解上面这段话的含义,我们先来看一个案例。
我们在Service中启动一个普通的Dialog,通常会提示以下错误:
错误中提示 “token null is not valid;is your activity running?"
这里的token是什么含义?为什么在service中启动它会为null?
要解答这个问题,我们需要关注一下dialog启动的关键流程。
public void show() {
...
WindowManager mWindowManager =(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mWindowManager.addView(mDecor, l);
...
}
dialog在调用show
方法时,首先会通过context.getSystemService(Context.WINDOW_SERVICE)
这条语句获取一个WindowManager
对象,然后通过WindowManager
的addView
方法,将dialog的PhoneWindow
中的decorView添加到窗口中。
请注意context.getSystemService(Context.WINDOW_SERVICE)
这条语句的context
对象,在Activity创建的dialog和在service创建的dialog,这条语句拿到的context是完全不同的对象。
如果Context为Activity,getSyStemService方法会调用如下代码片段:
[Activity.java]
public Object getSystemService(@ServiceName @NonNull String name) {
...
if (WINDOW_SERVICE.equals(name)) {
return mWindowManager;
}
...
return super.getSystemService(name);
}
如果Context为Service,getSystemService调用的代码片段如下:
[ContextImpl.java]
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
注意到两者的差别了吗?Activity中创建的Dialog,会直接复用Activity的WindowManager对象,而Service中创建的Dialog,则去SystemServiceRegistry类里的缓存池里去获取WindowManager对象。
WindowManager
是一个接口,它的实现类是WindowManagerImpl
,我们来看下WindowManagerImpl
里的addView方法。
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
请注意上面代码中的mParentWindow这个对象。Activity中的Dialog中使用的WindowManager,就是Activity的WindowManager,因此这里的mParentWindow就是Activity中的PhoneWindow。如果在Service中,这个mParentWindow对象的值为null。
代码的流程走到了WindowManagerGlobal
这个类中,我们继续往下看:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
...
}
Service中创建的Dialog,parentWindow的值为null,它不会执行 parentWindow.adjustLayoutParamsForSubWindow(wparams);
这条语句。
这条语句中又包含了什么关键的操作呢?
[Window.java]
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
...
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
...
}
里面最关键的内容如上所示,它将Activity的PhoneWindow的appToken,绑定到了Dialog的LayoutParams里的token属性上。
接下来,Dilaog需要经历ViewRootImpl.java
的setView方法,以及Session.java
的addToDisplay方法,最终调用到WMS的addWindow方法,真正地去向WMS中新增一个窗口。
这两步中间过程没有什么特别关键的内容,在此不做展开分析,直接从WMS的addWindow看起。
public int addWindow(Session session, IWindow client, int seq,
LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState) {
...
final boolean hasParent = parentWindow != null;
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
final int rootType = hasParent ? parentWindow.mAttrs.type : type;
boolean addToastWindowRequiresToken = false;
if (token == null) {
if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
Slog.w(TAG_WM, "Attempted to add application window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
...
}
final boolean hasParent = parentWindow != null;
这段代码的返回值为false。读者可能会有疑问了,之前不是说Dilaog在Activity中parentWindow不为null吗,怎么和想象中的不一样。
原因在于,这个parentWindow和之前提到的mParentWindow不是一个概念,parentWindow在WMS中所属的类是WindowState
,表示服务端的实际窗口。
因此, WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);
会根据attrs.token
去获取一个WindowToken。
attrs.token
是什么?它正是Dialog的LayoutParams里的token属性,在activity中,它实际上是Activity的PhoneWindow的appToken,在service中,它的值为null。
因此,在service中创建的dialog获得的token为null,它会走到以下代码片段:
if (token == null) {
if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
Slog.w(TAG_WM, "Attempted to add application window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
}
addWindow返回了WindowManagerGlobal.ADD_BAD_APP_TOKEN
,最终报出了我们在一开始提到的错误提示。
总结一下:
-
Activity与Window保持一致性的关键在于Context对象,Window通过Context获取到Activity的PhoneWindow的appToken,利用appToken取得和Activity相同的AppWindowToken,来维持Window和Activity的关联,所以说 AppWindowToken是保持Activity与Window一致性的桥梁。
-
当Window的AppWindowToken为null时,它必然与Activity没有直接的从属关系,此时WMS会为Window创建一个新的WindowToken变量,作为窗口的身份令牌。
窗口的顺序
还记得前文提到的WindowState的另外两个属性吗——mBaseLayer
、mSubLyer
。mBaseLayer
和mSubLayer
一起决定了窗口视图的层级。我们称mBaseLayer
为主序,mSubLayer
为子序。
一般来说,
- 主序越大,则窗口及其子窗口的显示位置相对于其他窗口的位置越靠前。
- 子序越大,则子窗口相对于其兄弟窗口的位置越靠前。
framework中对不同窗口定义的主序值如下所示:
子序值如下:
上述的内容只需要简单的了解一下就行了,我们需要重点看一下窗口排序的详细规则。
- 非应用窗口的排序规则
依照主序进行排序,主序高者排在前面,当现有窗口的主序与新窗口相同时,新窗口位于现有窗口的前面。
- 应用窗口的排序规则
应用窗口的排序依赖Activity的顺序。
如果当前应用已有窗口在显示,新窗口将插入到其所属应用其他窗口的前面。
如果新窗口是当前应用的第一个窗口,则参照其他应用的窗口顺序,将新窗口插入到位于前面的最后一个应用的最后一个窗口的后方,或者位于后面的第一个应用的最前一个窗口的前方(邻近应用窗口)。 如果当前没有其他应用的窗口可以参照,则直接根据主序将新窗口插入到窗口列表中。
- 子窗口的排序规则
子窗口的位置计算是相对父窗口的,并根据其子序进行排序。由于父窗口的子序为0,所以子序为负数的窗口会放置在父窗口的后面,而子序为正数的窗口会放置在父窗口的前面。如果新窗口与现有窗口子序相等,则正数子序的新窗口位于现有窗口的前面,负数子序的新窗口位于现有窗口的后面。
当窗口完成排序的工作后,便会被交付到Surface
以及SurfaceFlinger
去做进一步的显示工作,在此篇文章暂不做探讨。