第 8 章 理解 Window 和 WindowManager
Window 表示一个窗口的概念
Window 是一个抽象类,具体实现是 PhoneWindow
通过 WindowManager 即可创建一个 Window,WindowManager 是外界访问 Window 的入口
Window 的具体实现位于 WindowManagerService 中,WindowManager 和 WindowManagerService 的交互是一个 IPC 过程
Window 实际是 View 的直接管理者(Android 中的视图都是通过 Window 来呈现的)
8.1 Window 和 WindowManager
代码演示通过 Windowmanager 添加 Window:
val button = Button(this)
button.text = "asdfgasdrg"
val layoutParams = WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT
, WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT)
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
layoutParams.gravity = Gravity.START or Gravity.TOP
layoutParams.x = 100
layoutParams.y = 300
windowManager.addView(button, layoutParams)
WindowManager 中的 flags
和 type
这两个参数比较重要。
Flags 参数表示 Window 的属性,可以控制 Window 的显示特性
- FLAG_NOT_FOCUSABLE
表示 Window 不需要获取焦点,也不需要接受各种输入事件,此标记会同时启用 FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层具有焦点的 Window - FLAG_NOT_TOUCH_MODAL
此模式下会把当前区域以外的点击事件传递给底层 Window,当前 Window 区域以内的点击事件自己处理。很重要的标记位,一般都需要开启此标记为,不然不能处理点击事件 - FLAG_SHOW_WHEN_LOCKED
开启此模式可以让 Window 显示在锁屏的界面上
- FLAG_NOT_FOCUSABLE
Type 表示 Window 的类型
- 应用 Window
它对应着一个 Activity - 子 Window
不能单独存在,它需要附属在特定的父 Window 之中,比如常见的 Dialog 就是个子 Window - 系统 Window
需要声明权限才能创建的 Window。Toast 和 系统状态栏都是系统 Window
- 应用 Window
Window 是分层的,层级大的会覆盖层级小的。
层级范围:
应用 Window —— 1 ~ 99
子 Window —— 1000 ~ 1999
系统 Window —— 2000 ~ 2999
层级范围对应着 WindowManager.LayoutParams 的 type 参数。
系统级的 Window 层级大,可以全局显示。创建系统级 Window 需要设置 type 和添加权限:
系统级 type 有: TYPE_SYSTEM_OVERLAY、TYPE_SYSTEM_ERROR。
设置权限:<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
WindowManager 所提供的功能(继承自 ViewManager):
它可以创建一个 Window 并向其添加 View;更新 Window 中的 View;删除 Window(删除 View 就会删除 Window)。由此看来 WindowManager 操作 Window 的过程更像是在操作 Window 中的 View 。
可以拖动的 Window 效果实现:监听触摸事件,不断通过 WindowManager 的 updateViewLayout 来更新 layoutParams 的 x、y 值。
8.2 Window 的内部机制
每一个 Window 都对应着一个 View 和一个 ViewRootImpl,Window 和 View 通过 ViewRootImpl 来建立联系,因此 Window 并不是实际存在的,它是以 View 的形式存在的。
实际使用中无法直接访问 View,需要通过 WindowManager。
8.2.1 Window 的添加过程
api-27 Window 添加调用链:
WindowManagerImpl # addView
(@NonNull View view, @NonNull ViewGroup.LayoutParams params)
Window 是个接口类,它的实现是 WindowManagerImpl ,所以调用 Window#addView 即 WindowManagerImpl#addViewWindowManagerGlobal # addView
(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow)... //存储所有 Window 所对应的 View private final ArrayList<View> mViews = new ArrayList<View>(); //存储所有 Window 对应的 ViewRootImpl private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); //存储那些正在被删除的 View 对象,或者说是已经调用 removeView 方法但是删除操作还未完成的 Window 对象 private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>(); private final ArraySet<View> mDyingViews = new ArraySet<View>(); ... public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { //1.检查参数是否合法 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"); } final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; //1.如果是子 Window,调整一些布局参数 if (parentWindow != null) { parentWindow.adjustLayoutParamsForSubWindow(wparams); } ... ViewRootImpl root; View panelParentView = null; synchronized (mLock) { ... //2.创建 ViewRootImpl root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); //2.将 View 添加到列表中 mViews.add(view); mRoots.add(root); mParams.add(wparams); // do this last because it fires off messages to start doing things try { //3.通过 ViewRootImpl 来更新界面并完成 Window 的添加 root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { ... } } }
ViewRootImpl # setView
(View view, WindowManager.LayoutParams attrs, View panelParentView)/** * We have one child */ public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { ... mAdded = true; int res; /* = WindowManagerImpl.ADD_OKAY; */ // View 绘制过程在此方法里 requestLayout(); ... try { mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); //mWindowSession 的类型是 IWindowSession,是一个 Binder 对象,找源码最终的实现是 Session。所以说 Window 的添加过程是一次 IPC 调用 res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel); } catch (RemoteException e) { mAdded = false; mView = null; mAttachInfo.mRootView = null; mInputChannel = null; mFallbackEventHandler.setView(null); unscheduleTraversals(); setAccessibilityFocus(null, null); throw new RuntimeException("Adding window failed", e); } finally { if (restore) { attrs.restore(); } } ... } } } @Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); //View 绘制的入口 } }
Session # addToDisplay
(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, InputChannel outInputChannel)@Override public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, InputChannel outInputChannel) { return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outContentInsets, outStableInsets, outOutsets, outInputChannel); }
WindowManagerService # addWindow
(Session session, IWindow client, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, InputChannel outInputChannel)
这里就是具体实现了,就不贴代码了
Window 的添加最终交给 WindowManagerService 去处理了,在 WindowManagerService 内部会为每一个应用保留一个单独的 Session。
8.2.2 Window 的删除过程
api-27 调用链:
WindowManagerImpl # removeView
(View view)WindowManagerGlobal # removeView
(View view, boolean immediate)public void removeView(View view, boolean immediate) { if (view == null) { throw new IllegalArgumentException("view must not be null"); } synchronized (mLock) { //找到将要删除的 View 的索引 int index = findViewLocked(view, true); View curView = mRoots.get(index).getView(); //调用 removeViewLocked 进行进一步删除 removeViewLocked(index, immediate); if (curView == view) { return; } throw new IllegalStateException("Calling with view " + view + " but the ViewAncestor is attached to " + curView); } }
WindowManagerGlobal # removeViewLocked
(int index, boolean immediate)private void removeViewLocked(int index, boolean immediate) { ViewRootImpl root = mRoots.get(index); View view = root.getView(); if (view != null) { InputMethodManager imm = InputMethodManager.getInstance(); if (imm != null) { imm.windowDismissed(mViews.get(index).getWindowToken()); } } //通过 ViewRootImpl 来完成删除操作 boolean deferred = root.die(immediate); if (view != null) { view.assignParent(null); if (deferred) { //异步删除,把要删除的 View 放入列表 mDyingViews.add(view); } } }
WindowManager 有两种删除接口
removeView
和removeViewImmediate
,分别表示异步删除和同步删除。
一般不用同步删除,以免发生意外的错误ViewRootImpl # die
(boolean immediate)/** * @param immediate True, do now if not in traversal. False, put on queue and do later. * @return True, request has been queued. False, request has been completed. */ boolean die(boolean immediate) { // Make sure we do execute immediately if we are in the middle of a traversal or the damage // done by dispatchDetachedFromWindow will cause havoc on return. if (immediate && !mIsInTraversal) { //同步删除,直接调用 doDie 方法进行进一步删除 doDie(); return false; } if (!mIsDrawing) { destroyHardwareRenderer(); } else { Log.e(mTag, "Attempting to destroy the window while drawing!\n" + " window=" + this + ", title=" + mWindowAttributes.getTitle()); } //异步删除 给 handler 发消息后,直接返回 true。(会在 removeViewLocked 中把将要删除的 View 放到 mDyingViews 列表) //handler 处理此消息并调用 doDie 方法 mHandler.sendEmptyMessage(MSG_DIE); return true; }
ViewRootImpl # doDie
()void doDie() { ... synchronized (this) { if (mRemoved) { return; } mRemoved = true; if (mAdded) { dispatchDetachedFromWindow(); } ... mAdded = false; } //刷新 WindowManagerGlobal 的数据。包括 mRoots、mParams、mDyingViews,需要将当前 Window 所关联的这三类对象从列表中删除 WindowManagerGlobal.getInstance().doRemoveView(this); } void dispatchDetachedFromWindow() { if (mView != null && mView.mAttachInfo != null) { mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false); //调用 View 的 dispatchDetachedFromWindow,在内部会调用 View 的 onDetachedFromWindow() 方法以及 onDetachedFromWindowInternal()。 //onDetachedFromWindow() 当 View 被移除时,这个方法会被调用,处理一些资源回收、终止动画、停止线程等 mView.dispatchDetachedFromWindow(); } //垃圾回收相关工作,比如清除数据和消息、移除回调 start mAccessibilityInteractionConnectionManager.ensureNoConnection(); mAccessibilityManager.removeAccessibilityStateChangeListener( mAccessibilityInteractionConnectionManager); mAccessibilityManager.removeHighTextContrastStateChangeListener( mHighContrastTextManager); removeSendWindowContentChangedCallback(); destroyHardwareRenderer(); setAccessibilityFocus(null, null); mView.assignParent(null); mView = null; mAttachInfo.mRootView = null; mSurface.release(); if (mInputQueueCallback != null && mInputQueue != null) { mInputQueueCallback.onInputQueueDestroyed(mInputQueue); mInputQueue.dispose(); mInputQueueCallback = null; mInputQueue = null; } if (mInputEventReceiver != null) { mInputEventReceiver.dispose(); mInputEventReceiver = null; } //垃圾回收相关工作,比如清除数据和消息、移除回调 end try { //通过 Session 的 remove 方法删除 Window。这也是一个 IPC 过程,最终会调用 WindowManagerService 的 removeWindow 方法删除 mWindowSession.remove(mWindow); } catch (RemoteException e) { } // Dispose the input channel after removing the window so the Window Manager // doesn't interpret the input channel being closed as an abnormal termination. if (mInputChannel != null) { mInputChannel.dispose(); mInputChannel = null; } mDisplayManager.unregisterDisplayListener(mDisplayListener); unscheduleTraversals(); }
8.2.3 Window 的更新过程
api-27 调用链:
WindowManagerImpl # updateViewLayout
(@NonNull View view, @NonNull ViewGroup.LayoutParams params)WindowManagerGlobal # updateViewLayout
(View view, ViewGroup.LayoutParams params)public void updateViewLayout(View view, ViewGroup.LayoutParams params) { if (view == null) { throw new IllegalArgumentException("view must not be null"); } if (!(params instanceof WindowManager.LayoutParams)) { throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); } final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; //更新 View 的 LayoutParams 并替换掉老的 LayoutPArams。 view.setLayoutParams(wparams); synchronized (mLock) { int index = findViewLocked(view, true); ViewRootImpl root = mRoots.get(index); mParams.remove(index); mParams.add(index, wparams); //更新 ViewRootImpl 中的 LayoutParams root.setLayoutParams(wparams, false); } }
ViewRootImpl 中会调用
scheduleTraversals
方法对 View 重新绘制,包括 测量、布局、重绘这三个过程。
还会通过 WindowSession 来更新 Window 的视图,这个过程最终是由 WindowManagerService 的 relayoutWindow()来具体实现的,也是一个 IPC 过程。
8.3 Window 的创建过程
View 是 Android 中视图的呈现方式,但是不能单独存在,必须附着在 Window 这个抽象概念上,因此有视图的地方就有 Window
8.3.1 Activity 的 Window 创建过程
Activity 的启动最终会由 ActivityThread 中的 performLaunchActivity() 来完成整个启动过程。这个方法内部会 实例化 Activity、调用 attach 方法等,attach 方法会为其关联运行过程中所依赖的一系列上下文环境变量:
在 Activity 的 attach 方法里,系统会创建 Activity 所属的 Window 对象并设置回调接口。当 Window 接收到外界状态改变时就会回调 Activity 的方法。(如 onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent 等等):
//api-27
//Activity#attach()
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
下面分析 Activity 的视图是怎么附着到 Window 上的。看 setContentView 方法(它提供了 Activity 的视图):
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
Activity 的 setContentView 调用了 Window 的 setContentView,而 Window 的具体实现是 PhoneWindow(atach 方法中可以看出来)。PhoneWindow 的 setContentView 的步骤:
//PhoneWindow#setContentView()
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
如果没有 DecorView,就创建它
DecorView 是 Activity 顶层 View,包括 标题栏 和 内容栏,内容栏固定资源 id:android.R.id.content
通过 generateDecor() 来创建 DecorView(跟布局是 FrameLayout)
通过 generateLayout() 来加载具体的布局文件(标题栏和内容栏)将 View 添加到 DecorView 的 mContentParent 中
添加到了 内容栏(android.R.id.content)。
所以 Activity 的添加试图方法叫 setContentView 而不是 setView回调 Activity 的 onContentChanged 方法通知 Activity 视图已经发生改变
Activity 实现了 Window 的 CallBack 接口,就能收到回调做相应处理了
经过这三个步骤后,DecorView 创建并初始化完毕、Activity 的布局文件也添加到 DecorView 中了。但是,DecorView 还没有被 WindowManager 添加到 Window 中。
在 ActivityThread 的 handleResumeActivity 方法中,先调用 Activity 的 onResume 方法,接着调用 Activity 的 makeVisiable()。在 makeVisiable 方法中,DecorView 真正完成了添加和显示这两个过程:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
此时,Activity 中的 Window 的创建过程才算完了。
总结一波
:
ActivityThread#handleLaunchActivity
被调用,内部ActivityThread#performLaunchActivity
被调用,内部
- 实例化 Activity
- 调用
Activity#attach()
方法
- 内部
实例化 Window
(即 PhoneWindow)
- 内部
- 使 Activity 的
onCreate
方法被调用
setContentView
在此调用。
- 调用
Window 的 setContentView
创建 DecorView
- 把 Activity 的
布局文件添加到 DecorView 的内容区
- 调用
ActivityThread#handleResumeActivity
方法调用
- 使 Activity 的
onResume
被调用 - 调用 Activity 的
makeVisiable
方法
在 makeVisiable 方法里才真正完成 Window 的添加和显示过程
- 使 Activity 的
8.3.2 Dialog 的 Window 创建过程
创建 Window
Dialog 的 构造方法会实例化 Window(PhoneWindow)Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) { ... mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); final Window w = new PhoneWindow(mContext); mWindow = w; w.setCallback(this); w.setOnWindowDismissedCallback(this); w.setOnWindowSwipeDismissedCallback(() -> { if (mCancelable) { cancel(); } }); w.setWindowManager(mWindowManager, null, null); w.setGravity(Gravity.CENTER); mListenersHandler = new ListenersHandler(this); }
初始化 DecorView 并将 Dialog 的视图添加到 DecorView 中
调用的 Window 的 setContentView 方法public void setContentView(@LayoutRes int layoutResID) { mWindow.setContentView(layoutResID); }
将 DecorView 附着到 Window 上。(通过调用 Dialog 的 show 方法)
public void show() { ... mWindowManager.addView(mDecor, l); mShowing = true; sendShowMessage(); }
创建一个 Dialog 并显示的示例:
val textView = TextView(this@MainActivity)
textView.text = "hello"
val dialog = Dialog(this@MainActivity) //构造方法创建 PhoneWindow
dialog.setContentView(textView) //内部调用 PhoneWindow 的 setContentView 初始化 DecorView 并添加 Dialog 布局到 DecorView 内容区
dialog.show() //将 DecorView 附着到 Window 上
8.3.3 Toast 的 Window 创建过程
弹出 Toast 常规示例:
Toast
.makeText(this, "123", Toast.LENGTH_SHORT)
.show()
从弹出一个 Toast 开始分析:
- makeText 方法,会实例化 Toast、加载默认布局设置提示文字,把默认布局设置给 mNextView、设置 duration
- show 方法,内部通过 binder 方式 调用 NMS 的 enqueueToast 方法
- NMS 的 enqueueToast 方法,把 Toast 封装成 ToastRecord 并添加到 mToastQueue 集合中,然后尝试去调用 showNextToastLocked 方法弹出 Toast
- showNextToastLocked 方法,通过 binder 调用 Toast 的 内部 Binder 类 TN 的 show 方法、给 NMS 内部发延时消息隐藏 Toast,最终通过 binder 调用 Toast 的 内部 Binder 类 TN 的 hide 方法
- TN 的 show 方法 发送 handler 消息最终切换到原始线程调用 handleShow 方法,handleShow 方法里:把 Toast 的 View 附着到 Window 上
- TN 的 hide 方法 发送 handler 消息最终切换到原始线程调用 handleHide 方法,handleHide 方法里:调用 WindowManager 的 removeViewImmediate 方法移除视图
提问:为什么 Toast 不能在没有 Looper 的线程里弹出?(答案不写,便于每次看到的时候思考一下)