WindowManagerService添加窗口流程(4)
简述
添加窗口流程是WMS很重要的一个功能,如果你是一个app开发,基本上接触WMS最多的场景也就是这个流程,在添加悬浮窗的时候使用的addView就是这个流程,其实activity也是通过类似的流程去添加显示的窗口的,我们会通过介绍addView这个流程了解添加窗口这个过程。
和之前介绍SurfaceFlinger一样,在继续了解WMS之前,我们先来介绍一下app和WMS的binder通信架构,以便我们后面更好的了解WMS的其他流程。等我们看过几个核心服务之后,就会发现Framework里面所有服务和app的通信方式都是类似的。
端侧binder结构
我们app一般都是通过WindowManager去访问WMS的接口,WindowManager接口实现了ViewManager接口,ViewManager的接口就三个,addView,updateViewLayout,removeView,这三个接口app开发应该会在悬浮窗开发时使用,是三个挺常用也挺重要的接口,下一篇文章我们会介绍添加窗口的流程,也就是addView。
WindowManager扩展了很多接口,实际我们在使用时候拿到的是WindowManager的子类WindowManagerImpl。
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
WindowManagerImpl里面需要通过binder到WMS到方法都会通过WindowManagerGlobal去调用,WindowManagerGlobal是一个单例,WindowManagerGlobal里面有一个静态方法getWindowManagerService获取WMS binder句柄。sWindowManagerService是一个IWindowManager接口,IWindowManager是一个aidl接口用于和WMS通信。
public final class WindowManagerImpl implements WindowManager {
@UnsupportedAppUsage
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
// ...
}
public final class WindowManagerGlobal {
// ...
public static IWindowManager getWindowManagerService() {
synchronized (WindowManagerGlobal.class) {
if (sWindowManagerService == null) {
sWindowManagerService = IWindowManager.Stub.asInterface(
ServiceManager.getService("window"));
try {
if (sWindowManagerService != null) {
ValueAnimator.setDurationScale(
sWindowManagerService.getCurrentAnimatorScale());
sUseBLASTAdapter = sWindowManagerService.useBLAST();
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowManagerService;
}
}
// ...
}
还有一个getWindowSession,这里会通过之前的IWindowManager调用openSession,构建一个和WMS的会话,后续会用这个会话调用WMS的binder接口,这个Session实现了IWindowSession.aidl接口。
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
IWindowManager windowManager = getWindowManagerService();
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
});
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowSession;
}
}
app侧添加窗口
1.1 WindowManagerImpl.addView
前面我们提到过ViewManager里面有几个重要的接口,接下来我们就从addView开始了解app添加窗口的流程,这个流程是WMS很重要的一个流程。
我们在app开发中实现一个悬浮窗的时候,通常会通过这个方法来实现,本质上对话框也是通过这个实现的,所以如果你是app开发,应该也会常常接触到这个。
前面说过,WindowManagerImpl会通过WindowManagerGlobal去和WMS进行binder通信。顺带提一下,这里有一个参数mContext.getDisplayNoVerify(),这个是表示窗口需要添加到哪个屏幕上,所以当我们有虚拟屏的时候,app显示添加的窗口会显示在哪个屏幕上就是由这个参数决定的。
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyTokens(params);
// 详见1.2
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
1.2 WindowManagerGlobal.addView
首先做了一些参数校验
其次会构造一个ViewRootImpl,一个窗口在WindowManagerGlobal里面有三个关键的变量,一个View,一个ViewRootImpl,一个layoutParams。用三个列表记录。
ViewRootImpl里面有一个比较关键的变量是W类型的,是一个binder server,后面在添加窗口的过程会传给WMS,后续WMS就会通过这个binder和app交互。比如Insets发生变化就会通过这个binder通知app。
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
// ...参数校验
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
// 如果有父窗口,则将父窗口的token配置为layoutParams.token作为参数
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// ... 监听系统属性,RendorThread渲染有一些配置是通过系统属性决定的,所以需要监听系统属性实时更新配置
// mRoots是一个ViewRootImpl数组,每一个窗口都会对应一个ViewRootImpl,存储在mRoots中,这里尝试通过view找到对应的ViewRootImpl,如果有的话说明View已经显示在其他窗口上了,就会抛异常。
int index = findViewLocked(view, false);
if (index >= 0) {
// ... View已经添加,抛异常
}
// 如果窗口类型是子窗口,根据token找到父窗口的View存在panelParentView中供后续使用。
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
IWindowSession windowlessSession = null;
// mWindowlessRoots里面也是ViewRootImpl,SurfaceView之类的场景会出现没有Window但是SurfaceFlinger也有layer,这种场景就会放在mWindowlessRoots
if (wparams.token != null && panelParentView == null) {
for (int i = 0; i < mWindowlessRoots.size(); i++) {
ViewRootImpl maybeParent = mWindowlessRoots.get(i);
if (maybeParent.getWindowToken() == wparams.token) {
windowlessSession = maybeParent.getWindowSession();
break;
}
}
}
// 构建一个新的ViewRootImpl,里面主要是一些变量初始化,就不看了。
// 构造函数里会构建一个W类,是一个binder的server,WMS后续会通过这个binder来回调client(比如Insets变化等),详见1.2.1
if (windowlessSession == null) {
root = new ViewRootImpl(view.getContext(), display);
} else {
root = new ViewRootImpl(view.getContext(), display,
windowlessSession, new WindowlessWindowLayout());
}
view.setLayoutParams(wparams);
// 这三个列表里的对象是一一对应的,即一个窗口里有一个ViewRootImpl,一个View和一个LayoutParams
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
// 添加窗口的实际过程,详见1.3
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// ... 抛异常
}
}
}
1.2.1 class W
W类还有不少回调,这里随便列了两个。
static class W extends IWindow.Stub {
@Override
public void resized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration mergedConfiguration, InsetsState insetsState,
boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId,
boolean dragResizing) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor != null) {
viewAncestor.dispatchResized(frames, reportDraw, mergedConfiguration, insetsState,
forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing);
}
}
@Override
public void insetsControlChanged(InsetsState insetsState,
InsetsSourceControl[] activeControls) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor != null) {
viewAncestor.dispatchInsetsControlChanged(insetsState, activeControls);
}
}
// ...
}
1.3 ViewRootImpl.setView
setView这个方法比较长,我们省略了一些不关注的代码逻辑。
这里处理了很多逻辑,兼容模式,计算了mAttachInfo的一些属性,然后最重要的是通过addToDisplayAsUser binder到WMS添加窗口,以及根据添加窗口根据WMS计算窗口大小以及Inset等,同时还有配置Input通道相关的逻辑。
调用了requestLayout,会请求下一个VSync进行窗口刷新,是一个比较重要的过程,我们后面章节再介绍。
这里我们主要关注addToDisplayAsUser添加窗口的过程。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
mView = view;
mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
mFallbackEventHandler.setView(view);
// ... 设置一些attrs
setAccessibilityFocus(null, null);
// 设置回调,DecorView继承了RootViewSurfaceTaker
// 如果在Activity通过taskSurface设置的回调就是在这里配置的回调。
if (view instanceof RootViewSurfaceTaker) {
mSurfaceHolderCallback =
((RootViewSurfaceTaker)view).willYouTakeTheSurface();
if (mSurfaceHolderCallback != null) {
mSurfaceHolder = new TakenSurfaceHolder();
mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);
mSurfaceHolder.addCallback(mSurfaceHolderCallback);
}
}
// 计算Insets
if (!attrs.hasManualSurfaceInsets) {
attrs.setSurfaceInsets(view, false /*manual*/, true /*preservePrevious*/);
}
// 获取屏幕兼容模式
CompatibilityInfo compatibilityInfo =
mDisplay.getDisplayAdjustments().getCompatibilityInfo();
mTranslator = compatibilityInfo.getTranslator();
// 如果app设置了Surface回调,持有这个Surface就不能使用硬件加速,否则就可以开启硬件加速
if (mSurfaceHolder == null) {
enableHardwareAcceleration(attrs);
// ...
}
boolean restore = false;
// 兼容模式可能会放大或者缩小窗口,就是在这里实现,实际上就是修改attrs里面的属性。
if (mTranslator != null) {
mSurface.setCompatibilityTranslator(mTranslator);
restore = true;
attrs.backup();
mTranslator.translateWindowLayout(attrs);
}
mSoftInputMode = attrs.softInputMode;
// ...更新mAttachInfo
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */
// 请求下一个VSync信号
requestLayout();
// 构建InputChannel
InputChannel inputChannel = null;
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
inputChannel = new InputChannel();
}
mForceDecorViewVisibility = (mWindowAttributes.privateFlags
& PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
// ...
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
adjustLayoutParamsForCompatibility(mWindowAttributes);
controlInsetsForCompatibility(mWindowAttributes);
Rect attachedFrame = new Rect();
final float[] compatScale = { 1f };
// 通过binder接口添加窗口,详见1.4
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
mTempControls, attachedFrame, compatScale);
if (!attachedFrame.isValid()) {
attachedFrame = null;
}
if (mTranslator != null) {
// 如果之前兼容模式有缩放,这里缩放Inset
mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls.get());
mTranslator.translateRectInScreenToAppWindow(attachedFrame);
}
mTmpFrames.attachedFrame = attachedFrame;
mTmpFrames.compatScale = compatScale[0];
mInvCompatScale = 1f / compatScale[0];
} catch (RemoteException | RuntimeException e) {
// ...
} finally {
if (restore) {
attrs.restore();
}
}
// Inset相关的计算逻辑,到后面的章节单独介绍。
mAttachInfo.mAlwaysConsumeSystemBars =
(res & WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_SYSTEM_BARS) != 0;
mPendingAlwaysConsumeSystemBars = mAttachInfo.mAlwaysConsumeSystemBars;
mInsetsController.onStateChanged(mTempInsets);
mInsetsController.onControlsChanged(mTempControls.get());
final InsetsState state = mInsetsController.getState();
final Rect displayCutoutSafe = mTempRect;
state.getDisplayCutoutSafe(displayCutoutSafe);
final WindowConfiguration winConfig = getCompatWindowConfiguration();
// 计算一些窗口大小逻辑。
mWindowLayout.computeFrames(mWindowAttributes, state,
displayCutoutSafe, winConfig.getBounds(), winConfig.getWindowingMode(),
UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH,
mInsetsController.getRequestedVisibleTypes(), 1f /* compactScale */,
mTmpFrames);
setFrame(mTmpFrames.frame, true /* withinRelayout */);
registerBackCallbackOnWindow();
if (DEBUG_LAYOUT) Log.v(mTag, "Added window " + mWindow);
// 如果添加窗口没有成功
if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
mAdded = false;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
switch (res) {
// ...根据不同的返回值打印不同的错误信息,抛出异常。
}
}
// 注册屏幕变化监听,处理屏幕变化
registerListeners();
// ... 配置input通道接受器,设置InputStage,处理input事件在app层的逻辑
// 后续我们会专门介绍input,这里的细节在那个章节会介绍。
}
}
}
WMS侧添加窗口
接下来就到了WMS侧了
1.4 Session.addToDisplayAsUser
这里就是直接调用了WMS的addWindow
public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, int userId, @InsetsType int requestedVisibleTypes,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame,
float[] outSizeCompatScale) {
return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId,
requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls,
outAttachedFrame, outSizeCompatScale);
}
1.5 WindowManagerService.addWindow
这里的第二个参数IWindow client是端侧W类,后续WMS会通过这个W类来binder访问client端,这里会讲client存在新建的WindowState里。
先做了一些权限校验,根据窗口type做一些合理性检查。
然后如果窗口类型是一个子窗口,则和父窗口共用一个token,否则就新建一个WindowToken。
接下来会构建一个新的WindowState,它在WMS侧就代表这个新添加的窗口。displayPolicy.addWindowLw会看新增的窗口是否包含Inset,如果包含则更新WMS对Inset对记录。
后面还有一些更新Input焦点,更新Configuration,计算Insets。这些不是我们这节关注的点,这里就不细说了。
这个方法很长,但是主要大多数逻辑都是一些校验和计算,最重要的事就是新建WindowState,并且把它添加到树结构合适的节点处。
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, @InsetsType int requestedVisibleTypes,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame,
float[] outSizeCompatScale) {
// ...权限检测
synchronized (mGlobalLock) {
// 根据displayId获取DisplayContent,之前介绍过DisplayContent,在WMS树上就代表一个屏幕
final DisplayContent displayContent = getDisplayContentOrCreate(displayId, attrs.token);
// ...做一些有效检验,根据窗口type权限校验,userId等。
ActivityRecord activity = null;
final boolean hasParent = parentWindow != null;
// 通过attrs里面的token找到窗口
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
final int rootType = hasParent ? parentWindow.mAttrs.type : type;
boolean addToastWindowRequiresToken = false;
final IBinder windowContextToken = attrs.mWindowContextToken;
// 如果token为空,说明这次是新建一个窗口
if (token == null) {
// ...有效性检测
if (hasParent) {
// 如果是一个子窗口,和父窗口共用一个token
token = parentWindow.mToken;
} else if (mWindowContextListenerController.hasListener(windowContextToken)) {
final IBinder binder = attrs.token != null ? attrs.token : windowContextToken;
final Bundle options = mWindowContextListenerController
.getOptions(windowContextToken);
token = new WindowToken.Builder(this, binder, type)
.setDisplayContent(displayContent)
.setOwnerCanManageAppTokens(session.mCanAddInternalSystemWindow)
.setRoundedCornerOverlay(isRoundedCornerOverlay)
.setFromClientToken(true)
.setOptions(options)
.build();
} else {
final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
token = new WindowToken.Builder(this, binder, type)
.setDisplayContent(displayContent)
.setOwnerCanManageAppTokens(session.mCanAddInternalSystemWindow)
.setRoundedCornerOverlay(isRoundedCornerOverlay)
.build();
}
}
// ... 根据窗口Type做合理性检测
// ... 新建WindowState。
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], attrs, viewVisibility, session.mUid, userId,
session.mCanAddInternalSystemWindow);
// ...
final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
// 根据windowType对窗口attrs.flag做一些调整
displayPolicy.adjustWindowParamsLw(win, win.mAttrs);
attrs.flags = sanitizeFlagSlippery(attrs.flags, win.getName(), callingUid, callingPid);
attrs.inputFeatures = sanitizeSpyWindow(attrs.inputFeatures, win.getName(), callingUid,
callingPid);
win.setRequestedVisibleTypes(requestedVisibleTypes);
res = displayPolicy.validateAddingWindowLw(attrs, callingPid, callingUid);
if (res != ADD_OKAY) {
return res;
}
final boolean openInputChannels = (outInputChannel != null
&& (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
if (openInputChannels) {
win.openInputChannel(outInputChannel);
}
// ...处理Toast类型窗口
// 设置返回值为添加成功
res = ADD_OKAY;
// ...
// 会更新Session里面窗口的计数,每个Session就能知道当前自己有多少个窗口。
win.attach();
// 不同规定结构记录windowState,以便不同场景快速访问。
mWindowMap.put(client.asBinder(), win);
win.initAppOpsState();
final boolean suspended = mPmInternal.isPackageSuspended(win.getOwningPackage(),
UserHandle.getUserId(win.getOwningUid()));
win.setHiddenWhileSuspended(suspended);
final boolean hideSystemAlertWindows = !mHidingNonSystemOverlayWindows.isEmpty();
win.setForceHideNonSystemOverlayWindowIfNeeded(hideSystemAlertWindows);
boolean imMayMove = true;
// 将新建窗口添加到窗口树中
win.mToken.addWindow(win);
// 这里会判断新添加的窗口是否有Inset,有的话就更新WMS侧记录的Inset,这个会在后面讲Inset章节详细介绍。
displayPolicy.addWindowLw(win, attrs);
displayPolicy.setDropInputModePolicy(win, win.mAttrs);
// 如果是app启动窗口,需要关联activity,在后续activity自己的窗口绘制完成后需要删除这个窗口
if (type == TYPE_APPLICATION_STARTING && activity != null) {
activity.attachStartingWindow(win);
}
// 其他一些特殊窗口的特殊处理
final WindowStateAnimator winAnimator = win.mWinAnimator;
winAnimator.mEnterAnimationPending = true;
winAnimator.mEnteringAnimation = true;
// ...Input焦点的更新,添加窗口后可能会引起Input焦点变化,在Input章节再介绍
// 处理Configuration,如果新的窗口可见并且引起横竖屏变化,则需要重新分法该屏幕的Configurataion
// Configuration每个节点都有一个,但是每次更新是会从树多根节点以一定规则计算,每个节点都会更新。
// 这个也比较好理解,比如由于前台app横屏幕,整个屏幕其实都进入了横屏状态,需要同时改变其他窗口的Configuration来更新UI。
boolean needToSendNewConfiguration =
win.isVisibleRequestedOrAdding() && displayContent.updateOrientation();
if (win.providesDisplayDecorInsets()) {
needToSendNewConfiguration |= displayPolicy.updateDecorInsetsInfo();
}
if (needToSendNewConfiguration) {
displayContent.sendNewConfiguration();
}
// 计算更新Inset。细节我们后续章节在讲Inset的时候会介绍
displayContent.getInsetsStateController().updateAboveInsetsState(
false /* notifyInsetsChanged */);
// 设置返回给app侧Insets的信息
outInsetsState.set(win.getCompatInsetsState(), true /* copySources */);
getInsetsSourceControls(win, outActiveControls);
// 设置返回给app的窗口大小等信息
if (win.mLayoutAttached) {
outAttachedFrame.set(win.getParentWindow().getFrame());
if (win.mInvGlobalScale != 1f) {
outAttachedFrame.scale(win.mInvGlobalScale);
}
} else {
outAttachedFrame.set(0, 0, -1, -1);
}
outSizeCompatScale[0] = win.getCompatScaleForClient();
}
Binder.restoreCallingIdentity(origId);
return res;
}
小结
添加窗口这个流程有两个方法很长,主要是处理不同的窗口类型,做了一些权限校验和合理性检查,以及特定窗口类型的一些特殊业务逻辑,如果只关心其中核心逻辑其实很简单。
从app侧构建ViewRootImpl,ViewRootImpl在app侧和app自身的窗口是一一对应的,并且ViewRootImpl里有一个W类,是一个binder server侧。
然后向WMS发出添加窗口请求,会将W传给WMS,后续WMS会通过这个binder来回调app侧,WMS这边主要做的事就是新建一个WindowState,然后把它添加到窗口树合适的节点下。还有一些Inset相关的逻辑,处理一些Input相关的逻辑等等。
下一节我们会介绍Inset框架,介绍一下Inset到底是啥,以及WMS是怎么管理Inset,怎么同步给app的。