Android Window和WindowManager

荀辰钊
2023-12-01

Window 是一个抽象类,它的具体实现是 PhoneWindow。创建一个Window是很简单的事情,只需要通过 WindowManager 即可完成。WindowManager是外界访问Window的入口,Window的具体实现位于WindowManagerService中,WindowManager和WindowManagerService的交互是一个IPC过程。

Android中所有的视图都是通过Window来呈现的,不管是Activity、Dialog、Toast,它们的视图实际上都是附加在Window上,因此Window实际是View的直接管理者。

1 Window和WindowManager

// WindowManager添加一个Window的操作过程
Button button = new Button(this);
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
	LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);
lp.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL 
	| LayoutParams.FLAG_NOT_FOCUSABLE 
	| LayoutParams.FLAG_SHOW_WHEN_LOCKED;
lp.gravity = Gravity.LEFT | Gravity.TOP;
lp.x = 100;
lp.y = 300;
mWindowManager.addView(button, lp);

WindowManager.LayoutParams.flags 参数表示Window的属性,通过这些选项可以控制Window的显示特性,主要介绍几个比较常用的选项:

  • FLAG_NOT_FOCUSABLE

表示Window不需要获取焦点,也不需要接收各种输入事件,此标记会同时启用 FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层的具有焦点的Window

  • FLAG_NOT_TOUCH_MODAL

在此模式下,系统会将当前Window区域以外的单击事件传递给底层Window,当前Window区域以内的单击事件则自己处理。这个标记很重要,一般来说都需要开启此标记,否则其他Window将无法接收到单击事件

  • FLAG_SHOW_WHEN_LOCKED

开启此模式可以让Window显示在锁屏的界面上

WindowManager.LayoutParams.type 参数表示Window的类型,Window有三种类型,分别是应用Window、子Window和系统Window。应用类Window对应着一个Activity;子Window不能单独存在,它需要附属在特定的父Window中,比如Dialog;系统Window需要声明权限才能创建的Window,比如Toast和系统状态栏都是系统Window。

Window是分层的,层级大的会覆盖层级小的Window。

  • 应用Window层级范围:1~99

  • 子Window层级范围:1000~1999

  • 系统Window层级范围:2000~2999

系统层级有跟多值,一般我们可以选用 TYPE_SYSTEM_OVERLAY 或者 TYPE_SYSTEM_ERROR,并且声明权限:

// TYPE_SYSTEM_ERROR已被废弃,使用WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
WindowManager.LayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;

<used-permission android:name="android.permission.SYSTEM_ALERT_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);
}

对于开发者来说,WindowManager常用的就只有这三个功能而已,但是这三个功能已经足够使用了。创建一个Window并向其添加View、更新Window中的View,如果想删除Window,那么只需要删除它里面的View即可。由此看来,WindowManager操作Window的过程更像是在操作Window中的View。

// 拖动Window的效果,只需要根据手指的位置来设定LayoutParams中的x和y的值即可改变Window位置
mFloatingButton.setOnTouchListener(this);

public boolean onTouch(View v, MotionEvent event) {
	int rawX = (int) event.getRawX();
	int rawY = (int) event.getRawY();
	switch (event.getAction()) {
		case MotionEvent.ACTION_MOVE:
			mLayoutParams.x = rawX;
			mLayoutParams.y = rawY;
			mWindowManager.updateViewLayout(mFloatingButton, mLayoutParams);
			break;
	}
	return false;
}

2 Window的内部机制

Window 是一个抽象概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl建立联系,因此Window并不是实际存在的,它是以View的形式存在。这点从WindowManager提供的三个接口方法 addViewupdateViewLayoutremoveView 可以看出,View才是Window存在的实体。

2.1 Window的添加过程

Window 的操作入口是WindowManager,而WindowManager的真正实现是 WindowManagerImpl

// 可以发现,WindowManagerImpl没有直接实现Window的三大操作,而是全部交给WindowManagerGlobal处理
@Override
public void addView(View view, ViewGroup.LayoutParams params) {
	mGlobal.addView(view, params, mDisplay, mParentWindow);
}

@Override
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
	mGlobal.updateViewLayout(view, params);
}

@Override
public void removeView(View view) {
	mGlobal.removeView(view, false);
}

WindowMangerGlobal 的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) {
	// 添加的是子Window
	parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
  • 创建ViewRootImpl并将View添加到列表中
// WindowManagerGlobal内部比较重要的集合列表

// 存储所有Window所对应的View
private final ArrayList<View> mViews = new ArrayList<>();
// 存储所有Window所对应的ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<>();
// 存储所有Window所对应的布局参数
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<>();
// 存储正在被删除的View对应,即那些已经调用removeView方法但删除操作还未完成的Window对象
private final ArraySet<View> mDyingViews = new ArraySet<>();

// addView方法添加View
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
  • 通过ViewRootImpl来更新界面并完成Window的添加过程
// 这个步骤由ViewRootImpl的setView方法完成
// setView内部会通过requestLayout来完成异步刷新请求,scheduleTraversals绘制View,对View进行测量、布局、绘制过程
public void requestLayout() {
	if (!mHandingLayoutInLayoutRequest) {
		checkThread();
		mLayoutRequested = true;
		scheduleTraversals();
	}
}

// 接着通过WindowSession完成Window的添加过程
// mWindowSession的类型是IWindowSesssion,它是一个Binder对象,真正的实现类是Session
// Window的添加过程是一个IPC调用
try {
	mOriginWindowType = mWindowAttributes.type;
	mAttachInfo.mRecomputeGlobalAttributes = true;
	collectViewAttributes();
	res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
		getHostVisibility(), mDisplay.getDisplayId(), 
		mAttachInfo.mContentInsets, 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);
}

// Session内部会通过WindowManagerService来实现Window的添加
// WindowManagerService内部会为每一个应用保留一个单独的Session
public int addToDisplay(Window window, int seq, WindowManager.LayoutParams attr,
	int viewVisibility, int displayId, Rect outContentInsets, InputChannel outInputChannel) {
	return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
		outContentInsets, outInputChannel);
}

2.2 Window的删除过程

Window的删除过程和添加过程流程走向是一样的。

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索引
		View curView = mRoots.get(index).getView();
		removeViewLocked(index, immediate); // 删除View
		if (currView == view) return;
		throw new IlleagelStateException("Calling with view " + view + 
			"but the ViewAncestor is attached to " + curView);
	}
}

// 在WindowManager中提供了两种删除接口removeView和removeViewImmediate
// 分别表示异步和同步删除,一般不需要使用removeViewImmediate删除Window以免发生意外错误
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); // 交由ViewRootImpl删除
	if (view != null) {
		view.assignParent(null);
		if (deffered) {
			mDyingViews.add(view); // 异步删除,添加到待删除列表
		}
	}
}

boolean die(boolean immediate) {
	if (immediate && !mIsTraversal) {
		doDie();
		return false;
	}

	if (!mIsDrawing) {
		destroyHardwareRenderer();
	} else {
		Log.e(...);
	}
	// 异步删除只发送了一条消息,Handler会调用doDie()
	// doDie()内部会调用dispatchDetachedFromWindow()真正删除View
	mHandler.sendEmptyMessage(MSG_DIE); 
	return true;
}

dispatchDetachedFromWindow 主要做四件事:

  • 垃圾回收相关工作,比如清除数据和消息、移除回调

  • 通过Session的remove()删除Window;mWindowSession.remove(mWindow),同样是一个IPC过程,最终会调用 WindowManagerService.removeWindow()

  • 调用View的 dispatchDetachedFromWindow(),在内部会调用View的 onDetachedFromWindow() 以及 onDetachedFromWindowInternal()

  • 调用WindowManagerGlobal的 doRemoveView() 刷新数据,包括mRoots、mParams以及mDyingViews,需要将当前Window所关联的这三类对象从列表中删除

2.3 Window的更新过程

// Window的更新过程比较简单,首先更新View的LayoutParams,再更新ViewRootImpl的LayoutParams
// 然后ViewRootImpl通过scheduleTraversals()重新测量、布局、重绘
// ViewRootImpl还会通过WindowSession更新Window视图,最终是由WindowManagerService的relayoutWindow()实现更新。
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);
	}
}

2.4 总结操作Window的流程走向

无论是添加、删除还是更新都是走这个流程,最终都会由 WindowManagerService 处理。

WindowManager(WindowManagerImpl) -> WindowManagerGlobal -> ViewRootImpl -> WindowSession (Session) -> WindowManagerService

3 Window的创建过程

这里只会简述Activity、Dialog和Toast的Window创建过程,只分析流程不具体深入代码实现。

3.1 Activity的Window创建过程

Activity中的Window通过 PolicyManagermakeNewWindow() 完成,makeNewWindow() 创建的是 PhoneWindow。此时Activity的Window就创建完成了,下面分析Activity的视图怎么附属在Window上。

Activity的视图由 setContentView() 提供,Activity会在这个方法将具体实现交给PhoneWindow,PhoneWindow的 setContentView() 大致遵循如下几个步骤(setContentView的具体流程):

  • 如果没有DecorView,就创建DecorView

DecorView 是一个FrameLayout,是Activity中的顶级View,一般来说它的内部包含标题栏和内容栏,内容固定id为是 android.R.id.content。DecorView的创建过程由 installDecor() 完成,方法内部会通过 generateDecor() 直接创建DecorView,这个时候DecorView还是一个空白的FrameLayout。

protected DecorView generateDecor() {
	return new DecorView(getContext(), -1);
}

为了初始化DecorView结构,PhoneWindow还需要通过 generateLayout() 加载具体的布局到DecorView中。

View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
// public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content
ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
  • 将View添加到DecorView的mContentParent中

步骤1已经创建了DecorView,因此这一步直接将Activity的视图添加到DecorView的 mContentParent 中:mLayoutInflater.inflate(layoutResID, mContentParent)

从这里可以理解Activity的setContentView方法的来历了,因为Activity的布局文件只是被添加到DecorView的mContentParent中,叫setContentView比setView更合适。

  • 回调Activity的onContentChanged方法通知Activity视图已经发生改变

通知Activity做相应的回调处理。

但此时DecorView还没有被WindowManager添加到Window中。

在ActivityThread的 handleResumeActivity() 中,首先会调用Activity的 onResume(),接着调用Activity的 makeVisible(),正式在makeVisible(),DecorView真正完成了添加和显示这两个过程,到这里Activity的视图才能被用户看到。

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

总结一下Activity的Window创建流程:

Activity进行启动流程,通过 PolicyManager 创建出 PhoneWindow -> 创建 DecorView(空白的FrameLayout) -> PhoneWindow 加载具体的布局到 DecorView(标题栏和内容栏) -> DecorView 填充布局内容View -> Activity回调onResume,WindowManager 将DecorView添加到PhoneWindow中完成显示

3.2 Dialog的Window创建过程

Dialog的Window创建过程和Activity类似。

  • 创建Window(PhoneWindow)

也是通过 PolicyManager 创建PhoneWindow

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

这个过程也和Activity的 setContentView() 类似,创建DecorView,PhoneWindow加载布局到DecorView,DecorView通过布局文件填充内容View

  • 将DecorView添加到Window中并显示

在Dialog的 show() 中,会通过WindowManager将DecorView添加到Window中:

mWindowManager.addView(mDecor, 1);
mShowing = true;

当Dialog被关闭时,会通过WindowManager移除DecorView:

mWindowManager.removeViewImmediate(mDecor);

普通的Dialog有一个特殊之处,就是必须采用Activity的Context,如果采用Application的Context就会报错提示找不到token,因为token一般只有Activity拥有。

3.3 Toast的Window创建过程

Toast 也是基于Window来实现的,但是由于Toast具有定时取消这一功能,所以系统采用了Handler。在Toast内部有两类IPC过程,第一类是Toast访问 NotificationManagerService(NMS),第二类是NotificationManagerService回调Toast里的TN接口。

Toast属于系统Window,它内部的视图由两种方式指定,一种是系统默认样式,另一种是通过 setView() 指定一个自定义View,它们都对应Toast的一个View类型的内部成员 mNextView。Toast提供 show()cancel() 显示和隐藏Toast,它们内部是一个IPC过程。

public void show() {
	if (mNextView == null) throw new RuntimeException("setView must have been called");
	INotificationManager service = getService(); // Binder, NMS
	String pkg = mContext.getOpPackageName();
	// Binder,可以把TN理解为Toast和NMS进行IPC时的回调
	// Toast和NMS进行IPC过程中,当NMS处理Toast的现实或隐藏请求时会跨进程回调TN中的方法
	// TN运行在Binder线程池中,需要通过Handler将其切换到当前线程中
	// 这里的当前线程是指发送Toast请求所在的线程
	// 由于这里使用了Handler,意味着Toast无法在没有Looper的线程中弹出
	TN tn = mTN; 
	tn.mNextView = mNextView;

	try {
		// enqueueToast将Toast请求封装为ToastRecord对象并添加其添加到mToastQueue队列中
		// 对于非系统应用,mToastQueue最多能同时存在50个ToastRecord
		service.enqueueToast(pkg, tn, mDuration);
	} catch (RemoteException e) {}
}

public void cancel() {
	mTN.hide();

	try {
		getService().cancelToast(mContext.getPackageName(), mTN);
	} catch (RemoteException e) {}
}

// 当ToastRecord被添加到mToastQueue后,NMS会通过showNextToastLocked显示当前的Toast
void showNextToastLocked() {
	ToastRecord record = mToastQueue.get(0);
	while (record != null) {
		try {
			// callback实际上就是Toast中的TN对象的远程Binder
			// 最终被调用的TN中的方法会运行在发起Toast请求的应用的Binder线程池中
			record.callback.show();
			// 发送延时消息隐藏Toast
			scheduleTimeoutLocked(record);
			return;
		} catch (RemoteException e) {}
	}
}

private void scheduleTimeoutLocked(ToastRecord r) {
	mHandler.removeCallbackAndMessages(r);
	Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
	long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
	mHandler.sendMessageDelayed(m, delay);
}

try {
	// 接收scheduleTimeoutLocked的延时消息隐藏Toast,也是一次IPC过程
	record.callback.hide();
} catch (RemoteException e) {}

// TN有两个方法show和hide,这两个方法是被NMS以跨进程方式调用,因此它们运行在Binder线程池中
// 为了将执行环境切换到Toasst请求所在的线程,内部使用了Handler
// mShow和mHide是Runnable,内部分别调用了handleShow()和handleHide()真正完成显示和隐藏Toast
@Override
public void show() {
	mHandler.post(mShow);  
}

@Override
public void hide() {
	mHandler.post(mHide);
}

// Toast显示和隐藏最终会将Toast视图添加到Window中
// handleShow()
mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mWM.addView(mView, mParams);

// handleHide()
if (mView.getParent() != null) {
	mWM.removeView(mView);
}
 类似资料: