每一次的不得意和不痛快都会让你得到成长,不可能都是一帆风顺的。最近的我也很烦躁,工作很久了,有时
候很多从事的都不是我喜欢的,盲目的搬砖,作为底层员工你只能跟着公司的大方向走,但是我也是一个单独
的个体有时候大家也要多位自己想想。
问题
之前说过,要给大家讲讲Window和View的关系,有点忽略了,这里补上,首先抛出几个问题问题:
- window和view的关系是什么样的?
- window怎么设置大小呢,什么时候需要设置大小呢?
- 事件是window和view的传递,我们能跨window透传吗?
真实场景
我们日常开发中很少会遇到直接合Window打招呼的,就算有,Dialog和DialogFragment就可以满足我们的日常开发需求,但有时候我们也是需要自定义的,比如产品就曾经给我提过一个需求:
需求:IM聊天,当消息来的时候,需要在app内部,从顶部往下弹一个悬浮窗下来,如果页面切换,也要保证这个弹窗不能被盖住,依旧虚浮在最上方,而且,这个弹窗还可以实现拖拽效果。
分析:第一想法,就是在Activity的Decorview中添加这个卡片View,但是当页面切换的时候,是会被盖住的,所以直接就被pass了,只能通过单独定义一个window悬浮在APP内。Android的api中,windowmanager就提供了这个能力。
后面我会告诉大家怎么实现这个功能,现在我们先进入正题。
初识WindowManager
-
addView(View view, ViewGroup.LayoutParams params)
通过这个方法,根据传递来的View和布局参数,创建一个window,这个View你可以理解就是Activity的Decorview,为什么Activity需要Decorview,其实也是为了方便开发者调用,没有什么特别重要的含义。
-
updateViewLayout(View view, ViewGroup.LayoutParams params)
可以改变window的布局参数,改变大小和位置
-
removeView(View view)
移除这个window
接下里我们分析这个三个方法,梳理一下流程,看看WindowManager的实现类WindowManagerImpl的操作(其实没有什么特别好的办法,只能自己去梳理一下源代码,看看Android是怎么实现的,慢慢来)
addView分析
// 是一个单例
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// 交给了WindowManagerGlobal来处理
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
接下来我们看看这个类WindowManagerGlobal,着重注意一下这几个成员,通过这几个成员,我们可以实现一些风骚的操作,比如实现window的时间传递,后面会提及。
// window内部对应的view
private final ArrayList<View> mViews = new ArrayList<View>();
// window对应的ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
// window对应的布局参数Params
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
// 被删除的window对应的View,调用了windowmanager的removeView方法
private final ArraySet<View> mDyingViews = new ArraySet<View>();
接下来,粘贴出来addview的核心代码
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
ViewRootImpl root;
View panelParentView = null;
// 创建一个ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
// 一次添加View,ViewRootImpl以及Params
// 同时他们的顺序是保持一致的
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
// 通过ViewRootImpl将View和布局参数关联
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
接下来,我们看看这个setView方法干了什么,我们核心关注的就是这个Window是怎么创建的,view是怎么绘制的。 以下是核心代码。(其实代码太多了,各种属性,我也不是都很懂,但是我知道我关注的就是这个window怎么创建的,大小怎么控制的,以及view的绘制是怎么操作的,能达到我的目的就是OK的)
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView){
// 这个attrs里面有我们设置的布局参数,把布局参数拷贝一份
mWindowAttributes.copyFrom(attrs);
// 发现了,这个就是view绘制的入口,实现view显示的刷新
requestLayout();
// 这个方法就是实现了window的创建
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
}
mWindowSession是IWindowSession类型的,我们需要关注这个IWindowSession(www.androidos.net.cn/sourcecode 这个是Android源码观看的在线链接)发现这个类其实是一个AIDL,是一次的IPC过程(关于IPC后续我也会讲解,进程的同学后续也会讲解),那么需要关着这个binder对象的真正实现类,来看。
首先我们看看mWindowSession的初始化,一步步来
mWindowSession = WindowManagerGlobal.getWindowSession();
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
InputMethodManager imm = InputMethodManager.getInstance();
// 获得IWindowManager的binder代理对象,看下面的方法
IWindowManager windowManager = getWindowManagerService();
// 调用binder的方法,获取IWindowSession的binder对象
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
},
imm.getClient(), imm.getInputContext());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowSession;
}
}
public static IWindowManager getWindowManagerService() {
synchronized (WindowManagerGlobal.class) {
if (sWindowManagerService == null) {
// 发现有时一次IPC的操作,获取IWindowManager的binder代理对象
// 后面我讲插件化的时候会给大家详细讲解这个ServiceManager的,你可以理解,它就是一 个注册IBinder的map,实现IPC好多都是走这一套
sWindowManagerService = IWindowManager.Stub.asInterface(
ServiceManager.getService("window"));
try {
if (sWindowManagerService != null) {
ValueAnimator.setDurationScale(
sWindowManagerService.getCurrentAnimatorScale());
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowManagerService;
}
}
这里面涉及到了好几次IPC操作,总结一下:
- 首先,通过IPC获取WindowManagerService对象(WindowManagerService实现了IWindowManager.Stub,那么她就是一个IWindowManager的binder对象)
- 调用WindowManagerService的openSession方法,会创建一个Session对象,这个Session实现了IWindowSession.Stub,很鲜染,Session也是一个binder。这个Session就是ViewRootImpl的mWindowSession的具体实现binder
然后,ViewRootImpl或得到Session的Binder之后,开始执行addToDisplay方法
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel) {
// 最后还是交给了WindowManagerService来处理创建window的过程
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel);
}
最后,WindowManagerService代码太多,就不一一分析了,对token和window的类型做了很多的判断,每一个window都有一个windowstate保存其状态,有着window的各种信息。后面会调用WindowManagerPolicy的prepareAddWindowLw准备window,这个就看不到源代码了
其实吧,Activity,dialog,里面的window都是一个虚拟的概念,真正起作用的还是view,以及边界尺寸,有了window这个中间者,方便理解的同时,还可以实现解耦的操作,包括事件分发。应为最后渲染,窗口还是会分配surface,还是通过surface(这一块我不是很懂,还需要学习)完成渲染,而不是窗口本身。这里是我的理解,后续,随着我的深入,我会做更多的讲解。
updateView
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// 同样交给这个单例对象来操作
mGlobal.updateViewLayout(view, params);
}
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的布局参数
view.setLayoutParams(wparams);
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
// 这个方法内部调用了scheduleTraversals,会重新触发测量和布局
root.setLayoutParams(wparams, false);
}
}
removeView
同样,我们需要看mGlobal.removeView即可
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
// 在mViews集合中,找到要删除的view索引
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);
}
}
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());
}
}
// 默认immediate是false,通过异步来实现删除操作
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
// 把要删除的View放在mDyingViews集合中
mDyingViews.add(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(mTag, "Attempting to destroy the window while drawing!\n" +
" window=" + this + ", title=" + mWindowAttributes.getTitle());
}
// 异步删除,发送一个消息,通过handler来切换
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
不管是同步还是通过handler切换,都会走到doDie方法
void doDie() {
checkThread();
if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
synchronized (this) {
if (mRemoved) {
return;
}
mRemoved = true;
if (mAdded) {
// 这个方法,等下会讲一下
dispatchDetachedFromWindow();
}
if (mAdded && !mFirst) {
destroyHardwareRenderer();
if (mView != null) {
int viewVisibility = mView.getVisibility();
boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
if (mWindowAttributesChanged || viewVisibilityChanged) {
// If layout params have been changed, first give them
// to the window manager to make sure it has the correct
// animation info.
try {
if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
& WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
// 有事一次IPC调用,和动画有关,不是我们关注的
mWindowSession.finishDrawing(mWindow);
}
} catch (RemoteException e) {
}
}
// 资源释放
mSurface.release();
}
}
mAdded = false;
}
// 把mViews,mRoots,mDyingViews中,保留这个这个View相关的数据,清除掉
// 之前提到的比较重要的几个集合,看来他们的生命周期是绑定的
WindowManagerGlobal.getInstance().doRemoveView(this);
}
这个方法,就是真正的删除逻辑
void dispatchDetachedFromWindow() {
mFirstInputStage.onDetachedFromWindow();
if (mView != null && mView.mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
// 这个方法,大家应该比较熟悉,我们经常会监听这个方法,实现自己的资源释放
mView.dispatchDetachedFromWindow();
}
// 下面大多都是一些资源和引用的释放工作,移除回调
mAccessibilityInteractionConnectionManager.ensureNoConnection();
mAccessibilityManager.removeAccessibilityStateChangeListener(
mAccessibilityInteractionConnectionManager);
mAccessibilityManager.removeHighTextContrastStateChangeListener(
mHighContrastTextManager);
removeSendWindowContentChangedCallback();
destroyHardwareRenderer();
setAccessibilityFocus(null, null);
mView.assignParent(null);
mView = null;
mAttachInfo.mRootView = null;
mSurface.release();
if (mInputQueueCallback != null && mInputQueue != null) {
mInputQueueCallback.onInputQueueDestroyed(mInputQueue);
mInputQueue.dispose();
mInputQueueCallback = null;
mInputQueue = null;
}
if (mInputEventReceiver != null) {
mInputEventReceiver.dispose();
mInputEventReceiver = null;
}
try {
mWindowSession.remove(mWindow);
} catch (RemoteException e) {
}
// Dispose the input channel after removing the window so the Window Manager
// doesn't interpret the input channel being closed as an abnormal termination.
if (mInputChannel != null) {
mInputChannel.dispose();
mInputChannel = null;
}
mDisplayManager.unregisterDisplayListener(mDisplayListener);
// 移除回调和屏障消息(关于这个屏障消息,后续分析handler会给大家讲解,目的就是为了加快UI的渲 // 染)
unscheduleTraversals();
}
关于,mWindowSession.remove(mWindow)适合addView相似的,最终也会低啊用WindowManagerService的方法removeWindow,最后会调用WindowManagerPolicy的removeWindowLw的方法。
小结
总体来说,流程基本是疏通了,但是到了WindowManagerPolicy这个地方,我就有点找不到代码了(应该是功力不够,后续我又学习到,会持续补充的)这就可能会涉及到更底层,更细节的地方了。
大家需要知道下面几点就足够了
- window的创建流程,经历了和WMS的IPC操作
- view,window,ViewRootImpl的关系
接下来,我们回到最开始提的几个问题:
- window和view的关系是什么样的?
Android的window是一个虚拟的概念,核心还是View的显示,通常理解可以是一个layer,或者一个显示的约束和边界,核心的渲染还是通过surface来实现。一个window就对应着一个view树,我们方便通过window来管理view,管理屏幕上显示的各个view树,他们的事件分发,以及他们的层级等(每个window的优先级是不一样的,优先级高的会在最上方显示),以及通过在window级别的token校验。可能有点抽象,简单理解就是,window是一个班级的老师,管理这个班级,但是班级的平均成绩适合学生有关的(学生的分数/学生人数)。
- window怎么设置大小呢,什么时候需要设置大小呢?(这个可以不用特别关心,到时候查api即可)
window是可以约束边界的,默认不需要我们来改变,windowmanager的window大小,通过布局参数来改变,当你需要全屏或者其他情况,需要定制你的UI的时候可以控制,以及我想要拦截屏幕的所有事件,也可以通过全屏来拦截。(如果不是全屏,触摸到其他位置,事件根本就不会来到你的winow,怎么拦截)。
- 事件是window和view的传递,我们能跨window透传吗?
默认情况下,点击事件只会在当前window传递,如果能随便跨window就会出问题了(打开新的activity,被盖住的activity也有了这个事件,这不是bug吗),但是也是可以操作的,我们通过api获取到WindowManager(实际上就是WindowManagerIMpl这个类的实例对象),然后通过反射,获取到mGlobal对象,然后获取mViews集合,就可以获取到另外一个窗口的View根节点,可以理解为DecorView,然后通过这个DecorView重新分发一次事件dispatchTouchEvent(ev)即可。
核心类功能说明:
- WindowManager:
- 是一个接口,提供三个方法,实现类是WindowManagerImpl,最后通过WindowManagerGlobal这个单例实现,对window的各种操作。
- ViewRootImpl
- 负责连接view和window的桥梁。
- 负责和WindowManagerService的IPC同学。
- 负责管理和绘制view视图。
- WindowManagerService
- 这个是Window具体创建和管理的地方