点击阅读:Android 窗口显示系列文章
1. ViewRootImpl setView 流程
接上文在 Activity 启动过程中,在 ActivityThread 的 handleResumeActivity 中会 调用 WindowManagerImpl 的 addView 方法,最终会调用到 ViewRootImpl 的 setView 方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
// DecorView
mView = view;
mWindowAttributes.copyFrom(attrs);
// 核心流程3
requestLayout();
// 核心流程1
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
mTempControls, attachedFrame, compatScale);
// 核心流程2
mWindowLayout.computeFrames(mWindowAttributes, state,
displayCutoutSafe, winConfig.getBounds(), winConfig.getWindowingMode(),
UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH,
mInsetsController.getRequestedVisibleTypes(), 1f /* compactScale */,
mTmpFrames);
setFrame(mTmpFrames.frame, true /* withinRelayout */);
}
}
}
setView 的几个参数:
- view: 这个参数代表要设置为根视图的 View。在 Activity 中,这个 view 就是 DecorView(通过 setContentView 设置的内容的父容器)。
- attrs: 这个参数是窗口的属性,包括窗口的尺寸、类型、行为标志等。这些属性将用于窗口的布局和显示。
- panelParentView: 这个参数用于子窗口(如 PopupWindow)的情况,表示父窗口的 View。对于顶级窗口(如 Activity),这个参数为 null。
- userId: 这个参数表示用户 ID,用于多用户系统。
ViewRootImpl setView 流程中调用的三个核心方法:
- addToDisplayAsUser:远程调用到 WMS,添加 Window
- computeFrames:预计算 Window 的尺寸
- requestLayout:请求布局,开始 View 的绘制,这个过程是异步的,虽然代码在前面,实际上是后执行的.
下面对这三个方法逐一进行分析。
2. addToDisplayAsUser
addToDisplayAsUser:这部分主要的任务是在 WMS 中创建当前 Activity/窗口 对应的 WindowState 对象,并挂载到窗口容器树中去。还有匿名服务 Client 与 WindowState 的映射关系会被保存到 mWindowMap 中,方便后续找到 WindowState。
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
mTempControls, attachedFrame, compatScale);
2.1 WMS openSession
在 ViewRootImpl 的构造方法中,调用了 WindowManagerGlobal 的 getWindowSession 方法:
public ViewRootImpl(Context context, Display display) {
this(context, display, WindowManagerGlobal.getWindowSession(), new WindowLayout());
}
getWindowSession 方法:
@UnsupportedAppUsage
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
// Emulate the legacy behavior. The global instance of InputMethodManager
// was instantiated here.
// TODO(b/116157766): Remove this hack after cleaning up @UnsupportedAppUsage
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;
}
}
跨进程调用了 WMS openSession,创建了 Session 对象并返回:
@Override
public IWindowSession openSession(IWindowSessionCallback callback) {
return new Session(this, callback);
}
2.2 通过 WMS 添加 window
调用 Session 匿名 Binder 服务,这部分主要的任务是在 WMS 中创建当前 Activity/窗口 对应的 WindowState 对象,并挂载到窗口容器树中去。
@Override
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);
}
调用 WMS 的 addWindow 方法:
// 所有的 WindowState 都会保存在这里
final HashMap<IBinder, WindowState> mWindowMap = new HashMap<>();
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) {
// 检查权限
int res = mPolicy.checkAddPermission(attrs.type, isRoundedCornerOverlay, attrs.packageName, appOp);
WindowState parentWindow = null;// Activity 无父窗口
// DecorView,type,客户端创建默认 TYPE_BASE_APPLICATION 1
final int type = attrs.type;
synchronized (mGlobalLock) {
// 窗口已添加,直接 return
if (mWindowMap.containsKey(client.asBinder())) {
ProtoLog.w(WM_ERROR, "Window %s is already added", client);
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
}
ActivityRecord activity = null;
final boolean hasParent = parentWindow != null;// Activity false
// Activity 窗口,attrs.token 的类型就是 ActivityRecord,在 Activity 启动的过程中赋值
// 系统窗口或者悬浮窗,token 的类型就是 WindowToken,值为 null
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
// If this is a child window, we want to apply the same type checking rules as the
// parent window type.
final int rootType = hasParent ? parentWindow.mAttrs.type : type;
final IBinder windowContextToken = attrs.mWindowContextToken;
if (token == null) {
}else if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
activity = token.asActivityRecord();
}
// 创建当前窗口对应的 WindowState
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], attrs, viewVisibility, session.mUid, userId,
session.mCanAddInternalSystemWindow);
// 调整 Window 的参数
final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
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);
// 验证 window 是否可以添加,主要是验证权限
res = displayPolicy.validateAddingWindowLw(attrs, callingPid, callingUid);
if (res != ADD_OKAY) {
return res;
}
win.attach();
// windowState 缓存到 mWindowMap 中
mWindowMap.put(client.asBinder(), win);
// windowState 挂载到窗口容器树中
win.mToken.addWindow(win);
displayPolicy.addWindowLw(win, attrs);
displayPolicy.setDropInputModePolicy(win, win.mAttrs);
在之前的流程中我们可以知道 attrs.type 的值为 TYPE_BASE_APPLICATION,实际的值为 1。
接着会判断 mWindowMap 是否已经添加该窗口,若添加则直接返回。
Activity 一般是没有父窗口,所以 hasParent 为 false。所以 hasParent ? parentWindow.mAttrs.token : attrs.token 最终取到是 attrs.token,通过之前的流程可知,attrs.token 实际上指向的是系统侧的 ActivityRecord.token,是在 Activity 启动的过程中进行赋值的。
rootType 的值为 TYPE_BASE_APPLICATION。
接着创建当前窗口对应的 WindowState 对象,接着会将其 mWindowMap 中,并将 windowState 挂载到窗口容器树中。
3. computeFrames
computeFrames:预计算 Window 的尺寸,预计算的主要目的是为后续的预 measure 服务,让预 measure 有一个参考尺寸。
mWindowLayout.computeFrames(mWindowAttributes, state,
displayCutoutSafe, winConfig.getBounds(), winConfig.getWindowingMode(),
UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH,
mInsetsController.getRequestedVisibleTypes(), 1f /* compactScale */,
mTmpFrames);
public void computeFrames(WindowManager.LayoutParams attrs, InsetsState state,
Rect displayCutoutSafe, Rect windowBounds, @WindowingMode int windowingMode,
int requestedWidth, int requestedHeight, @InsetsType int requestedVisibleTypes,
float compatScale, ClientWindowFrames frames) {
// 获取窗口的类型、标志和私有标志,以便后面的计算使用
final int type = attrs.type;
final int fl = attrs.flags;
final int pfl = attrs.privateFlags;
// 判断窗口是否指定了 FLAG_LAYOUT_IN_SCREEN 标志,该标志表示窗口是否应该扩展到屏幕的尺寸
// 获取输出的各个位置信息,包括显示区域、父窗口区域和窗口区域
final Rect attachedWindowFrame = frames.attachedFrame;// null
final Rect outDisplayFrame = frames.displayFrame;// Rect(0, 0 - 0, 0)
final Rect outParentFrame = frames.parentFrame;// Rect(0, 0 - 0, 0)
// outFrame 与 frames.frame 引用的是同一个对象,同步修改
final Rect outFrame = frames.frame;
// 通过 InsetsState 计算窗口的 Insets 值。(系统UI如状态栏/导航栏占据的区域)
// 这里实际是去所有 Insets 的最大值
final Insets insets = state.calculateInsets(windowBounds, attrs.getFitInsetsTypes(),
attrs.isFitInsetsIgnoringVisibility());
final @WindowInsets.Side.InsetsSide int sides = attrs.getFitInsetsSides();
// 确定需要应用Insets的边
// 获取窗口在各个方向上是否需要计算 Insets 值,然后根据这些方向计算出对应方向的 Insets 值
final int left = (sides & WindowInsets.Side.LEFT) != 0 ? insets.left : 0;// 0
final int top = (sides & WindowInsets.Side.TOP) != 0 ? insets.top : 0;// 0
final int right = (sides & WindowInsets.Side.RIGHT) != 0 ? insets.right : 0;// 0
final int bottom = (sides & WindowInsets.Side.BOTTOM) != 0 ? insets.bottom : 0;// 0
// 将显示区域的边界计算出来,它是应用窗口在整个屏幕上的可见区域。其中,
// windowBounds 表示应用窗口的位置和大小,left、top、right 和 bottom 表示计算得到的窗口与屏幕边界的间距。
// outDisplayFrame 中保存了 Window 可以显示的最大区域,在屏幕区域的基础上剔除了 Inset 的区域。
// Rect(0, 0 - 1280, 720)
outDisplayFrame.set(windowBounds.left + left, windowBounds.top + top,
windowBounds.right - right, windowBounds.bottom - bottom);
// 计算出应用窗口父容器的边界
if (attachedWindowFrame == null) {// true
// 如果应用窗口未附加到其他窗口上,则父容器边界与显示区域相同。
outParentFrame.set(outDisplayFrame);
if ((pfl & PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME) != 0) {
if (source != null) {
outParentFrame.inset(source.calculateInsets(
outParentFrame, false /* ignoreVisibility */));
}
}
} else {
// 如果应用窗口附加到其他窗口上,则父容器边界要根据应用窗口所附加的窗口的位置和大小进行调整
outParentFrame.set(!layoutInScreen ? attachedWindowFrame : outDisplayFrame);
}
// 刘海处理
final int cutoutMode = attrs.layoutInDisplayCutoutMode;// 0
// cutout表示屏幕凸起部分的信息
// displayCutoutSafe 表示在应用窗口可见区域中,不会被刘海遮挡的区域
final Rect displayCutoutSafeExceptMaybeBars = mTempDisplayCutoutSafeExceptMaybeBarsRect;// Rect(0, 0 - 0, 0)
displayCutoutSafeExceptMaybeBars.set(displayCutoutSafe);
frames.isParentFrameClippedByDisplayCutout = false;
// 处理窗口与显示屏中的刘海屏幕(也称为"切口")之间的关系,以确保窗口不会被刘海屏幕覆盖
// 在Android系统中,刘海屏幕的信息由DisplayCutout类来表示,而这个类的实例通常可以从InsetsState对象中获取
if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS && !cutout.isEmpty()) {// 一般不走这
// 处理刘海屏/输入法窗口
...
}
// noLimits 表示是否启用无限制的布局
final boolean noLimits = (attrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0;// false
// inMultiWindowMode 表示窗口是否处于多窗口模式
final boolean inMultiWindowMode = WindowConfiguration.inMultiWindowMode(windowingMode);// windowingMode 1, inMultiWindowMode false
// 启用了无限制的布局、窗口不是系统错误类型(TYPE_SYSTEM_ERROR)
// 并且窗口不处于多窗口模式,那么将窗口的显示范围设置为整个屏幕
if (noLimits && type != TYPE_SYSTEM_ERROR && !inMultiWindowMode) {
outDisplayFrame.left = MIN_X;
outDisplayFrame.top = MIN_Y;
outDisplayFrame.right = MAX_X;
outDisplayFrame.bottom = MAX_Y;
}
// compatScale 表示兼容比例
final boolean hasCompatScale = compatScale != 1f;// false
// 父级窗口的宽度和高度
final int pw = outParentFrame.width();
final int ph = outParentFrame.height();
// 表示窗口是否扩展到刘海区
final boolean extendedByCutout =// false
(attrs.privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0;
// 请求的窗口宽度和高度等
int rw = requestedWidth;// -1
int rh = requestedHeight;// -1
// x,y,w,h 后面将用于计算窗口的位置和大小
float x, y;
int w, h;
// If the view hierarchy hasn't been measured, the requested width and height would be
// UNSPECIFIED_LENGTH. This can happen in the first layout of a window or in the simulated
// layout. If extendedByCutout is true, we cannot use the requested lengths. Otherwise,
// the window frame might be extended again because the requested lengths may come from the
// window frame.
// 处理窗口的宽度和高度
if (rw == UNSPECIFIED_LENGTH || extendedByCutout) {
// 窗口的宽度和高度是UNSPECIFIED_LENGTH,且扩展到刘海区就重新计算高度
// 计算结果为如果窗口的宽度 attrs.width 大于等于0,就使用 attrs.width 作为窗口的宽度,
// 否则使用父容器的宽度 pw 作为窗口的宽度
rw = attrs.width >= 0 ? attrs.width : pw;// rw = -1 >= 0 ? -1 : 1280 = 1280
}
if (rh == UNSPECIFIED_LENGTH || extendedByCutout) {
rh = attrs.height >= 0 ? attrs.height : ph;// 同上,720
}
// 根据窗口的属性计算窗口的宽度和高度
if ((attrs.flags & FLAG_SCALED) != 0) {// false
...
} else {
if (attrs.width == MATCH_PARENT) {// true
w = pw;
} else if (hasCompatScale) {
...
}
// 处理是否需要缩放
if (hasCompatScale) {
x = attrs.x * compatScale;
y = attrs.y * compatScale;
} else {
x = attrs.x;// 0.0
y = attrs.y;// 0.0
}
// 如果窗口处于多窗口模式且不是全屏任务
if (inMultiWindowMode
&& (attrs.privateFlags & PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME) == 0) {
// Make sure window fits in parent frame since it is in a non-fullscreen task as
// required by {@link Gravity#apply} call.
w = Math.min(w, pw);
h = Math.min(h, ph);
}
// We need to fit it to the display if either
// a) The window is in a fullscreen container, or we don't have a task (we assume fullscreen
// for the taskless windows)
// b) If it's a secondary app window, we also need to fit it to the display unless
// FLAG_LAYOUT_NO_LIMITS is set. This is so we place Popups, dialogs, and similar windows on
// screen, but SurfaceViews want to be always at a specific location so we don't fit it to
// the display.
// 如果不在多窗口模式下,或者不是TYPE_BASE_APPLICATION类型,且noLimits属性为false,
// 则需要将窗口大小限制在显示器内。fitToDisplay变量用于表示是否需要将窗口大小限制在显示器内
final boolean fitToDisplay = !inMultiWindowMode // true
|| ((attrs.type != TYPE_BASE_APPLICATION) && !noLimits);
// Set mFrame
// 计算出窗口在父视图中的矩形框的位置和大小
Gravity.apply(attrs.gravity, w, h, outParentFrame,
(int) (x + attrs.horizontalMargin * pw),
(int) (y + attrs.verticalMargin * ph), outFrame);
// Now make sure the window fits in the overall display frame.
// 计算出窗口在整个屏幕范围内的位置和大小
if (fitToDisplay) {// true
Gravity.applyDisplay(attrs.gravity, outDisplayFrame, outFrame);
}
if (extendedByCutout) {// false
extendFrameByCutout(displayCutoutSafe, outDisplayFrame, outFrame,
mTempRect);
}
...
}
该方法实际就是以屏幕大小去测量 Window 大小。在屏幕大小的基础上根据 App 的配置信息剔除系统窗口的影响(Inset 刘海 输入法等),剩下的就是 Window 可以显示的区域。