浅析Android的窗口

7363只鸡 7年前
   <h2>一、窗口的概念</h2>    <p>在开发过程中,我们经常会遇到,各种跟窗口相关的类,或者方法。但是,在 Android 的框架设计中,到底什么是窗口?窗口跟 Android Framework 中的 Window 类又是什么关系?以手Q 的主界面为例,如下图所示,上面的状态栏是一个窗口,手Q 的主界面自然是一个窗口,而弹出的 PopupWindow 也是一个窗口,我们经常使用的 Toast 也是一个窗口。像 Dialog,ContextMenu,以及 OptionMenu 等等这些都是窗口。这些窗口跟 Window 类的关系是什么,或者窗口跟 Window 类描述的是同一个概念吗?</p>    <p><img src="https://simg.open-open.com/show/5f033c21d99252289fc57e6fb665ac7d.png"></p>    <p>其实窗口的概念,从不同的角度来看,其含义是不一样的。我们知道,WindowManagerService(后面简称 WmS)管理所有的窗口。但是对于WmS来讲,一个窗口其实就是一个 View 类,而不是 Window 类。WmS 负责管理这些 View 的 Z-order,显示区域,以及把消息派发到对应的 View 中。View 本身并不能直接从 WmS 中接收消息,而是通过实现了 IWindow 接口的 ViewRootImpl.W 类来实现,以下是这些类的关系:</p>    <p><img src="https://simg.open-open.com/show/80d3128dd03ea9d7fefda64eab159730.png"></p>    <p>所以这里窗口分为两层概念:</p>    <p>(1)WmS 眼中的,窗口是可以显示用来显示的 View。对于 WmS 而言,所谓的窗口就是一个通过 WindowManagerGlobal.addView()添加的 View 罢了;</p>    <p>(2)Window 类是一个针对窗口交互的抽象,也就是对于 WmS 来讲所有的用户消息是直接交给 View/ViewGroup 来处理的。而 Window 类把一些交互从 View/ViewGroup 中抽离出来,定义了一些窗口的行为,例如菜单,以及处理系统按钮,如“Home”,“Back”等等。由此可见,Window 描述的窗口只是在通用窗口的基础上,再抽象了一层,把符合某种规范的窗口统一了一下。Window 所描述的窗口,应该是通用窗口的一个子集。例如 PopupWindow 是一个窗口,但是分析其源码可以知道,该类并没有创建任何 Window 对象。而 Dialog 则是通过 PolicyManager.makeNewWindow(mContext) 创建了一个 Window 对象来管理窗口。当一个 Dialog 显示时,我们可以通过按 back 把它 dismiss 了,但是 PopupWindow 则不行,需要自己去处理。</p>    <h2>二、窗口类型</h2>    <p>添加一个窗口是通过 WindowManagerGlobal.addView()来完成的,分析 addView 方法的参数,有三个参数是必不可少的,view,params,以及 display。而 display 一般直接取 WindowMnagerImpl 中的 mDisplay,表示要输出的显示设备。view 自然表示要显示的 View,而 params 是 WindowManager.LayoutParams,用来描述这个 view 的些窗口属性,其中一个重要的参数 type,用来描述窗口的类型。</p>    <pre>  <code class="language-java">public void addView(View view, ViewGroup.LayoutParams params,          Display display, Window parentWindow) {      if (view == null) {          throw new IllegalArgumentException("view must not be null");      }      if (display == null) {          throw new IllegalArgumentException("display must not be null");      }      if (!(params instanceof WindowManager.LayoutParams)) {          throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");      }       .....    }</code></pre>    <p>分析 WindowManager 对于 type 可赋值类型的描述可知,Framework 中定义了三种类型的窗口:</p>    <p>(1)应用窗口 Activity 对应的窗口就是应用窗口, 所有 Activity 默认的窗口类型是 TYPE _BASE _APPLICATION。WindowManager 的 LayoutParams 的默认构建方法的实现,可以看到默认类型是 TYPE _ APPLICATION。 Dialog 的窗口类型是 TYPE _ APPLICATION,而很多 Dialog 的子类,修改了窗口类似,如 ContextMenu,本质是用 Dialog 来实现的,但是在添加窗口前,修改了 type 类型,赋值为 TYPE _ APPLICATION _ ATTACHED _ DIALOG。从这个我们可以看到,WmS 并没有把应用窗口与子窗口区分得那么清楚。</p>    <pre>  <code class="language-java">public LayoutParams() {      super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);      type = TYPE_APPLICATION;      format = PixelFormat.OPAQUE;  }</code></pre>    <p><img src="https://simg.open-open.com/show/418d69ef0f09da1055f810e78f07d3d6.png"></p>    <p>(2)子窗口子窗口是指该窗口必须要有一个父窗口,父窗口可以是一个应用类型窗口,也可以是任何其他类型的窗口。例如前面手Q 界面中,点击右上角的按钮显示一个 PopupWindow,它就是一个子窗口,其类型一般 TYPE _ APPLICATION _ PANEL。既然称为子窗口,其与父窗口的关系是比较容易理解的。B 是 A 的子窗口,当 A 不可见时,B 也会不可见的。如果A不可见时添加B,B 也是不可见的,直到 A 可见为止,B 跟随一起可见。</p>    <p><img src="https://simg.open-open.com/show/36e7208e5ac70d2d5135d8a1296483ed.png"></p>    <p>(3)系统窗口 系统窗口跟应用窗口不同,不需要对应 Activity。跟子窗口不同,不需要有父窗口。一般来讲,系统窗口应该由系统来创建的,例如发生异常,ANR时的提示框,又如系统状态栏,屏保等。但是,Framework 还是定义了一些,可以被应用所创建的系统窗口,如 TYPE_ TOAST,TYPE _INPUT _ METHOD,TYPE _WALLPAPTER 等等。</p>    <p><img src="https://simg.open-open.com/show/d45e7c7672421c076a3d758ab5b67d8a.png"></p>    <p>token 的含义</p>    <p>相信大家对于 token 这个词并不陌生,在开发过程中经常遇到,例如 Bad Token 的异常。到底在 Android 框架中,token 代表什么?分析源码,我们发现,大多数 token 的对象,都表示一个 IBinder 对象。提到 IBinder,大家一点也不陌生,就是 Android 的 IPC 通信机制。在创建窗口过程中,涉及到的 IPC 通信,无非包含两方面,一个是 WmS 用来跟应用所在的进程进行通信的 ViewRootImpl.W 类的对象,另一个是指向一个 ActivityRecord 的对象,自然应该是WmS用来跟 AmS 进行通信的了。我们梳理了一下,token 以下几处的定义,分别来讲讲这里的 token 代表什么。</p>    <p><img src="https://simg.open-open.com/show/ff19880fbe312ffab0e78c9cb688e316.png"></p>    <p>分析一下 View 的 AttachInfo 的赋值。ViewRootImpl 在构建方法里,会初始化一个 AttachInfo 实例,把它的 Session,以及 W类对象赋值给 AttachInfo。分析可以看到,AttachInfo 中的 mWindowToken,与mWindow 都是指向 ViewRootImpl 中的 mWindow(W类实例)。当一个 View attach 到窗口后,ViewRootImpl会执行performTraversals,如果发现是首次调用会,会把自己的 mAttachInfo 传递给根 View(通过dispatchAttachedToWindow),告诉 View 树现在已经 attch to Window 了,马上可以显示了。根 View(一般是 ViewGroup)会把这个信息,遍历地传递给 View 树中的每一个子 View,这样每个 View 的 mAttachInfo 都被赋值为 ViewRootImp 的 mAttachInfo了。</p>    <pre>  <code class="language-java">//分析一下 View 中的 AttachInfo 的赋值,以及 ViewRootImpl 中的 mAttachInfo      public ViewRootImpl(Context context, Display display) {          ...          mWindow = new W(this);          ...           mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);          ...      }      //看到mWindowToken其实就是IWindow实例      AttachInfo(IWindowSession session, IWindow window, Display display,                   ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer) {             mSession = session;             mWindow = window;             mWindowToken = window.asBinder();             mDisplay = display;             mViewRootImpl = viewRootImpl;             mHandler = handler;             mRootCallbacks = effectPlayer;      }      // ViewRootImpl在第一次执行performTraversals时,会把自己的mAttachInfo传递给根View,然后由根View逐级传递下去      private void performTraversals() {         ...          if (mFirst) {              ...              host.dispatchAttachedToWindow(mAttachInfo, 0);              ...          }else{              ...          }      }      //ViewGroup.java      void dispatchAttachedToWindow(AttachInfo info, int visibility) {         mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;         super.dispatchAttachedToWindow(info, visibility);         mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;         final int count = mChildrenCount;         final View[] children = mChildren;         for (int i = 0; i < count; i++) {             final View child = children[i];             child.dispatchAttachedToWindow(info,                     visibility | (child.mViewFlags & VISIBILITY_MASK));        }      }      //View.java      void dispatchAttachedToWindow(AttachInfo info, int visibility) {         mAttachInfo = info;         ...       }</code></pre>    <p>WindowManager.LayoutParams 中的 type 与 token</p>    <p>WindowManager.LayoutParams 用来描述一个窗口的特性,最终在添加窗口时,会传递给 WmS。而且 WmS 会保存在 WindowState 的 mAttrs 中。LayoutParams 有很多参数,但是跟窗口创建相关的参数,最重要的就是 type 与 token 了,这里我们可以通过分析 WmS 的 addWindow 代码的可以知道:</p>    <pre>  <code class="language-java">[i]      //WindowManagerService.java addWindow       ...      //权限检查,需要用到type类型,会检查窗口类型是否合法,如果是系统窗口类型      //还需要进行权限检查,详见PhoneWindowManager.java      int res = mPolicy.checkAddPermission(attrs, appOp);      ...      //如果是子窗口类型,还会检查其父窗口是否存在,如果父窗口不存在,直接抛出异常      if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {                  attachedWindow = windowForClientLocked(null, attrs.token, false);                  if (attachedWindow == null) {                       Slog.w(TAG, "Attempted to add window with token that is not a window: " + attrs.token + ".  Aborting.");                       return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;                   }                   if (attachedWindow.mAttrs.type >= FIRST_SUB_WINDOW                         && attachedWindow.mAttrs.type <= LAST_SUB_WINDOW) {                      Slog.w(TAG, "Attempted to add window with token that is a sub-window: " + attrs.token + ".  Aborting.");                      return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;                   }        }        ...       //如果是类型是TYPE_PRIVATE_PRESENTATION ,还会检查相应的显示设备       if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {              Slog.w(TAG, "Attempted to add private presentation window to a non-private display.  Aborting.");              return WindowManagerGlobal.ADD_PERMISSION_DENIED;          }       ...          //根据LayoutParams中的token,会检索WmS中保存的WindowToken,      //由引可见,不同的窗口类型,其对应的token是有区别的。WmS要根据窗口类型来检查其传递过来的token是否合法。      boolean addToken = false;      WindowToken token = mTokenMap.get(attrs.token);      if (token == null) {         if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {                  Slog.w(TAG, "Attempted to add application window with unknown token " + attrs.token + ".  Aborting.");                  return WindowManagerGlobal.ADD_BAD_APP_TOKEN;              }      if (type == TYPE_INPUT_METHOD) {                  Slog.w(TAG, "Attempted to add input method window with unknown token " + attrs.token + ".  Aborting.");                  return WindowManagerGlobal.ADD_BAD_APP_TOKEN;              }      if (type == TYPE_VOICE_INTERACTION) {                  Slog.w(TAG, "Attempted to add voice interaction window with unknown token "+ attrs.token + ".  Aborting.");                  return WindowManagerGlobal.ADD_BAD_APP_TOKEN;              }      if (type == TYPE_WALLPAPER) {                  Slog.w(TAG, "Attempted to add wallpaper window with unknown token " + attrs.token + ".  Aborting.");                  return WindowManagerGlobal.ADD_BAD_APP_TOKEN;              }      if (type == TYPE_DREAM) {                  Slog.w(TAG, "Attempted to add Dream window with unknown token " + attrs.token + ".  Aborting.");                  return WindowManagerGlobal.ADD_BAD_APP_TOKEN;              }      if (type == TYPE_ACCESSIBILITY_OVERLAY) {                  Slog.w(TAG, "Attempted to add Accessibility overlay window with unknown token "  + attrs.token + ".  Aborting.");                  return WindowManagerGlobal.ADD_BAD_APP_TOKEN;              }              token = new WindowToken(this, attrs.token, -1, false);              addToken = true;      } else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {              AppWindowToken atoken = token.appWindowToken;              if (atoken == null) {                  Slog.w(TAG, "Attempted to add window with non-application token " + token + ".  Aborting.");                  return WindowManagerGlobal.ADD_NOT_APP_TOKEN;              } else if (atoken.removed) {                  Slog.w(TAG, "Attempted to add window with exiting application token "  + token + ".  Aborting.");                  return WindowManagerGlobal.ADD_APP_EXITING;              }              if (type == TYPE_APPLICATION_STARTING && atoken.firstWindowDrawn) {                  // No need for this guy!                  if (localLOGV) Slog.v(                          TAG, "**** NO NEED TO START: " + attrs.getTitle());                  return WindowManagerGlobal.ADD_STARTING_NOT_NEEDED;              }          } else if (type == TYPE_INPUT_METHOD) {              if (token.windowType != TYPE_INPUT_METHOD) {                  Slog.w(TAG, "Attempted to add input method window with bad token "  + attrs.token + ".  Aborting.");                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;              }          } else if (type == TYPE_VOICE_INTERACTION) {              if (token.windowType != TYPE_VOICE_INTERACTION) {                  Slog.w(TAG, "Attempted to add voice interaction window with bad token "  + attrs.token + ".  Aborting.");                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;              }          } else if (type == TYPE_WALLPAPER) {              if (token.windowType != TYPE_WALLPAPER) {                  Slog.w(TAG, "Attempted to add wallpaper window with bad token "   + attrs.token + ".  Aborting.");                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;              }          } else if (type == TYPE_DREAM) {              if (token.windowType != TYPE_DREAM) {                  Slog.w(TAG, "Attempted to add Dream window with bad token " + attrs.token + ".  Aborting.");                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;              }          } else if (type == TYPE_ACCESSIBILITY_OVERLAY) {              if (token.windowType != TYPE_ACCESSIBILITY_OVERLAY) {                  Slog.w(TAG, "Attempted to add Accessibility overlay window with bad token " + attrs.token + ".  Aborting.");                  return WindowManagerGlobal.ADD_BAD_APP_TOKEN;              }          } else if (token.appWindowToken != null) {              Slog.w(TAG, "Non-null appWindowToken for system window of type=" + type);              // It is not valid to use an app token with other system types; we will              // instead make a new token for it (as if null had been passed in for the token).              attrs.token = null;              token = new WindowToken(this, null, -1, false);              addToken = true;          }           ...      //经过一系列的检查之后,最后会生成窗口在WmS中的表示WindowState,并且把LayoutParams赋值给WindowState的mAttrs      win = new WindowState(this, session, client, token, attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);        ...        if (addToken) {       mTokenMap.put(attrs.token, token);      }       ...       mWindowMap.put(client.asBinder(), win);     [/i]</code></pre>    <p>总结一下:</p>    <p>(1)窗口类型必须是指定合法范围内的,即应用窗口,子窗口,系统窗口中的一种,否则检查会失败; (2)如果是系统,需要进行权限检查 以下类型不需要特别声明权限 TYPE _ TOAST,TYPE _ DREAM,TYPE _ INPUT _ METHOD,TYPE _ WALLPAPER,TYPE _ PRIVATE _ PRESENTATION,TYPE _ VOICE _ INTERACTION,TYPE _ ACCESSIBILITY _ OVERLAY 以下类型需要声明使用权限:android.permission.SYSTEM _ ALERT _ WINDOW TYPE _ PHONE,TYPE _ PRIORITY _ PHONE,TYPE _ SYSTEM _ ALERT,TYPE _ SYSTEM _ ERROR,TYPE _ SYSTEM _ OVERLAY 其他的系统窗口,需要声明权限:android.permission.INTERNAL _ SYSTEM _ WINDOW (3)如果是应用窗口,通过 token 检索出来的 WindowToken,一定不能为空,而且还必须是 Activity 的 mAppToken,同时对应的 Activity 还必须是没有被 finish。之前分析 Activity 的启动过程我们知道,Activity 在启动过程中,会先通过 WmS 的 addAppToken( )添加一个 AppWindowToken 到 mTokenMap 中,其中 key 就用了 IApplicationToken token。而 Activity 中的 mToken,以及 Activity 对应的 PhoneWindow 中的 mAppToken 就是来自 AmS 的 token (代码见 Activity 的 attach 方法)。 (4)如果是子窗口,会通过 attrs.token 去通过 windowForClientLocked 查找其父窗口,如果找不到其父窗口,会抛出异常。或者如果找到的父窗口的类型还是子窗口类型,也会抛出异常。这里查找父窗口的过程,是直接取了 attrs.token 去 mWindowMap 中找对应的 WindowState,而 mWindowMap 中的 key 是 IWindow。所以,由此可见,创建一个子窗口类型,token 必须赋值为其父窗口的 ViewRootImpl 中的 W 类对象 mWindow。 (5)如果是如下系统窗口,TYPE _ INPUT _ METHOD,TYPE _ VOICE _ INTERACTION,TYPE _ WALLPAPER,TYPE _ DREAM,TYPE _ ACCESSIBILITY _ OVERLAY,token 不能为空,而且通过 token 检索到的 WindowToken 的类型不能是其本身对应的类型。 (6)如果是其他系统窗口,会直接把 attrs 中的 token 给清除了,不需要 token。因此其他类型的系统窗口,LayoutParams 中 token 是可以为空的。 (7)检查通过后,如果需要创建新的 WindowToken,会以 attrs.token 为 key,add 到 mTokenMap 中。 (8)WindowState 创建后,会以 IWindow 为 key (对应应用进程中的 ViewRootImpl.W 类对象 mWindow,重要的事强调多遍!!),添加到 mWindowMap 中。</p>    <p>由此可见,我们要成功添加一个窗口,对于 type 与 token 的赋值是有要求的,否则先不说能否正确显示,直接就创建失败了。那 type 与 token 是如何赋值的呢?最直接来讲,就是在调用 WindowManagerImpl 的 addView 方法前,把值赋好就可以了。但是,分析 FrameWork 所提供的一些窗口的显示,如 Dialog 等,并没有看到在调用 addView 之前,对 token 赋值呢。其窗口类型是应用窗口,根据前面所描述的检查,token 肯定不能为 null 的,而且还必须是 Activity 的 mAppToken,否则创建失败的。这里我们分析一下,在 token 没有赋值的情况下,调用 addView 会做哪些处理。代码就要回到 WindowManagerImpl 开始了。</p>    <pre>  <code class="language-java">[i]      //WindowManagerImpl.java addView      //applyDefaultToken会检查,有没有设置默认token,如果有设置,而且没有设置父窗口的情况下,token又是null的话,直接把默认token赋值给token吧。      @Override      public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {         applyDefaultToken(params);         mGlobal.addView(view, params, mDisplay, mParentWindow);      }      //WindowManagerGlobal.java addView      //如果有设置父窗口,会通过adjustLayoutParamsForSubWindow来调整params。      final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;      if (parentWindow != null) {          parentWindow.adjustLayoutParamsForSubWindow(wparams);      }      //Window.java adjustLayoutParamsForSubWindow      if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&          wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {          //如果是子窗口类型,而且token为null,直接取父窗口的AttachInfo中的mWindowToken,其实就是父窗口对应的ViewRootImpl中的W类对象mWindow。          if (wp.token == null) {              View decor = peekDecorView();              if (decor != null) {                  wp.token = decor.getWindowToken();              }          }          ...      }else {      //如果token为null,直接取父窗口的mAppToken吧。      if (wp.token == null) {              wp.token = mContainer == null ? mAppToken :           mContainer.mAppToken;        }      }  [/i]</code></pre>    <p> </p>    <p>来自:http://dev.qq.com/topic/5923ef85bdc9739041a4a798</p>    <p> </p>