Window 表示一个窗口的概念,是一个抽象类,它的具体实现是 PhoneWindow。
8.1 Window 和 WindowManager
WindowManager windowManager = getWindowManager();
Button button = new Button(this);
button.setText("一个按钮");
mLayoutParams = new WindowManager.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);
mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y = 300;
windowManager.addView(button,mLayoutParams);
FLAG_NOT_TOUCH_MODAL
表示 Window 不需要获取焦点,也不需要输入时间,会启用 FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给具有焦点的 Window。FLAG_NOT_TOUCH_MODAL
在此模式下,系统会将当前 Window 区域以外的点击事件传递给底层的 Window,区域以内的单击事件则自己处理。FLAG_SHOW_WHEN_LOCKED
开启此模式可以让 Winodw 显示在锁屏的界面上。
Type 参数表示 Window 的类型,Window 有三种类型:
应用 Window:对应这一个 Activity,层级范围:1~99;
子 Window:子 Window 不能单独存在,它需要附属在特定的父 Window 中,比如 Dialog。层级范围:1000~1999;
系统 Window:需要声明权限才能创建,比如 Toast 和系统状态栏。层级范围:2000~2999;
层级大的覆盖层级小的,对应这 WindowManager.LayoutParams 的type 参数。系统的 WindowManager 层级是最大的,一般我们可以选用 TYPE_SYSTEM_OVERLAY 或者 TYPE_SYSTEM_ERROR
mLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;//对应值:2006
使用 TYPE_SYSTEM_ERROR 必须声明权限
mLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;//对应值:2010
...
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
WindowManager 所提供的功能,只有三个常用方法:
public interface ViewManager
{
// 添加 View
public void addView(View view, ViewGroup.LayoutParams params);
// 更新 View
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
// 移除 View
public void removeView(View view);
}
8.2 Window 的内部机制
Window 是一个抽象的概念,每一个 Window 都对应着一个 View 和 ViewRootImpl,Window 和 View 通过 View RootIml来建立联系,因此 View 并不是实际存的,它是以 View 的形式存在,对 Window 的访问必须通过 WindowManager。
8.2.1 Window 的添加过程
Window 的添加过程需要通过 WIndowManager 的 addView 来实现。
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
WindowManager 是一个接口,它的真正实现是 WindowManagerImpl。
public interface WindowManager extends ViewManager {
...
WindowManagerImpl 类中并没有直接实现 Window 的三大操作,而是交给了 WindowManagerGlobal。
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@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);
}
...
WindowManagerImpl 这种工作米时候是典型的桥接模式,将所有方法都委托给 WindowManagerGlpbal 来实现。
- 检查参数是否合法,如果是子 Window 那么还需要调整一些布局参数
public final class WindowManagerGlobal {
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");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
...
- 创建 ViewRootImpl 并将 View 添加到列表中
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
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) {
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
// 将一系列对象添加到列表中
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
- mViews:储存的是所有Window 所对应的 View;
- mRoots:存储的是所有 Window 所对应的 ViewRootImpl;
- ** mParams:**存储的是所有 Window 的布局参数;
- mDyingViews:存储了那些正在被删除的 View 对象,调用 removeView 但是删除操作还未完成的 Window 对象;
-
通过 ViewTootImpl 来更新界面并完成 Window 的添加过程
这个步骤由 ViewRootImpl 的 setView 方法来完成,在 setView 内部会通过 requestLayout 来完成异步刷新请求。
public final class WindowManagerImpl implements WindowManager {
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
...
requestLayout();
....
scheduleTraversals 实际是 View 绘制的入口;
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
接着会通过 WindowSession 来最终完成 Window 的添加过程,WindowSession 是一个 Binder 对象,真正的实现类是 Session,也就是 Window 的添加过程是一次 IPC 调用。在Session 内部会通过 WIndowManagerService 来实现 Window 的添加。 如此一来,Window 的添加请求就交给 WindowManagerService 去处理了。WindowManagerService 内部就不用去深入分析了。(此处看不到源码。)
8.2.2 Window 的删除过程
Window 的删除过程和添加过程一样,都是先通过 WIndowManagerImpl 后,在进一步通过 WindowManagerGlobal 来实现的。
public final class WindowManagerGlobal {
...
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);
}
}
removeView 的逻辑很清晰,首先通过 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);
//异步删除时,添加到 mDyingViews 中
if (deferred) {
mDyingViews.add(view);
}
}
}
removeViewLocked 是通过 ViewRootImpl 来完成删除操作的,在 WindowManager 中提供两种删除接口:
removeView:异步删除(常用)
在 die 方法中异步删除,那么就发送一个 MSG_DIE 的消息,ViewRootImpl 中的 Handler 会处理此消息并调用 doDie 方法。removeViewImmediate:同步删除(基本用不到)
在 die 方法中同步删除,会直接就直接调用 doDie 方法。
这里主要说明异步删除,具体的删除操作由 ViewRootImpl 的 die 方法来完成,这里异步删除会返回到 removeViewLocked 方法中继续处理,而同步删除则不会返回。
boolean die(boolean immediate) {
// 同步删除
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;
}
在 doDie 方法内部会调用 dispatchDetachedFromWindow 方法,真正的删除 View 的逻辑在该方法内部实现,该方法主要做四件事:
- 垃圾回收相关的工作,比如清除数据和消息、移除回调。
- 通过 Session 的 remove 方法删除 Window:mWindowSession.remove(mWindw),这同样是一个 IPC 过程,最终会调用 WindowManagerService 的 removeWindow 方法。
- 调用 VIew 的dispatahDetachedFromWindow 方法,在内部会调用 View 的onDetachedFromWIndow()以及 onDetachedFromWindowInternal()。(onDetachedFromWIndow 当 View 被移除时会被调用)
- 调用 WindowManagerGlobal 的 doRemoveView 方法刷新数据,包括 mRoots、mParams、mDyingViews,需要将当前 Window 所关联的这三类对象从列表中删除。
8.2.3 Window 的更新过程
public final class WindowManagerGlobal {
...
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.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);
}
}
updateViewLayout 的步骤:
- 更新 View 的 LayoutParams 并替换掉老的 LayoutParams。
- 再更新 ViewRootImpl 中的 LayoutParanms,这一步通过 ViewRootImpl 的 setLayoutParams 方法来实现。
ViewRootImpl 的 setLayoutParams 方法会通过 scheduleTraversals 方法来对 View 重新布局,包括测量、布局、重绘这三个过程。ViewRootImpl 还会通过 WIndowSession 来更新 Window 的视图,这过程由 WindowManagerService 的 relayoutWindow()来具体实现,是个 IPC 过程。
8.3 Window 的创建过程
有视图的地方就有 Window
8.3.1 Activity 的 Window 创建过程
要分析 Activity 中的 Window 创建过程就必须立交 Activity 的启动过程,这里先大概了解即可。Activity 的启动过程很复杂,最终会由 ActivityThread 中的 performLaunchActivity来完成整个启动过程,在这个方法内部会通过类加载器创建 Activity 的实例对象,并调用其 attach 方法为其关联运行过程中所依赖的一系列上下文环境变量。
public final class ActivityThread {
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
if (activity != null) {
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window);
在 Activity 的 attch 方法里,系统会创建 Activity 所属的 Window 对象并为其设置回调接口,Window 对象的创建是通过 PolicyManager 的makeNewWindow 方法实现的。由于 Activity 实现了 Window 的 Callback 接口,因此当 WIndow 接收到外界的状态改变时就会回调 Activity 的方法。Callback 接口中的方法有很多,比如 onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent等待。
Activity 的 Window 是通过 PolicyManager 的一个工厂方法来创建的,但是从 PolicayManager 的类名可以看出,他不是一个普通的类,它是一个策略类。
public interface IPolicy {
public Window makeNewWindow(Context context);
public LayoutInflater makeNewLayoutInflater(Context context);
public WindowManagerPolicy makeNewWindowManager();
public FallbackEventHandler makeNewFallbackEventHandler(Context context);
}
在实际的调用中,PolicyManager是如何关联到Policy类中的mackNewWindow方法来实现如下,由此可以发现,window的具体实现的确就是phoneWindow
public static Window makeNewWindow(Context context) {
return new PhoneWindow(context);
}
关于PolicyManager是如何关联到IPolicy中,这个无法从源码中调用关系得到,这里的猜测可能是编译环节动态控制的,到这里window已经创建完成了,下面分析activity的视图是怎么依附在window上的由于activity的视图是由setContentView开始的,所有我们先看下这个方法:
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
PhoneWindow 的 setContentView 方法大致遵循如下几个步骤:
-
如果没有 DecorView,那么就创建它
DecorView 是一个 FrameLayout,是Activity 的顶级 View,包含标题栏和内容栏。DecorView 的创建过程由 installDecor 方法来完成,在方法内部会通过 generateDecor 方法来直接创建 DecorView,这时候 DecorView 还只是一个空白的 FrameLayout:
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
protected DecorView generateDecor(int featureId) {
....
return new DecorView(context, featureId, this, getAttributes());
}
为了初始化 DecorView 的结构,PhoneWIndow还需要通过 generateLayout 方法来加载具体的布局文件到 DecorView 中,具体的布局文件和系统版本以及主题有关。
protected ViewGroup generateLayout(DecorView decor) {
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
其其中 ID_ANDROID_CONTENT 的定义如下,这个id对应的就是 ViewGroup 的 mContentParent。
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
-
将 View 添加到 DecorView 的 mContentParent 中
这一步直接将 Activity 的视图添加到 DecorView 的 mContentParent 中即可:mLayoutInflater.inflate(layoutResID, mContentParent)。 -
回调 Activity 的 onCreateChanged 方法来通知 Activity 视图已经发生改变
由于 Activity 实现了 Window 的 Callback 接口,这里表示 Activity 的布局文件以及被添加到 DecorVIew 的 mContentParent 中了,于是需要通知 Acitivity 可以做相应的处理。 Activity 的 onContentChanged 方法是个空实现,我们可以在子 Activity 中处理这个回调。
@Override
public void setContentView(int layoutResID) {
....
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
...
}
经过上面的三个步骤,到这里为止 DecorView 以及被创建并初始化完毕,Activity 的布局文件也以及成功添加到了 DecorView 的 mContentParent 中,但是这个时候DecorView还没有被windowmanager添加到window中,这里需要正确的理解window的概念,window更多的是表示一种抽象的功能集合,虽然说早在activity的attch中window就已经被创建了,但是这个时候由于DecorView还没有被windowmanager识别,所有还不能提供具体的功能,因为他还无法接收外界的输入,在 ActivityThread 的 handleResumeActivity 方法,首先会调用 Acitivity 的onResume 方法没接调用 makeVisible 方法 。 在activityThread的makeVisible中,DecorView 才正在地完成了添加和显示两个过程,到这里 Activity 的视图才能被用户看到:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
8.3.2 Dialog 的 Window 创建过程
Dialog 的 Window 创建过程和 Activity 类似,有几个步骤:
- 创建 Window
public class Dialog implements....{
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
...
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//创建 PhoneWindow
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
- 初始化 DecorView 并将 Dialog 的视图添加到 DecorView 中
public void setContentView(@NonNull View view) {
mWindow.setContentView(view);
}
-
将 DecorView 添加到 Window 中并显示
在 Dialog 的 show 方法中,会通过 WindowManager 将 DecorView 添加到 Window 中。
public void show() {
...
mWindowManager.addView(mDecor, l);
mShowing = true;
当 Dialog 被关闭时,它会通过 WIndowManager 来移除 DecorView。
void dismissDialog() {
...
mWindowManager.removeViewImmediate(mDecor);
普通 Dialog 必须采用 Activity 的 Context,如果采用 Application 的Context,那么会报错。
Dialog dialog = new Dialog(this.getApplicationContext());
TextView textView = new TextView(this);
textView.setText("this is a toast");
dialog.setContentView(textView);
dialog.show()
错误信息提示:是没有应用 token 所导致,而应用 token 一般只有 Activity 拥有。另外系统 Window 它可以不需要 token,只需要设定一下 Window 的层级范围为 2000~2999即可:
mLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;//对应值:2010
...
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
8.3.3 Toast 的 Window 创建过程
Toast 也是基于 Window 来实现的,但是由于 Toast 具有定时取消这一功能,所以系统采用了 Handler。在 Toast 的内部有两类 IPC 过程。
- 第一类:Toast 访问 NotificaitonManagerService(简称:NMS)
- 第二类:NotificationManagerService 回调 Toast 里的 TN 接口
Toast 属于系统 Window,它内部的视图由两种方式指定:
- 一种是系统默认
- 一种是通过 setView 方法来指定一个自定义 View
他们都对应 Toast 的一个View 类型的内部成员 mNewView。Toast 提供了 show 和cancel ,他们内部是一个 IPC 过程。
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
public void cancel() {
mTN.hide();
try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// Empty
}
}
Toast 的 show 和 cancel 都要通过 NMS 来实现,NMS 运行在系统的进程中,所以只能通过远程调用的方式来显示和隐藏 Toast。TN 这个类是一个Binder 类,在 Toast 和 NMS 进行 IPC 的过程中,当 NMS 处理 Toast 的请求时会跨进程回调 TN 中的方法,由于 TN 运行在 Binder 线程池中,所以需要通过 Handler 将其切换当发送 Toast 请求所在的线程。这里使用 Handler ,意味着 Toast 无法在没有 Looper 的线程中弹出,因为 Handler 需要 Looper 才能完成切换线程的功能。
Toast 的 show 过程。它调用了 enqueneToast 方法:
INotificationManager service = getService();
String pkg = mContext.getPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
// 参数 :包名、远程回调、Toast 的时常
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
enqueneToast 首先将 Toast 请求封装为 ToastRecord 对象并将其添加到一个名为 mToastQuene 的队列中。mToastRecord 其实是一个 ArrayList。对于非系统应用来说,mToastQuene 中最多能同时存在 50 个 ToastRecord ,这样做是防止 Dos(Denial of Service),防止其他应用没有机会弹出 Toast。
此处无源码(要翻墙),摘一条重要的
final ToastRecord r = mToastQueue.get(i);
正常情况下,一个应用不可能达到上限,当ToastRecord被添加到mToastQueue中后,NMS就会通过showNextToastLocked方法来显示Toast,下面的代码很好理解,需要注意的是,Toast的显示是由ToastRecord的callback来完成的,这个callback实际上就是TN对象的远程Binder,通过callback来访问TN中的方法是需要跨进程来完成的,最终被调用的对象是TN中方法发起在Binder线程池中。
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 显示以后,NMS 还会通过 scheduleTimeoutLocked 方法来发送一个延时消息:
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);
}
LONG_DELAY 是3.5s,SHORT_DELAY 是 2s,NMS会通过 cancelToastLocked来隐藏 toast 并且清除队列
通过上面的分析,大家知道toast的显示隐藏实际上是toast的TN这个类来实现的,分别对应的show/hide,由于这两个方法都是NMS以跨进程的方式调用的,因此他运行在Binder池中,为了切换,在他们内部使用了handler:
private static class TN extends ITransientNotification.Stub {
final Runnable mShow = ()—>{ handleShow(); } };
final Runnable mHide = ()—> { handleHide();
mNextView = null;
}
};
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
上述代码中,mShow 和 mHide 是两个 Runnable,他们内部分别调用 handleShow 和 handleHide 方法。TN 的 handleShow 中会将 Toast 的视图添加到 Window 中:
public void handleShow() {
...
if (mView != mNextView) {
handleHide();
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
...
mWM.addView(mView, mParams);
...
}
}
而 TN 的handleHide 中会将 Toast 的视图从 Window 中移除:
public void handleHide() {
if (mView != null) {
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
mView = null;
}
}