Android 10 WMS窗口管理浅析

538 阅读8分钟

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的视图结构:

c47d389ea38a452e9d9ebddb8d1373fd.png

一个Activity包含一个窗口,即图中的PhoneWindow。

android用Window这个抽象类来表示窗口,PhoneWindowWindow的唯一实现。


窗口的分类

为了更方便地定义窗口的权限和层级关系,Android系统将窗口划分为三种类型:

c47d389ea38a452e9d9ebddb8d1373fd.png

如上图所示,三种类型分别是 应用窗口、子窗口以及系统窗口。这里需要讲一下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个重要的属性:WindowTokenAppWindowTokenmBaseLayermSubLyer。 我们先来认识一下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,通常会提示以下错误:

image.png

错误中提示 “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对象,然后通过WindowManageraddView方法,将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的另外两个属性吗——mBaseLayermSubLyermBaseLayermSubLayer一起决定了窗口视图的层级。我们称mBaseLayer为主序,mSubLayer为子序。

一般来说,

  • 主序越大,则窗口及其子窗口的显示位置相对于其他窗口的位置越靠前。
  • 子序越大,则子窗口相对于其兄弟窗口的位置越靠前。

framework中对不同窗口定义的主序值如下所示:

kk_image.png

子序值如下:

kk_image1.png

上述的内容只需要简单的了解一下就行了,我们需要重点看一下窗口排序的详细规则。

  • 非应用窗口的排序规则

依照主序进行排序,主序高者排在前面,当现有窗口的主序与新窗口相同时,新窗口位于现有窗口的前面。

  • 应用窗口的排序规则

应用窗口的排序依赖Activity的顺序。

如果当前应用已有窗口在显示,新窗口将插入到其所属应用其他窗口的前面。

如果新窗口是当前应用的第一个窗口,则参照其他应用的窗口顺序,将新窗口插入到位于前面的最后一个应用的最后一个窗口的后方,或者位于后面的第一个应用的最前一个窗口的前方(邻近应用窗口)。 如果当前没有其他应用的窗口可以参照,则直接根据主序将新窗口插入到窗口列表中。

  • 子窗口的排序规则

子窗口的位置计算是相对父窗口的,并根据其子序进行排序。由于父窗口的子序为0,所以子序为负数的窗口会放置在父窗口的后面,而子序为正数的窗口会放置在父窗口的前面。如果新窗口与现有窗口子序相等,则正数子序的新窗口位于现有窗口的前面,负数子序的新窗口位于现有窗口的后面。

当窗口完成排序的工作后,便会被交付到Surface以及SurfaceFlinger去做进一步的显示工作,在此篇文章暂不做探讨。