《Android 开发艺术探索》 08-理解Window和WindowManager

抄书系列

Posted by Suzeyu on 2016-08-15

第八章: 所有的视图都是Windown呈现的, 那它都干了什么?

blog相关代码保存在第7章项目window包中

Window表示一个窗口的概念, 如有需要在桌面上显示一个类似悬浮窗的东西, 那么这种效果就需要Window来实现. Window是一个抽象类, 具体实现是PhoneWindow. 如果想要创建一个Window只需要通过WindowManager即可完成. WindowManager是外界访问Window的入口, Window具体实现位于WindowManagerService中, WMWMS的交互是一个IPC过程. Android中所有的视图都是通过Window来呈现的, 不管是Activity, Dialog, Toast他们的视图实际上都是附加在Window上的.

Window和WindowManager

先演示使用WindowManger添加一个Window.

public void addWindow(){
Button button = new Button(getApplicationContext());
button.setText("动态添加");
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
layoutParams.gravity = Gravity.LEFT ;
layoutParams.width = 400;
layoutParams.height = 300;
getWindowManager().addView(button, layoutParams);
}

Flag参数表示Window的属性,这些属性可以控制Window的显示特性. 下面是常用的属性:

  • FLAG_NOT_FOCUSABLE: 表示Window不需要获取焦点, 而不需要接收各种输入事件, 此标记会同时启动FLAG_NOT_TOUCH_MODAL, 最终事件会直接传递给下层的具有焦点的Window.
  • FLAG_NOT_TOUCH_MODAL: 这种模式下, 系统会将当前Window区域以外的点击事件传递给底层的Window, 当前Window区域以内的单击事件则自己处理. 这个标记很重要, 一般来说都需要开启此标记, 否则其他Window将无法接收到单击事件.
  • FLAG_SHOW_WHEN_LOCKED: 开启此模式可以让Window显示在锁屏的界面上.

Type参数表示Window的类型

Window共有三种类型, 分别是应用Window, 子Window, 系统Window. 应用类Window对应着一个Activity. 子Window不能单独存在, 他需要附属在特定的父Window中,比如常见的Dialog就是一个子Window. 系统Window是需要声明权限才能创建的Window, 比如Toast和系统状态栏都是系统的Window.

Window是分层的, 每个Window都有对应的z-ordered, 层级大的会覆盖在层级小的Window的上面, 这和HTML中的z-index的概念一样. 应用Window的层级范围是1~99, 子Window的层级范围是1000~1999, 系统Window的层级范围是2000~2999.

如果想要在最顶层显示, 可以选择使用TYPE_SYSTEM_OVERLAY, TYPE_SYSTEM_ERROR. 如果采用了TYPE_SYSTEM_ERROR同时要声明权限android.permission.SYSTEM_ALERT_WINDOW. 如果不声明那么在创建的时候就会报错.

WindowManager常用的功能

ViewManager接口中定义了三个方法. 就是我们常用的方法添加View,删除View,修改View. WM继承了这个接口.

Window的内部机制

Window是一个抽象的概念, 每一Window都对应着一个View和一个ViewRootImpl, Window和View通过ViewRootImpl来建立联系, 因此Window并不是实际存在的, 他是以View的形式存在. 通过WindowManager的定义和提供的三个接口方法看出都是针对View的. 说明View才是Windwo存在的实体. 而在实际的使用中无法直接访问Window, 对Window的访问都是必须通过WM.

Window的添加过程

Window的添加过程需要通过WindowManageraddView()来实现, 而WindowManager是一个接口, 它的真正实现是WindowManagerImpl类, 在WindowManagerImpl中Window的三大操作如下.

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, 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并没有直接实现Window的三大操作, 而是全部交给了WindowManagerGlobal来处理. WindowManagerGlobal以工厂的形式向外提供自己的实例. 而WindowManagerImpl这种工作模式就典型的桥接模式, 将所有的操作全部委托给WindowManagerGlobal来实现.

WindowManagerGlobal的addView()主要分为

  1. 检查所有参数是否合法, 如果是子Window那么还需要调整一些布局参数.
  2. 创建ViewRootImpl并将View添加到列表中.
  3. 通过ViewRootImpl来更新界面并完成Window的添加过程. 这个过程是通过ViewRootImpl#setView()来完成的. View的绘制过程是由ViewRootImpl来完成的, 在内部会调用requestLayout()来完成异步刷新请求. 而scheduleTraversals()实际上是View绘制的入口. 接着会通过WindowSession完成Window的添加过程(Window的添加过程是一次IPC调用). 最终会通过WindowManagerService来实现Window的添加.

WindowManagerService内部会为每一个应用保留一个单独的Session.

Window的删除过程

Window 的删除过程和添加过程一样, 都是先通过WindowManagerImpl后, 在进一步通过WindowManagerGlobal的removeView()来实现的.

public void removeView(View view, boolean immediate) {
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);
}
}
}

这里通过ViewRootImpldie()完成来完成删除操作. die()方法只是发送了请求删除的消息后就立刻返回了, 这个时候View并没有完成删除操作, 所以最后会将其添加到mDyingViews中, mDyingViews表示待删除的View的列表.

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(TAG, "Attempting to destroy the window while drawing!\n" +
" window=" + this + ", title=" + mWindowAttributes.getTitle());
}
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}

die方法中只是做了简单的判断, 如果是异步删除那么就发送一个MSG_DIE的消息, ViewRootImpl中的Handler会处理此消息并调用doDie(); 如果是同步删除, 那么就不发送消息直接调用doDie()方法.

doDie()方法中会调用dispatchDetachedFromWindow()方法, 真正删除View的逻辑在这个方法内部实现. 其中主要做了四件事:

  1. 垃圾回收的相关工作, 比如清除数据和消息,移除回调.
  2. 通过Session的remove方法删除Window: mWindowSession.remove(mWindow), 这同样是一个IPC过程, 最终会调用WMS的removeWindow()方法.
  3. 调用View的dispatchDetachedFromWindow()方法, 内部会调用View的onDetachedFromWindow()以及onDetachedFromWindowInternal(). 而对于onDetachedFromWindow()就是在View从Window中移除时, 这个方法就会被调用, 可以在这个方法内部做一些资源回收的工作. 比如停止动画,停止线程
  4. 调用WindowManagerGlobal#doRemoveView方法刷新数据, 包括mRoots, mParams, mDyingViews, 需要将当前Window所关联的这三类对象从列表中删除.

Window的更新过程

WindowManagerGlobal#updateViewLayout()方法做的比较简单, 它需要更新View的LayoutParams并替换掉老的LayoutParams, 接着在更新ViewRootImpl中的LayoutParams. 这一步主要是通过setLayoutParams()方法实现.

ViewRootImpl中会通过scheduleTraversals()来对View重新布局, 包括测量,布局,重绘. 除了View本身的重绘以外, ViewRootImpl还会通过WindowSession来更新Window的视图, 这个过程最后由WMSrelayoutWindow()实现同样是一个IPC过程.

Window的创建过程

View是Android中视图的呈现方式, 但是View不能单独存在, 它必须依附在Window这个抽象的概念上面, 因此有视图的地方就有Window.

Activity的Window创建过程

Activity的大体启动流程: 最终会由ActivityThread中的PerformLaunchActivity()来完成整个启动过程, 这个方法内部会通过类加载器创建Activity的实例对象, 并调用其attach()方法为其关联运行过程中所依赖的一系列上下文环境变量. 代码如下:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
Activity activity = null;
//获得类加载器
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
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);
return activity;
}

attach()方法里, 系统会创建Activity所属的Window对象并为其设置回调接口, Window对象的创建是通过PolicyManager#makeNewWindow()方法实现. 由于Activity实现了Window的CallBack接口, 因此当Window接收到外界的状态改变的时候就会回调Activity方法. 比如说我们熟悉的onAttachedToWindow(), onDetachedFromWindow(), dispatchTouchEvent()等等.代码如下

mWindow = new PhoneWindow(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);
}
mUiThread = Thread.currentThread();

那么Activity视图是怎么附属在Window上的呢? 查看经常使用的setContentView()方法干了什么

public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

Activity将具体实现交给了Window处理, 而Window的具体实现就是PhoneWindow, 所以只需要看PhoneWindow的相关逻辑分为以下几步.

  1. 如果没有DecorView, 那么就创建它. 由installDecor()-->generateDecor()触发
  2. 将View添加到DecorViewmContentParent
  3. 回调Activity的onContentChanged()通知activity视图已经发生改变

这个时候DecorView已经被创建并初始化完毕, Activity的布局文件也已经添加成功到DecorView的mContentParent中. 但是这个时候DecorView还没有被WindowManager正式添加到Window中. 虽然早在Activity的attach方法中window就已经被创建了, 但是这个时候由于DecorView并没有被WindowManager识别, 所以这个时候的Window无法提供具体功能, 因为他还无法接收外界的输入信息.

ActivityThread#handleResumeActivity()方法中, 首先会调用Activity#onResume(), 接着会调用Activity#makeVisible(), 正是在makeVisible方法中, DecorView真正的完成了添加和显示这两个过程. 如下:

void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}

Dialog的Window创建过程

DialogWindow的创建过程和Activity类似, 有如下几步

1. 创建Window

Dialog的创建后的实际就是PhoneWindow, 这个过程和Activity的Window创建过程一致.

2. 初始化DecorView并将Dialog的视图添加到DecorView中

这个过程也类似, 都是通过Window去添加指定的布局文件.

3. 将DecorView添加到Window中并显示

Dialog的show方法中, 会通过WindowManagerDecorView添加Window中.

mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();

普通的Dialog有一个特殊之处, 那就是必须采用Activity的Content, 如果采用Application的Content, 那么就会报错. 报的错是没有应用token所导致的, 而应用token一般只有Activity才拥有.

还有一种方法. 系统Window比较特殊, 他可以不需要token, 因此只需要指定对话框的Window为系统类型就可以正常弹出对话框.

//JAVA 给Dialog的Window改变为系统级的Window
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
//XML 声明权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

Toast的Window创建过程

ToastDialog不同, 它的工作过程就稍显复杂. 首先Toast也是基于Window来实现的. 但是由于Toast具有定时取消的功能, 所以系统采用了Handler. 在Toast的内部有两类IPC过程, 第一类是Toast访问NotificationManagerService()后面简称NMS. 第二类是NotificationManagerService回调Toast里的TN接口.

Toast属于系统Window, 它内部的视图有两种方式指定, 一种是系统默认的样式, 另一种是通过setView方法来指定一个自定义View. 不管如何, 他们都对应Toast的一个View类型的内部成员mNextView. Toast内部提供了cancelshow两个方法. 分别用于显示和隐藏Toast. 他们内部是一个IPC过程.

显示和隐藏Toast都是需要通过NMS来实现的. 由于NMS运行在系统的进程中, 所以只能通过远程调用的方式来显示和隐藏Toast. 而TN这个类是一个Binder类. 在ToastNMS进行IPC的过程中, 当NMS处理Toast的显示或隐藏请求时会跨进程回调TN的方法. 这个时候由于TN运行在Binder线程池中, 所以需要通过Handler将其切换到当前主线程. 所以由其可知, Toast无法在没有Looper的线程中弹出, 因为Handler需要使用Looper才能完成切换线程的功能.

if (!isSystemToast) {
int count = 0;
final int N = mToastQueue.size();
for (int i=0; i<N; i++) {
final ToastRecord r = mToastQueue.get(i);
if (r.pkg.equals(pkg)) {
count++;
// MAX_PACKAGE_NOTIFICATIONS == 50
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}

对于非系统应用来说, 最多能同时存在对Toast封装的ToastRecord上限为50个. 这样做是为了防止DOS(Denial of Service). 如果不这样, 当通过大量循环去连续的弹出Toast, 这将会导致其他应用没有机会弹出Toast, 那么对于其他应用的Toast请求, 系统的行为就是拒绝服务, 这就是拒绝服务攻击的含义.

ToastRecord被添加到mToastQueue()中后, NMS就会通过showNextToastLocked()方法来显示当前的Toast.

Toast的显示是由ToastRecordcallback来完成的. 这个callback实际上就是Toast中的TN对象的远程Binder. 通过callback来访问TN中的方法是需要跨进程的. 最终被调用的TN中的方法会运行在发起Toast请求的应用的Binder线程池.

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);
}

如上代码就是在Toast显示以后, NMS通过这个方法来发送一个延时消息, 具体取决Toast的时长. LONG_DELAY, SHORT_DELAY分别对应着3.5秒和2秒. 当延时时间达到的时候. NMS会通过cancelToastLocked()方法来隐藏Toast并将其从mToastQueue中移除, 这个时候如果mToastQueue中还有其余Toast那么NMS就继续显示其他.

Toast的隐藏也会通过ToastRecord的callback完成的.同样是一次IPC过程. 方式和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
}
mToastQueue.remove(index);
keepProcessAliveLocked(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的显示和影响过程实际上是通过Toast中的TN这个类来实现的. 他有两个方法show(), hide(). 分别对应着Toast的显示和隐藏. 由于这两个方法是被NMS以跨进程的方式调用的, 因此他们运行在Binder线程池中. 为了将执行环境切换到Toast请求所在线程中, 在他们内部使用了handler,如下

@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}

上面代码中, mShow, mHide是两个Runnable, 他们内部分别调用了handleShowhandleHide方法. 所以这两个方法才是真正完成隐藏和显示Toast的地方.

TNhandleShow中会将Toast的视图添加到Window中.

TNhandleHide中会将Toast的视图从Window中移除.

具体实现代码如下:

//handleShow()
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
mWM.addView(mView, mParams);
//handleHide()
if (mView != null) {
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
mView = null;
}

关于Toast流程已经完事. 除了说到的Activity, Dialog, Toast. 还有PopupWindow
菜单栏, 状态栏都是通过Window来实现的.

下一章: 第9章: 四大组件的工作过程