Android 窗口显示(三)—— ViewRootImpl setView 流程

224 阅读8分钟

点击阅读: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 的几个参数:

  1. view: 这个参数代表要设置为根视图的 View。在 Activity 中,这个 view 就是 DecorView(通过 setContentView 设置的内容的父容器)。
  2. attrs: 这个参数是窗口的属性,包括窗口的尺寸、类型、行为标志等。这些属性将用于窗口的布局和显示。
  3. panelParentView: 这个参数用于子窗口(如 PopupWindow)的情况,表示父窗口的 View。对于顶级窗口(如 Activity),这个参数为 null。
  4. userId: 这个参数表示用户 ID,用于多用户系统。

ViewRootImpl setView 流程中调用的三个核心方法

  1. addToDisplayAsUser:远程调用到 WMS,添加 Window
  2. computeFrames:预计算 Window 的尺寸
  3. 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 可以显示的区域。