写在最前:本文涉及到源码的部分,查看的是 Android 8.1.0_r33 的源码,部分与原文中代码有出入。
附上查看源码的网址:http://androidxref.com/
学习内容:
原文开篇部分:
通过 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_TOUCH_MODAL
系统会将当前 Window 区域以外的单击事件传递给底层的 Window,当前 Window 区域以内的事件则自己处理。一般开启,否则其他 Window 将无法收到单击事件。
FLAG_SHOW_WHEN_LOCKED
开启此模式可以让 Window 显示在锁屏界面
Type 参数表示 Window 的类型,Window 有三种类型:
关于上面提到的层级范围,此处进行说明: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“。
Window 是一个抽象概念,每一个 Window都对应着一个 View 和一个 ViewRootImpl,WIndow 和 View 通过 ViewRootImpl 建立联系,因此 Window 以 View 的形式存在,View 才是 Window 存在的实体。
实际开发中,对 Window 的访问必须通过 WindowManager。
添加过程通过 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 mViews = new ArrayList();
//存储所有 Window 所对应的 ViewRootImpl
private final ArrayList mRoots = new ArrayList();
//存储所有 Window 中对应的布局参数
private final ArrayList mParams = new ArrayList();
//存储正在被删除的 View 对象,或者说已经调用 removeView 方法但是删除操作尚未完成 Window 对象
private final ArraySet mDyingViews = new ArraySet();
通过 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 也只是一系列代码细节)
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 方法内部会调用 dispatchDetachedFromWindow 方法,在其中实现真正删除 View 的逻辑,该方法中主要做 四件事:
直接看 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);
}
}
这个过程就比较简单:
首先应该了解 Activity 的启动过程,这一部分留待第九章再说。
简单来说,最终会由 ActivityThread 的 performLaunchActivity 来完成整个启动过程,此方法中会通过类加载器创建 Activity 的实例对象,并调用 attach 方法为其关联运行过程中所依赖的一系列上下文环境对象。
在 Activity 的 attach 方法里,系统会创建 Activity 所属的 Window 对象并为其设置回调接口,Window 的对象创建由 PolicyManager 的 makeNewWindow 方法实现。同时当 Window 接收到外界的状态改变时就会通过 Callback 接口,回调 Activity 的方法。
关于 Window 的创建:
关于 Activity 的视图如何附属到 Window:
Activity 的 setContentView 方法交由 Window 处理,直接分析 PhoneWindow 的 setContentView 即可
PhoneWindow 的 setContentView 方法遵循以下几个步骤
如果没有 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);
}
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。
需要注意:
首先 Toast 也是基于 Window 实现的,但是由于 Toast 具有定时取消这一功能,所以系统采用了 Handler。
Toast 内部有两类 IPC 过程:
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 方法)
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 的地方。
到这里 Toast 地 Window 的创建过程就分析完了。
本章结束。