写在最前:本文涉及到源码的部分,查看的是 Android 8.1.0_r33 的源码,部分与原文中代码有出入。
附上查看源码的网址:http://androidxref.com/
学习内容:
- Window 和 WindowManager
- Window 的内部工作原理
- Window 的添加、更新和删除
- Actvitiy、Dialog 等类型的 Window 对象的创建过程
原文开篇部分:
- Window 是一个抽象类,具体实现是 PhoneWindow
- WindowManager 是外界访问 Window 的入口,Window 的实现位于 WindowManagerService 中,WindowManager 和 WindowManagerService 的交互是一个 IPC 过程
- Window 实际是 View 的管理者,视图都是通过 Window 呈现的。
1.Window 和 WindowManger
通过 WindowManager 添加一个 Window
mFloatingButton = new Button(this);
mFloatintButton.setText("button");
mLayoutParams = new WindowManager.LayoutParams(LayoutParams.RTAP_CONTENT,LayoutParams.WRAP_CONTENT,0,0,PixelFormat.TRANSPARENT);
mLayoutParams.flags = KatiytOarams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y = 300;
mWindowManager.addView(mFloatingButton, mLayoutParams);
通过以上代码,可以将一个 Button 添加到坐标为(100,300)的位置上。
下面说明 WindowManager.LayoutParams 中的 flags 和 type 这两个参数:
-
flags 参数表示 Window 的属性,下面列出一个常用的选项:
-
FLAG_NOT_FOCUSABLE
表示 WIndow 不需要获取焦点,也不需要接收各种输入事件,最终事件会传递给下层的具有焦点的 Window
-
FLAG_NOT_FOCUSABLE
-
FLAG_NOT_TOUCH_MODAL
系统会将当前 Window 区域以外的单击事件传递给底层的 Window,当前 Window 区域以内的事件则自己处理。一般开启,否则其他 Window 将无法收到单击事件。
-
FLAG_SHOW_WHEN_LOCKED
开启此模式可以让 Window 显示在锁屏界面 -
Type 参数表示 Window 的类型,Window 有三种类型:
- 应用 Window:对应一个 Activity;层级范围是 1 ~ 99。
- 子 Window:不能单独存在,需要附属再特定的父 Window 上,比如常见的 Dialog;层级范围是 1000 ~ 1999
- 系统 Window:需要声明权限才能创建的 Window,比如 Toast;层级范围是 2000 ~ 2999
关于上面提到的层级范围,此处进行说明:Window 是分层的,每个 Window 都有对应的 z-ordered,层级大的会覆盖再层级小的 Window 的上面。
WindowManager 提供的功能
提供的功能很简单,一般只有三个方法:添加 View、更新 View 和删除 View,这三个方法定义在接口 ViewManager 中(WindowManager 继承了 ViewManager)
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
//继承关系
public interface WindowManager extends ViewManager {
//...
}
原文中说道,"WindowManager 操作 Window 的过程更像是在操作 Window 中的 View"。
2. Window 的内部机制
Window 是一个抽象概念,每一个 Window都对应着一个 View 和一个 ViewRootImpl,WIndow 和 View 通过 ViewRootImpl 建立联系,因此 Window 以 View 的形式存在,View 才是 Window 存在的实体。
实际开发中,对 Window 的访问必须通过 WindowManager。
2.1 Window 的添加过程
添加过程通过 WindowManager 的 addView 来实现,具体实现类是 WindowManagerImpl 类。
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
可以看到,实际上 WindowManagerImp 采用了桥接模式,将具体的实现 委托 给了 mGlobal(WindowManagerGlobal)来处理,WindowManagerGlobal 以工厂的形式向外提供自己的实例。
WindowManagerGlobal 的 addView 方法有如下几步:
-
检查参数是否合法,如果是子 Window 那么还需要调整一些布局参数
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; if (parentWindow != null) { parentWindow.adjustLayoutParamsForSubWindow(wparams); }
-
创建 ViewRootImpl 并将 View 添加到列表中
root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams);
关于上面代码中的 mViews,mRoots 等:
//存储所有 Window 所对应的 View private final ArrayList<View> mViews = new ArrayList<View>(); //存储所有 Window 所对应的 ViewRootImpl private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); //存储所有 Window 中对应的布局参数 private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>(); //存储正在被删除的 View 对象,或者说已经调用 removeView 方法但是删除操作尚未完成 Window 对象 private final ArraySet<View> mDyingViews = new ArraySet<View>();
-
通过 ViewRootImpl 来更新界面并完成 Window 的添加过程
这个步骤通过 ViewRootImpl 的 setView 方法完成:
//WindowManagerGlobal.addView() 方法内部 root.setView(view, wparams, panelParentView); //ViewRootImpl.setView(...) 方法内部 //... requestLayout(); //... //ViewRootImpl @Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
可以看到,通过 requestLayout 发起异步刷新请求,而其中的 scheduleTraversals 实际上是 View 绘制的入口。
接着会通过 WindowSession 最终来完成 Window 的添加过程。
//ViewRootImpl.setView(..)方法内部 try { mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); 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); }
上面代码中,mWindowSession 类型是 IWindowSession,是一个 Binder 对象,真正的实现类是 Session,也就是说 WIndow 的添加过程是一个 IPC 调用。
在 Session 内部会通过 WindowManagerService 实现 Window 的添加:
@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); }
如此,Window 的添加请求就交给 WindowManagerService 去处理了,在 WindowManagerService 中会为每一个应用保留一个单独的 Session,具体不再分析。
(原文分析到此,认为到此添加流程已经很清晰,再深入 WindowManagerSercice 也只是一系列代码细节)
2.2 Window 的删除过程
Window 的删除过程和添加过程初始阶段类似,直接分析 WindowManagerGlobal 的 removeView 方法:
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
if (curView == view) {
return;
}
throw new IllegalStateException("Calling with view " + view + " but the ViewAncestor is attached to " + curView);
}
}
逻辑很清晰,首先通过 findViewLocked 方法查找待删除的 View 的索引,然后调用 removeViewLocked 来做进一步删除:
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());
}
}
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}
removeViewLocked 是通过 ViewRootImpl 来完成删除操作的,调用了其 die 方法。
上面代码中的 immediate 参数需要注意,该参数对应了 WindowManager 中的两种删除接口 removeView(immediate 为 false) 和 removeViewImmediate(immediate 为 true),分别表示异步删除和同步删除。
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();
return false;
}
if (!mIsDrawing) {
destroyHardwareRenderer();
} else {
Log.e(mTag, "Attempting to destroy the window while drawing!\n" + " window=" + this + ", title=" + mWindowAttributes.getTitle());
}
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
die 方法内部做了简单判断:
- 如果是同步删除,那么直接调用 doDie 方法;
- 如果是异步删除,则会发送一个 MSG_DIE 的请求删除消息,ViewRootImpl 中的 Handler 会处理此消息并调用 doDie 方法;在此过程中,由于 View 尚未完成删除操作,因此在上面 removeViewLocked 方法末尾部分代码中,会将其添加到 mDyingViews 中。
在 doDie 方法内部会调用 dispatchDetachedFromWindow 方法,在其中实现真正删除 View 的逻辑,该方法中主要做 四件事:
- 垃圾回收相关的工作,比如清楚数据和消息、移除回调
- 通过 Session 的 remove 方法删除Window,同样是一个 IPC 过程,最终会调用 WindowManagerService 的 removeView 方法
- 调用 View 的 dispatchDetachedFromWindow 方法,内部会调用 View 的 onDetachedFromWindow(View 从 Window 中移除时的回调,做终止动画、停止线程等一系列资源回收工作) 以及 onDetachedFromWindowInternal。
- 调用 WindowManagerGloabl 的 doRemoveView 方法刷新数据,包括 mRoots、mParams 以及 mDyingViews,需要将当前 Window 关联的这三类对象从列表中删除。
2.3 Window 的更新过程
直接看 WindowManagerGlobal 的 updateViewLayout 方法:
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 LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
root.setLayoutParams(wparams, false);
}
}
这个过程就比较简单:
- 首先更新 View 的 LayoutParams 并替换旧的 LayoutParams
- 更新 ViewRootImpl 的 LayoutParams
- ViewRootImpl 会通过 schedulTraversals 方法对 View 重新布局,包括 测量、布局、重绘三个过程
- ViewRootImpl 还会通过 WindowSession 更新 Window 的视图,这个过程最终由 WindowManagerService 的 relayoutWindow 具体实现,同样是一个 IPC 过程
3.Window 的创建过程
3.1 Activity 的 Window 的创建过程
首先应该了解 Activity 的启动过程,这一部分留待第九章再说。
简单来说,最终会由 ActivityThread 的 performLaunchActivity 来完成整个启动过程,此方法中会通过类加载器创建 Activity 的实例对象,并调用 attach 方法为其关联运行过程中所依赖的一系列上下文环境对象。
在 Activity 的 attach 方法里,系统会创建 Activity 所属的 Window 对象并为其设置回调接口,Window 的对象创建由 PolicyManager 的 makeNewWindow 方法实现。同时当 Window 接收到外界的状态改变时就会通过 Callback 接口,回调 Activity 的方法。
关于 Window 的创建:
- Activity 的 Window 通过 PolicyManager 的工厂方法创建,PolicyManager 是一个契约类,实现了 IPolicy 策略接口,该接口中定义了众多工厂方法。
- PolicyManager 的真正实现是 Policy 类,在 makeNewWindow 方法中,返回了 PhoneWindow 对象,由此得出 Window 的具体实现是 PhoneWindow。
关于 Activity 的视图如何附属到 Window:
Activity 的 setContentView 方法交由 Window 处理,直接分析 PhoneWindow 的 setContentView 即可
PhoneWindow 的 setContentView 方法遵循以下几个步骤
-
如果没有 DecorView,那么创建之。
- 通过 installDecor 方法内部的 generateDecor 直接创建 DecorView
- 通过 generateLayout 方法加载具体的布局文件到 DecorView
- 将 View 添加到 DecorView 的 mContentParent 中
到此步,Activity 的布局文件已经添加到 DecorView 里面。
- 回调 Activity 的 onContentChanged 方法通知 Activity 视图已经发生改变
Activity 的 onContentChanged 方法是个空实现,可以在 子Activity 中处理该回调。
但是!!
经过上面的三个步骤,DecorView 尚未被 WindowManager 正式添加到 Window。由于此时 DecorView 未被 WindowManager 识别,所以这个时候的 Window 无法提供具体功能,因为它还无法接收外界的输入信息。
在 ActivityThread 的 handleResumeActivity 方法中,首先调用 Activity 的 onResume 方法,接着调用 Activity 的 makeVisible(),在 makeVisible 方法中,DecorView 真正完成添加和显式这两个过程,此时 Activity 的视图才能被用户看到,如下所示:
void makeVisible() {
if(!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAtrributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
3.2 Dialog 的 Window 的创建过程
Dialog 的 Window 创建过程和 Activity 类似,有如下几个步骤:
1. 创建 Window:
2. 初始化 DecorView 并将 Dialog 的视图添加到 DecorView 中
3. 将DecorView 添加到 Window 中显示
在 Dialog 的 show 方法中,通过 WindowManager 的 addView 方法将 DecorView 添加到 Window 中。
Dialog 的 Window 创建和 Activity 的 Window 创建过程很类似,几乎没有什么区别;当 Dialog 被关闭时,会通过 WindowManager 的 removeViewImmediate(mDecor) 移除 DecorView。
需要注意:
- 普通 Dialog 必须采用 Activity 的 Context,如果采用 Application 的 Context 会报错,因为需要 应用token,而只有 Activity 拥有
- 系统 Window 比较特殊,不需要 token,所以也可以采用 Application 的 Context 的同时,指定对话框的 Window 为系统类型即可正常弹出对话框
3.3 Toast 的 Window 的创建过程
首先 Toast 也是基于 Window 实现的,但是由于 Toast 具有定时取消这一功能,所以系统采用了 Handler。
Toast 内部有两类 IPC 过程:
- Toast 访问 NotificationManagerService(NMS)
- NMS 回调 Toast 里的 TN 接口
Toast 提供 show 和 cancel 分别用于显示和隐藏 Toast,二者内部都是一个 IPC 过程:
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
public void cancel() {
mTN.cancel();
}
显示和隐藏都需要 NMS 实现,而 NMS 无法运行在系统的进程中,所以需要通过远程调用的方式。
关于 TN 这个类,它是一个 Binder 类,在 Toast 和 NMS 进行 IPC 的过程中,当 NMS 处理 Toast 的显示或隐藏请求时会跨进程回调 TN 中的方法,此时由于 TN 运行在 Binder 线程池中,所以需要通过 Handler 将其切换到当前线程中。同时这也意味着 Toast 无法在没有 Looper 的线程中弹出。
首先分析 Toast 的显示过程( show 方法)
-
调用了 NMS 中的 enqueueToast 方法
-
enqueueToast 首先将 Toast 请求封装为 ToastRecord 对象并将其添加到 mToastQueue 队列中。
mToastQueue 是一个 ArrayList,对非系统应用而言,mToastQueue 最多同时存在 50 个 ToastRecord,此举是为了防止 DOS 拒绝服务攻击,避免其他应用没有机会弹出 Toast
-
添加后,NMS 通过 showNextToastLocked 方法显示当前 Toast。
void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); while (record != null) { if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback); try { record.callback.show(record.token); scheduleTimeoutLocked(record); return; } catch (RemoteException e) { Slog.w(TAG, "Object died trying to show notification " + record.callback + " in package " + record.pkg); // remove it from the list and let the process die int index = mToastQueue.indexOf(record); if (index >= 0) { mToastQueue.remove(index); } keepProcessAliveIfNeededLocked(record.pid); if (mToastQueue.size() > 0) { record = mToastQueue.get(0); } else { record = null; } } } }
上面的代码很简单,Toast 的显示通过 ToastRecord 的 callback 来完成,这个callback 实际上就是 Toast 的 TN 对象的远程 Binder,通过 callback 来访问 TN 中的方法是需要跨进程来完成的,最终被调用的 TN 中的方法会运行在发起 Toast 请求的应用的 Binder 线程池中。
-
Toast 显示之后,NMS 会通过 scheduleTimeoutLocked 方法发送延时消息,具体延时取决于 Toast 的时长:LONG_DELAY 是 3.5s,SHORT_DELAY 是 2s
-
private void scheduleTimeoutLocked(ToastRecord r) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; mHandler.sendMessageDelayed(m, delay); }
延时时间过后,NMS 通过 cancelToastLocked 方法隐藏 Toast 并将其从 mToastQueue 中移除,此时如果 mToastQueue 中还有其他 Toast,那么 NMS 就继续显示其他 Toast。
void cancelToastLocked(int index) { ToastRecord record = mToastQueue.get(index); try { record.callback.hide(); } catch (RemoteException e) { Slog.w(TAG, "Object died trying to hide notification " + record.callback + " in package " + record.pkg); // don't worry about this, we're about to remove it from // the list anyway } ToastRecord lastToast = mToastQueue.remove(index); mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY); keepProcessAliveIfNeededLocked(record.pid); if (mToastQueue.size() > 0) { // Show the next one. If the callback fails, this will remove // it from the list, so don't assume that the list hasn't changed // after this point. showNextToastLocked(); } }
再分析 Toast 的隐藏过程(cancel 方法)
-
同样是通过 ToastRecord 的 callback 完成的,同样是一次 IPC 过程,其工作过程和 Toast 的显示过程是类似的
try { record.callback.hide(); } catch (RemoteException e) { Slog.w(TAG, "Object died trying to hide notification " + record.callback + " in package " + record.pkg); // don't worry about this, we're about to remove it from // the list anyway }
再看 TN 类:
通过上面的分析,Toast 的显示和隐藏实际上是通过 Toast 的 TN 这个类来实现的,它有两个方法 show 和 hide,分别对应 Toast 的显示和隐藏。这两个方法运行在 Binder 线程池中,内部使用了 Handler。
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
上面代码中,分别发送了 SHOW 和 HIDE 两种消息,在 handleMessage 中分别调用 handleShow 和 handleHide 方法,这二者才是真正完成显示和隐藏 Toast 的地方。
- handleShow 中 addView 方法将 Toast 的视图添加到 Window 中
- handleHide 中调用 remoteView 方法将 Toast 的视图从 Window 中移除
到这里 Toast 地 Window 的创建过程就分析完了。
本章结束。
网友评论