Window十二问(快扶我起来,我还能问)

2021-01-26

前言

关于Window,你了解多少呢?看看下面这些问题你都能答上来吗。

如果你遇到这些问题

  • Window是什么?和View的关系?
  • WindowManager是什么?和WMS的关系?
  • 怎么添加一个Window?
  • Window怎样可以显示到锁屏界面
  • Window三种类型都存在的情况下,显示层级是怎样。
  • Window就是指PhoneWindow吗?
  • PhoneWindow什么时候被创建的?
  • 要实现可以拖动的View该怎么做?
  • Window的添加、删除和更新过程。
  • Activity、PhoneWindow、DecorView、ViewRootImpl 的关系?
  • Window中的token是什么,有什么用?
  • Application中可以直接弹出Dialog吗?
  • 关于事件分发,事件到底是先到DecorView还是先到Window的?

Window是什么

窗口。你可以理解为手机上的整个画面,所有的视图都是通过Window呈现的,比如Activity、dialog都是附加在Window上的。Window类的唯一实现是PhoneWindow,这个名字就更加好记了吧,手机窗口呗。

那Window到底在哪里呢?我们看到的View是Window吗?是也不是。

  • 如果说的只是Window概念的话,那可以说是的,View就是Window的存在形式,Window管理着View。
  • 如果说是Window类的话,那确实不是View,唯一实现类PhoneWindow管理着当前界面上的View,包括根布局——DecorView,和其他子view的添加删除等等。

不知道你晕没有,我总结下,Window是个概念性的东西,你看不到他,如果你能感知它的存在,那么就是通过View,所以View是Window的存在形式,有了View,你才感知到View外层有一个皇帝的新衣——window。

WindowManager是什么?和WMS的关系?

WindowManager就是用来管理Window的,实现类为WindowManagerImpl,实际工作会委托给WindowManagerGlobal类中完成。

而具体的Window操作,WM会通过Binder告诉WMS,WMS做最后的真正操作Window的工作,会为这个Window分配Surface,并绘制到屏幕上。

怎么添加一个Window?

  1. var windowParams: WindowManager.LayoutParams = WindowManager.LayoutParams() 
  2.     windowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 
  3.     windowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG 
  4.     var btn = Button(this) 
  5.     windowManager.addView(btn, windowParams) 

简单贴了下代码,加了一个Button。

有的朋友可能会疑惑了,这明明是个Button,是个View啊,咋成了Window?

刚才说过了,View是Window的表现形式,在实际实现中,添加window其实就是添加了一个你看不到的window,并且里面有View才能让你感觉得到这个是一个Window。

所以通过windowManager添加的View其实就是添加Window的过程。

这其中还有两个比较重要的属性:flags和type,下面会依次说到。

Window怎样可以显示到锁屏界面

Window的flag可以控制Window的显示特性,也就是该怎么显示、touch事件处理、与设备的关系、等等。所以这里问的锁屏界面显示也是其中的一种Flag。

  1. // Window不需要获取焦点,也不接受各种输入事件。 
  2. public static final int FLAG_NOT_FOCUSABLE = 0x00000008; 
  3.  
  4. // @deprecated Use {@link android.R.attr#showWhenLocked} or 
  5. // {@link android.app.Activity#setShowWhenLocked(boolean)} instead to prevent an 
  6. // unintentional double life-cycle event. 
  7.  
  8.  
  9. // 窗口可以在锁屏的 Window 之上显示 
  10. public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000; 

Window三种类型都存在的情况下,显示层级是怎样。

Type表示Window的类型,一共三种:

  • 应用Window。对应着一个Activity,Window层级为1~99,在视图最下层。
  • 子Window。不能单独存在,需要附属在特定的父Window之中(如Dialog就是子Window),Window层级为1000~1999。
  • 系统Window。需要声明权限才能创建的Window,比如Toast和系统状态栏,Window层级为2000-2999,处在视图最上层。

可以看到,区别就是有个Window层级(z-ordered),层级高的能覆盖住层级低的,离用户更近。

Window就是指PhoneWindow吗?

如果有人问我这个问题,我肯定心里要大大的疑惑了??。

可不就是PhoneWindow吗?都唯一实现类了,净问些奇怪问题。

但是面试的时候遇到这种问题总要答啊?这时候就要扯出Window的概念了。

  • 如果指的Window类,那么PhoneWindow作为唯一实现类,一般指的就是PhoneWindow。
  • 如果指的Window这个概念,那肯定不是指PhoneWindow,而是存在于界面上真实的View。当然也不是所有的View都是Window,而是通过WindowManager添加到屏幕的view才是Window,所以PopupWindow是Window,上述问题中添加的单个View也是Window。

PhoneWindow什么时候被创建的?

熟悉Activity启动流程的朋友应该知道,启动过程会执行到ActivityThread的handleLaunchActivity方法,这里初始化了WindowManagerGlobal,也就是WindowManager实际操作Window的类,待会会看到:

  1. public Activity handleLaunchActivity(ActivityClientRecord r, 
  2.                                          PendingTransactionActions pendingActions, Intent customIntent) { 
  3.         //... 
  4.         WindowManagerGlobal.initialize(); 
  5.         //... 
  6.         final Activity a = performLaunchActivity(r, customIntent); 
  7.         //... 
  8.         return a; 
  9.     } 

然后会执行到performLaunchActivity中创建Activity,并调用attach方法进行一些数据的初始化(伪代码):

  1. final void attach() { 
  2.      //初始化PhoneWindow 
  3.      mWindow = new PhoneWindow(this, window, activityConfigCallback); 
  4.      mWindow.setWindowControllerCallback(mWindowControllerCallback); 
  5.      mWindow.setCallback(this); 
  6.  
  7.      //和WindowManager关联 
  8.      mWindow.setWindowManager( 
  9.              (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), 
  10.              mToken, mComponent.flattenToString(), 
  11.              (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0); 
  12.  
  13.      mWindowManager = mWindow.getWindowManager(); 

可以看到,在Activity的attach方法中,创建了PhoneWindow,并且设置了callback,windowManager。

这里的callback待会会说到,跟事件分发有关系,可以说是当前Activity和PhoneWindow建立联系。

要实现可以拖动的View该怎么做?

还是接着刚才的btn例子,如果要修改btn的位置,使用updateViewLayout即可,然后在ontouch方法中传入移动的坐标即可。

  1. btn.setOnTouchListener { v, event -> 
  2.             val index = event.findPointerIndex(0) 
  3.             when (event.action) { 
  4.                 ACTION_MOVE -> { 
  5.                     windowParams.x = event.getRawX(index).toInt() 
  6.                     windowParams.y = event.getRawY(index).toInt() 
  7.                     windowManager.updateViewLayout(btn, windowParams) 
  8.                 } 
  9.                 else -> { 
  10.                 } 
  11.             } 
  12.             false 
  13.  
  14.         } 

Window的添加、删除和更新过程。

Window的操作都是通过WindowManager来完成的,而WindowManager是一个接口,他的实现类是WindowManagerImpl,并且全部交给WindowManagerGlobal来处理。下面具体说下addView,updateViewLayout,和removeView。

1)addView

  1. //WindowManagerGlobal.java 
  2. public void addView(View view, ViewGroup.LayoutParams params, 
  3.             Display display, Window parentWindow) { 
  4.  
  5.         if (parentWindow != null) { 
  6.             parentWindow.adjustLayoutParamsForSubWindow(wparams); 
  7.         } 
  8.              
  9.             ViewRootImpl root; 
  10.             View panelParentView = null
  11.  
  12.             root = new ViewRootImpl(view.getContext(), display); 
  13.             view.setLayoutParams(wparams); 
  14.  
  15.             mViews.add(view); 
  16.             mRoots.add(root); 
  17.             mParams.add(wparams); 
  18.  
  19.             try { 
  20.                 root.setView(view, wparams, panelParentView); 
  21.             }  
  22.         } 
  23.     } 
  • 这里可以看到,创建了一个ViewRootImpl实例,这样就说明了每个Window都对应着一个ViewRootImpl。
  • 然后通过add方法修改了WindowManagerGlobal中的一些参数,比如mViews—存储了所有Window所对应的View,mRoots——所有Window所对应的ViewRootImpl,mParams—所有Window对应的布局参数。
  • 最后调用了ViewRootImpl的setView方法,继续看看。
  1. final IWindowSession mWindowSession; 
  2.  
  3. mWindowSession = WindowManagerGlobal.getWindowSession(); 
  4.  
  5. public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { 
  6.     // 
  7.     requestLayout(); 
  8.  
  9.     res = mWindowSession.addToDisplay(mWindow,); 

setView方法主要完成了两件事,一是通过requestLayout方法完成异步刷新界面的请求,进行完整的view绘制流程。其次,会通过IWindowSession进行一次IPC调用,交给到WMS来实现Window的添加。

其中mWindowSession是一个Binder对象,相当于在客户端的代理类,对应的服务端的实现为Session,而Session就是运行在SystemServer进程中,具体就是处于WMS服务中,最终就会调用到这个Session的addToDisplay方法,从方法名就可以猜到这个方法就是具体添加Window到屏幕的逻辑,具体就不分析了,下次说到屏幕绘制的时候再细谈。

2)updateViewLayout

  1. public void updateViewLayout(View view, ViewGroup.LayoutParams params) { 
  2. /... 
  3.        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; 
  4.  
  5.        view.setLayoutParams(wparams); 
  6.  
  7.        synchronized (mLock) { 
  8.            int index = findViewLocked(viewtrue); 
  9.            ViewRootImpl root = mRoots.get(index); 
  10.            mParams.remove(index); 
  11.            mParams.add(index, wparams); 
  12.            root.setLayoutParams(wparams, false); 
  13.        } 
  14.    } 

这里更新了WindowManager.LayoutParams和ViewRootImpl.LayoutParams,然后在ViewRootImpl内部同样会重新对View进行绘制,最后通过IPC通信,调用到WMS的relayoutWindow完成更新。

3)removeView

  1. public void removeView(View view, boolean immediate) { 
  2.         if (view == null) { 
  3.             throw new IllegalArgumentException("view must not be null"); 
  4.         } 
  5.  
  6.         synchronized (mLock) { 
  7.             int index = findViewLocked(viewtrue); 
  8.             View curView = mRoots.get(index).getView(); 
  9.             removeViewLocked(index, immediate); 
  10.             if (curView == view) { 
  11.                 return
  12.             } 
  13.  
  14.             throw new IllegalStateException("Calling with view " + view 
  15.                     + " but the ViewAncestor is attached to " + curView); 
  16.         } 
  17.     } 
  18.      
  19.      
  20.     private void removeViewLocked(int index, boolean immediate) { 
  21.         ViewRootImpl root = mRoots.get(index); 
  22.         View view = root.getView(); 
  23.  
  24.         if (view != null) { 
  25.             InputMethodManager imm = view.getContext().getSystemService(InputMethodManager.class); 
  26.             if (imm != null) { 
  27.                 imm.windowDismissed(mViews.get(index).getWindowToken()); 
  28.             } 
  29.         } 
  30.         boolean deferred = root.die(immediate); 
  31.         if (view != null) { 
  32.             view.assignParent(null); 
  33.             if (deferred) { 
  34.                 mDyingViews.add(view); 
  35.             } 
  36.         } 
  37.     }     

该方法中,通过view找到mRoots中的对应索引,然后同样走到ViewRootImpl中进行View删除工作,通过die方法,最终走到dispatchDetachedFromWindow()方法中,主要做了以下几件事:

  • 回调onDetachedFromeWindow。
  • 垃圾回收相关操作;
  • 通过Session的remove()在WMS中删除Window;
  • 通过Choreographer移除监听器

Activity、PhoneWindow、DecorView、ViewRootImpl 的关系?

看完上面的流程,我们再来理理这四个小伙伴之间的关系:

  • PhoneWindow 其实是 Window 的唯一子类,是 Activity 和 View 交互系统的中间层,用来管理View的,并且在Window创建(添加)的时候就新建了ViewRootImpl实例。
  • DecorView 是整个 View 层级的最顶层,ViewRootImpl是DecorView 的parent,但是他并不是一个真正的 View,只是继承了ViewParent接口,用来掌管View的各种事件,包括requestLayout、invalidate、dispatchInputEvent 等等。

Window中的token是什么,有什么用?

token?又是个啥呢?刚才window操作过程中也没出现啊。

token其实大家应该工作中会发现一点踪迹,比如application的上下文去创建dialog的时候,就会报错:

  1. unable to add window --token null 

所以这个token跟window操作是有关系的,翻到刚才的addview方法中,还有个细节我们没说到,就是adjustLayoutParamsForSubWindow方法。

  1. //Window.java 
  2.     void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) { 
  3.         if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW && 
  4.                 wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) { 
  5.             //子Window 
  6.             if (wp.token == null) { 
  7.                 View decor = peekDecorView(); 
  8.                 if (decor != null) { 
  9.                     wp.token = decor.getWindowToken(); 
  10.                 } 
  11.             } 
  12.         } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW && 
  13.                 wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) { 
  14.             //系统Window 
  15.         } else { 
  16.             //应用Window 
  17.             if (wp.token == null) { 
  18.                 wp.token = mContainer == null ? mAppToken : mContainer.mAppToken; 
  19.             } 
  20.              
  21.         } 
  22.     } 

上述代码分别代表了三个Window的类型:

  • 子Window。需要从decorview中拿到token。
  • 系统Window。不需要token。
  • 应用Window。直接拿mAppToken,mAppToken是在setWindowManager方法中传进来的,也就是新建Window的时候就带进来了token。

然后在WMS中的addWindow方法会验证这个token,下次说到WMS的时候再看看。

所以这个token就是用来验证是否能够添加Window,可以理解为权限验证,其实也就是为了防止开发者乱用context创建window。

拥有token的context(比如Activity)就可以操作Window。没有token的上下文(比如Application)就不允许直接添加Window到屏幕(除了系统Window)。

Application中可以直接弹出Dialog吗?

这个问题其实跟上述问题相关:

  • 如果直接使用Application的上下文是不能创建Window的,而Dialog的Window等级属于子Window,必须依附与其他的父Window,所以必须传入Activity这种有window的上下文。
  • 那有没有其他办法可以在Application中弹出dialog呢?有,改成系统级Window:
  1. //检查权限 
  2. if (!Settings.canDrawOverlays(this)) { 
  3.     val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) 
  4.     intent.data = Uri.parse("package:$packageName"
  5.     startActivityForResult(intent, 0) 
  6.  
  7. dialog.window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG) 
  8.  
  9. <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> 

  • 另外还有一种办法,在Application类中,可以通过registerActivityLifecycleCallbacks监听Activity生命周期,不过这种办法也是传入了Activity的context,只不过在Application类中完成这个工作。

关于事件分发,事件到底是先到DecorView还是先到Window的?

经过上述一系列问题,是不是对Window印象又深了点呢?最后再看一个问题,这个是wanandroid论坛上看到的,

这里的window可以理解为PhoneWindow,其实这道题就是问事件分发在Activity、DecorView、PhoneWindow中的顺序。

当屏幕被触摸,首先会通过硬件产生触摸事件传入内核,然后走到FrameWork层(具体流程感兴趣的可以看看参考链接),最后经过一系列事件处理到达ViewRootImpl的processPointerEvent方法,接下来就是我们要分析的内容了:

  1. //ViewRootImpl.java 
  2.  private int processPointerEvent(QueuedInputEvent q) { 
  3.             final MotionEvent event = (MotionEvent)q.mEvent; 
  4.             ... 
  5.             //mView分发Touch事件,mView就是DecorView 
  6.             boolean handled = mView.dispatchPointerEvent(event); 
  7.             ... 
  8.         } 
  9.  
  10. //DecorView.java 
  11.     public final boolean dispatchPointerEvent(MotionEvent event) { 
  12.         if (event.isTouchEvent()) { 
  13.             //分发Touch事件 
  14.             return dispatchTouchEvent(event); 
  15.         } else { 
  16.             return dispatchGenericMotionEvent(event); 
  17.         } 
  18.     } 
  19.  
  20.     @Override 
  21.     public boolean dispatchTouchEvent(MotionEvent ev) { 
  22.         //cb其实就是对应的Activity 
  23.         final Window.Callback cb = mWindow.getCallback(); 
  24.         return cb != null && !mWindow.isDestroyed() && mFeatureId < 0 
  25.                 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); 
  26.     } 
  27.  
  28.  
  29. //Activity.java 
  30.     public boolean dispatchTouchEvent(MotionEvent ev) { 
  31.         if (ev.getAction() == MotionEvent.ACTION_DOWN) { 
  32.             onUserInteraction(); 
  33.         } 
  34.         if (getWindow().superDispatchTouchEvent(ev)) { 
  35.             return true
  36.         } 
  37.         return onTouchEvent(ev); 
  38.     } 
  39.  
  40. //PhoneWindow.java 
  41.     @Override 
  42.     public boolean superDispatchTouchEvent(MotionEvent event) { 
  43.         return mDecor.superDispatchTouchEvent(event); 
  44.     } 
  45.  
  46. //DecorView.java 
  47.     public boolean superDispatchTouchEvent(MotionEvent event) { 
  48.         return super.dispatchTouchEvent(event); 
  49.     }     

事件的分发流程就比较清楚了:

ViewRootImpl——>DecorView——>Activity——>PhoneWindow——>DecorView——>ViewGroup

(这其中就用到了getCallback参数,也就是之前addView中传入的callback,也就是Activity本身)

但是这个流程确实有些奇怪,为什么绕来绕去的呢,光DecorView就走了两遍。

参考链接中的说法我还是比较认同的,主要原因就是解耦。

ViewRootImpl并不知道有Activity这种东西存在,它只是持有了DecorView。所以先传给了DecorView,而DecorView知道有AC,所以传给了AC。

Activity也不知道有DecorView,它只是持有PhoneWindow,所以这么一段调用链就形成了。

参考

《Android开发艺术探索》 《Android进阶解密》 https://mp.weixin.qq.com/s/wy9V4wXUoEFZ6ekzuLJySQ https://blog.csdn.net/weixin_43766753/article/details/108350589 https://wanandroid.com/wenda/show/12119