Android Framework 窗口子系统 (03) 窗口显示次序

系列文章解读&说明:

Android Framework 窗口子系统 的 分析主要分为以下部分:

(01)WindowMangerService基础知识

(02)应用进程和WindowManagerService之间的关系

(03)窗口显示次序

(04)确定窗口尺寸

(05)窗口布局说明

(06)Choreographer机制

(07)窗口动画系统

本模块分享的内容:窗口显示次序

本章关键点总结 & 说明:

该图关注➕思维导图中右下方:窗口次序 部分即可。通过代码分析先说明了主序、次序、窗口类型的基础知识;之后说明了窗口次序的流程,排序和调整是如何实现的?同时解释了实现的原理。

说明:手机屏幕是以左上角为原点,向右为X轴方向,向下为Y轴方向的一个二维空间。为方便管理窗口显示次序,手机屏幕被扩展为了一个三维空间,多定义了一个Z轴,方向为垂直于屏幕表面指向屏幕外。多个窗口依照其前后顺序排布在这个虚拟的Z轴上,因此窗口的显示次序又被称为Z序(Z order)。

1 主序、子序、窗口显示类型

从WS的构造方法开始分析,代码如下:

 WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
           WindowState attachedWindow, int appOp, int seq, WindowManager.LayoutParams a,
           int viewVisibility, final DisplayContent displayContent) {
        ...
        //如果是子窗口,使用它依附的窗口类型来计算
        if ((mAttrs.type >= FIRST_SUB_WINDOW && mAttrs.type <= LAST_SUB_WINDOW)) {
            mBaseLayer = mPolicy.windowTypeToLayerLw(
                    attachedWindow.mAttrs.type) * WindowManagerService.TYPE_LAYER_MULTIPLIER
                    + WindowManagerService.TYPE_LAYER_OFFSET;//计算mBaseLayer,关键点1
            //表示子窗口和父窗口的相对位置
            mSubLayer = mPolicy.subWindowTypeToLayerLw(a.type);//计算mSubLayer,关键点2
            ...
        } else {//非子窗口,直接使用窗口类型来计算
            //注:TYPE_LAYER_MULTIPLIER的值是10000,TYPE_LAYER_OFFSET的值是1000
            mBaseLayer = mPolicy.windowTypeToLayerLw(a.type)
                    * WindowManagerService.TYPE_LAYER_MULTIPLIER
                    + WindowManagerService.TYPE_LAYER_OFFSET;
            mSubLayer = 0;//无效,仅在子窗口中有用
            ...
        }
        ...
    }

由上可知,窗口显示次序由两个成员字段描述:主序mBaseLayer和子序mSubLayer。

  1. 主序用于描述窗口及其子窗口在所有窗口中的显示位置。主序越大,则窗口及其子窗口的显示位置相对于其他窗口的位置越靠前。
  2. 子序描述一个子窗口在其兄弟窗口中的显示位置。子序越大,相对于其兄弟窗口的位置越靠前。

@1 关键点1的分析,这里继续分析mPolicy.windowTypeToLayerLw的内部实现,代码如下:

 //根据类型返回窗口的种类,从1-31,
    public int windowTypeToLayerLw(int type) {
        if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
            return 2;
        }
        switch (type) {
        case TYPE_UNIVERSE_BACKGROUND:
            return 1;
        case TYPE_PRIVATE_PRESENTATION:
            return 2;
        case TYPE_WALLPAPER:
            // wallpaper is at the bottom, though the window manager may move it.
            return 2;
        case TYPE_PHONE:
            return 3;
        case TYPE_SEARCH_BAR:
            return 4;
        ...
        case TYPE_HIDDEN_NAV_CONSUMER:
            return 30;
        /// M:JB migration
        case TYPE_TOP_MOST:
            return 31;
        }
        Log.e(TAG, "Unknown window type: " + type);
        return 2;
    }

 这里相当于把窗口根据类型分成不同的层,但系统中同类型的窗口很多,因此最后将其*10000,相当于为同类型窗口保留10000个位置。
@2 关键点2的分析,这里继续分析mPolicy.subWindowTypeToLayerLw的内部实现,代码如下:

public int subWindowTypeToLayerLw(int type) {
        switch (type) {
        case TYPE_APPLICATION_PANEL:
        case TYPE_APPLICATION_ATTACHED_DIALOG:
            return APPLICATION_PANEL_SUBLAYER;//等于1
        case TYPE_APPLICATION_MEDIA:
            return APPLICATION_MEDIA_SUBLAYER;//等于-2
        case TYPE_APPLICATION_MEDIA_OVERLAY:
            return APPLICATION_MEDIA_OVERLAY_SUBLAYER;//等于-1
        case TYPE_APPLICATION_SUB_PANEL:
            return APPLICATION_SUB_PANEL_SUBLAYER;//等于2
        }
        Log.e(TAG, "Unknown sub-window type: " + type);
        return 0;
    }

如果返回值<0,将位于所依附窗口的下面;否则将覆盖所依附窗口。

于父窗口而言,主序取决于类型,子序则保持为0。子窗口主序与父窗口一样,子序取决于类型。窗口类型的主序和子序分别如下,窗口主序如下所示:

窗口子序如下所示:

特殊说明:

1 MEDIA和MEDIA_OVERLAY为何为负数?

这表明它们的显示次序位于其父窗口的后面。两个类型子窗口由SurfaceView控件创建。SurfaceView被实例化后,会向WMS添加一个类型为MEDIA的子窗口,它的父窗口就是承载SurfaceView控件的窗口。这个子窗口的Surface将被用于视频回放、相机预览或游戏绘制。为了不让这个子窗口覆盖住所有的父窗口中承载的其他控件(如拍照按钮,播放器控制按钮等),它必须位于父窗口之后。

2 WALLPAPER类型的窗口的主序为何和APPLICATION类型的窗口主序相同?

WALLPAPER类型的窗口需要在所有的APPLICATION窗口之间切换。因为有的Activity指定了android:windowShowWallpaper为true,则表示窗口要求将用户当前壁纸作为其背景。对于WMS来说,最简单的办法就是将WALLPAPER窗口放置到紧邻拥有这个式样的窗口的下方。在这种需求下,为了保证主序决定窗口顺序的原则,WALLPAPER使用了与APPLICATION相同的主序。

3 如果有两个相同类型的窗口,那么它们的主序与子序会完全相同?

以上描述的主序和子序仅仅是排序的依据之一,WMS需要根据当前所有同类型窗口的数量为每个窗口计算最终的现实次序。

2 主序、次序 确定窗口次序

WMS的addWindow()函数分析如下:

public int addWindow(Session session, IWindow client, int seq,
        WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
        Rect outContentInsets, Rect outStableInsets, InputChannel outInputChannel) {
    int res = mPolicy.checkAddPermission(attrs, appOp);
    //...
    synchronized(mWindowMap) {
		//...
        final DisplayContent displayContent = getDisplayContentLocked(displayId);
        if (displayContent == null) {
            Slog.w(TAG, "Attempted to add window to a display that does not exist: "
                    + displayId + ".  Aborting.");
            return WindowManagerGlobal.ADD_INVALID_DISPLAY;
        }
        //...
        WindowToken token = mTokenMap.get(attrs.token);
        //...
		// 新的WindowState对象在其构造函数中根据窗口类型初始化了其主序mBaseLayer和mSubLayer
        win = new WindowState(this, session, client, token,
                attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);
		
		 
        //...
        if (type == TYPE_INPUT_METHOD) {
            win.mGivenInsetsPending = true;
            mInputMethodWindow = win;
            addInputMethodWindowToListLocked(win);
            imMayMove = false;
        } else if (type == TYPE_INPUT_METHOD_DIALOG) {
            mInputMethodDialogs.add(win);
            addWindowToListInOrderLocked(win, true);
            moveInputMethodDialogsLocked(findDesiredInputMethodWindowIndexLocked(true));
            imMayMove = false;
        } else {
			// 将新的WindowState按显示次序插入到当前DisplayContent的mWindows列表中
            addWindowToListInOrderLocked(win, true);
            if (type == TYPE_WALLPAPER) {
                mLastWallpaperTimeoutTime = 0;
                displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
            } else if ((attrs.flags&FLAG_SHOW_WALLPAPER) != 0) {
                displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
            } else if (mWallpaperTarget != null
                    && mWallpaperTarget.mLayer >= win.mBaseLayer) {
                displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
            }
        }
        //...
        // 根据窗口的排序结果,为DisplayContent的所有窗口分配最终的显示次序
        assignLayersLocked(displayContent.getWindowList());
        //...
    }
    //...
    return res;
}

这里关注2个关键点:

  1. addWindowToListInOrderLocked():将新建的WindowState按照一定的顺序插入到当前DisplayContent的mWindows列表中。它严格地按照显示顺序存储了所有窗口的WindowState。
  2. assignLayersLocked():将根据mWindows的存储顺序对所有的WindowState的主序和子序进行调整。

下面针对2个关键点进行分析,对于一组窗口,这里是需要先排序窗口,之后再调整窗口,因此,这里分两部分来分析

2.1 addWindowToListInOrderLocked分析
@1 排序窗口对应的方法是addWindowToListInOrderLocked,代码如下:

    //根据窗口类型分三种情况处理
    private void addWindowToListInOrderLocked(final WindowState win, boolean addToToken) {
        if (win.mAttachedWindow == null) {
            final WindowToken token = win.mToken;
            int tokenWindowsPos = 0;
            if (token.appWindowToken != null) {
                tokenWindowsPos = addAppWindowToListLocked(win);//Activity的顶层窗口,关键点1
            } else {
                addFreeWindowToListLocked(win);//系统窗口,关键单2
            }
            if (addToToken) {
                token.windows.add(tokenWindowsPos, win);//将WS对象插入到WindowToken列表中
            }
        } else {
            addAttachedWindowToListLocked(win, addToToken);//添加子窗口,关键点3
        }
        if (win.mAppToken != null && addToToken) {
            win.mAppToken.allAppWindows.add(win);//将WS对象插入到AppWindowToken列表中
        }
    }

@@1.1 关键点1,addAppWindowToListLocked的分析

private int addAppWindowToListLocked(final WindowState win) {
        final IWindow client = win.mClient;
        final WindowToken token = win.mToken;
        final DisplayContent displayContent = win.getDisplayContent();
        if (displayContent == null) {
            return 0;
        }
        final WindowList windows = win.getWindowList();
        final int N = windows.size();
        //这里通过token获取对应的tokenWindowList
        WindowList tokenWindowList = getTokenWindowsOnDisplay(token, displayContent);
        int tokenWindowsPos = 0;
        int windowListPos = tokenWindowList.size();
        //判断系统中是否存在和待插入窗口具有相同token的窗口,如果有说明这不是Activity的第一个窗口
        if (!tokenWindowList.isEmpty()) {
            //判定类型,是TYPE_BASE_APPLICATION则放在和它具有相同token窗口的下面
            if (win.mAttrs.type == TYPE_BASE_APPLICATION) {
                WindowState lowestWindow = tokenWindowList.get(0);
                placeWindowBefore(lowestWindow, win);
                tokenWindowsPos = indexOfWinInWindowList(lowestWindow, token.windows);
            } else {//不是TYPE_BASE_APPLICATION类型
                AppWindowToken atoken = win.mAppToken;
                WindowState lastWindow = tokenWindowList.get(windowListPos - 1);
                //判断是否位于最前面,即正在启动
                if (atoken != null && lastWindow == atoken.startingWindow) {
                    placeWindowBefore(lastWindow, win);//放在启动窗口的下面
                    tokenWindowsPos = indexOfWinInWindowList(lastWindow, token.windows);
                } else {//判断不在最前面
                    //寻找同一应用中位置最高的窗口
                    int newIdx = findIdxBasedOnAppTokens(win);
                    //插入在它上面,表示加入的窗口将覆盖在前面窗口之上
                    windows.add(newIdx + 1, win);
                    if (newIdx < 0) {
                        tokenWindowsPos = 0;
                    } else {
                        tokenWindowsPos = indexOfWinInWindowList(
                                windows.get(newIdx), token.windows) + 1;
                    }
                    mWindowsChanged = true;
                }
            }
            return tokenWindowsPos;
        }
        //如果系统中不存在和待插入窗口具有相同token的窗口则继续执行下面的逻辑
        //说明Activity刚启动,第1个窗口还没有创建完毕
        WindowState pos = null;
        final ArrayList<Task> tasks = displayContent.getTasks();
        int taskNdx;
        int tokenNdx = -1;
        //遍历系统中所有的task以及task中包含的AppWindowToken,找到窗口的位置
        for (taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
            AppTokenList tokens = tasks.get(taskNdx).mAppTokens;
            for (tokenNdx = tokens.size() - 1; tokenNdx >= 0; --tokenNdx) {
                final AppWindowToken t = tokens.get(tokenNdx);
                if (t == token) {
                    --tokenNdx;
                    if (tokenNdx < 0) {
                        --taskNdx;
                        if (taskNdx >= 0) {
                            tokenNdx = tasks.get(taskNdx).mAppTokens.size() - 1;
                        }
                    }
                    break;
                }
                tokenWindowList = getTokenWindowsOnDisplay(t, displayContent);
                if (!t.sendingToBottom && tokenWindowList.size() > 0) {
                    pos = tokenWindowList.get(0);
                }
            }
            if (tokenNdx >= 0) {
                break;
            }
        }
        ...
        //在task中排在本窗口前面的窗口中,找出距离自己最近的且token列表不为NULL的窗口
        //插入到它的最后一个子窗口的后面
        //如果前面窗口的列表也都是NULL,则寻找排在本窗口后面的第一个包含窗口对象的AppWindowToken
        //把本窗口插入到它前面
        //如果前后窗口的AppWinodwToken都为NULL,则重新遍历整个窗口,根据mBaseLayer的值来确定窗口的位置
        ...
        return tokenWindowsPos;
    }

 @@1.2 关键点2,addFreeWindowToListLocked的分析:

private void addFreeWindowToListLocked(final WindowState win) {
        final WindowList windows = win.getWindowList();
        final int myLayer = win.mBaseLayer;
        int i;
        for (i = windows.size() - 1; i >= 0; i--) {//遍历同一设备所有的windows
            if (windows.get(i).mBaseLayer <= myLayer) {//比较mBaseLayer的值
                break;
            }
        }//退出循环表示找到合适位置
        i++;
        windows.add(i, win);//插入到合适的位置
        mWindowsChanged = true;
    }

 @@1.3 关键点3,addAttachedWindowToListLocked的分析:

private void addAttachedWindowToListLocked(final WindowState win, boolean addToToken) {
        final WindowToken token = win.mToken;
        final DisplayContent displayContent = win.getDisplayContent();
        if (displayContent == null) {
            return;
        }
        final WindowState attached = win.mAttachedWindow;
        WindowList tokenWindowList = getTokenWindowsOnDisplay(token, displayContent);
        // Figure out this window's ordering relative to the window it is attached to.
        final int NA = tokenWindowList.size();
        final int sublayer = win.mSubLayer;
        int largestSublayer = Integer.MIN_VALUE;
        WindowState windowWithLargestSublayer = null;
        int i;
        for (i = 0; i < NA; i++) {
            WindowState w = tokenWindowList.get(i);
            final int wSublayer = w.mSubLayer;
            if (wSublayer >= largestSublayer) {
                largestSublayer = wSublayer;
                windowWithLargestSublayer = w;
            }
            if (sublayer < 0) {
                // For negative sublayers, we go below all windows in the same sublayer.
                if (wSublayer >= sublayer) {
                    if (addToToken) {
                        token.windows.add(i, win);
                    }
                    placeWindowBefore(wSublayer >= 0 ? attached : w, win);
                    break;
                }
            } else {
                // For positive sublayers, we go above all windows in the same sublayer.
                if (wSublayer > sublayer) {
                    if (addToToken) {
                        token.windows.add(i, win);
                    }
                    placeWindowBefore(w, win);
                    break;
                }
            }
        }
        if (i >= NA) {
            if (addToToken) {
                token.windows.add(win);
            }
            if (sublayer < 0) {
                placeWindowBefore(attached, win);
            } else {
                placeWindowAfter(largestSublayer >= 0? windowWithLargestSublayer: attached,win);
            }
        }
    }

上面是窗口次序的代码分析,经过分析后,我们这里抽象出原则

@2 原则如下所示

@@2.1 子窗口排序规则:相对父窗口,根据子序排列;父窗口子序为0,因此:

  1. 子序为负数,会放在父窗口后面
  2. 子序为正数,会放在父窗口前面
  3. 新窗口子序==现有窗口子序,正数子序新窗口在前;负数子序新窗口在后

@@2.2 非应用窗口排序规则:

  1. 依照主序排序,主序高在前;
  2. 新窗口子序==现有窗口子序,新窗口在前

@@2.3 应用窗口排序规则:同一应用显示位置必须相邻。

  1. 当前应用已有窗口在显示,新窗口在该应用其他窗口前面。
  2. 当前应用第一个窗口,参照其他应用窗口顺序:插入最后一个应用,最后一个窗口后面 or 后面第一个应用,最前面窗口前面
  3. 没有参照应用窗口,根据主序将新窗口插入列表中。

@3 输入法和壁纸窗口特殊说明
窗口管理系统是按照叠层方式来管理所有的窗口,窗口的Z轴位置计算和设置过程如下图所示:

WMS主要用来管理各个窗口的位置,确保正确显示;在所有窗口中,输入法窗口和壁纸窗口比较特殊

  •  输入法窗口需要显示在所有窗口的前面
  •  壁纸窗口位于当前Activity界面的下面:
  1. 如果希望Activity的背景显示壁纸,需要把background设置成透明/半透明
  2. 在LayoutParams属性中加FLAG_SHOW_WALLPAPER标志
  3. SurfaceFlinger合成图像时将显示位于Activity窗口下的壁纸

2.2 assignLayersLocked分析

@1 assignLayersLocked会根据窗口的排序结果,为DisplayContent的所有窗口分配最终的显示次序,代码实现如下:

private final void assignLayersLocked(WindowList windows) {
        int N = windows.size();
        int curBaseLayer = 0;
        int curLayer = 0;
        int i;
        boolean anyLayerChanged = false;
        for (i=0; i<N; i++) {
            final WindowState w = windows.get(i);
            final WindowStateAnimator winAnimator = w.mWinAnimator;
            boolean layerChanged = false;
            int oldLayer = w.mLayer;
            //如果mBaseLayer和前一个相同,或者是输入法窗口/壁纸窗口
            if (w.mBaseLayer == curBaseLayer || w.mIsImWindow|| (i > 0 && w.mIsWallpaper)) {
                curLayer += WINDOW_LAYER_MULTIPLIER;//注:WINDOW_LAYER_MULTIPLIER ==5
                w.mLayer = curLayer;
            } else {
                //如果mBaseLayer和前一个不同,则进入下一级的计算
                curBaseLayer = curLayer = w.mBaseLayer;
                w.mLayer = curLayer;
            }
            if (w.mLayer != oldLayer) {//如果层级改变,对下面的变量赋值
                layerChanged = true;
                anyLayerChanged = true;
            }
            //调整mAnimLayer的值
            final AppWindowToken wtoken = w.mAppToken;
            oldLayer = winAnimator.mAnimLayer;
            if (w.mTargetAppToken != null) {
                winAnimator.mAnimLayer =
                        w.mLayer + w.mTargetAppToken.mAppAnimator.animLayerAdjustment;
            } else if (wtoken != null) {
                winAnimator.mAnimLayer =
                        w.mLayer + wtoken.mAppAnimator.animLayerAdjustment;
            } else {
                winAnimator.mAnimLayer = w.mLayer;
            }
            if (w.mIsImWindow) {
                winAnimator.mAnimLayer += mInputMethodAnimLayerAdjustment;
            } else if (w.mIsWallpaper) {
                winAnimator.mAnimLayer += mWallpaperAnimLayerAdjustment;
            }
            if (winAnimator.mAnimLayer != oldLayer) {
                layerChanged = true;
                anyLayerChanged = true;
            }
            final TaskStack stack = w.getStack();
            if (layerChanged && stack != null && stack.isDimming(winAnimator)) {
                scheduleAnimationLocked();//执行动画
            }
        }
        if (mAccessibilityController != null && anyLayerChanged
                && windows.get(windows.size() - 1).getDisplayId() == Display.DEFAULT_DISPLAY) {
            mAccessibilityController.onWindowLayersChangedLocked();
        }
    }

@2 mLayer值的计算就是从assignLayersLocked方法开始分析,该方法的目的是根据窗口的位置重新调整mLayer的值。
说明:在assignLayersLocked方法之前,需要通过方法对窗口进行添加和排序,即在调整前是已经排序好了的
调整方法:从最底层开始,具有相同mBaseLayer值作为一组,每组窗口mLayer值都是从mBaseLayer值开始,依次+WINDOW_LAYER_MULTIPLIER(=5)

这样做的目的:同层窗口中每隔一个窗口就留下4个空位,方便下次插入新的窗口。同时,对于输入法和壁纸,做特殊处理,会和插入它位置前面的窗口处于一个层级,不通过mBaseLayer的值来计算。

@3 mAnimLayer

  1. 当没有动画时,mAnimLayer==mLayer;
  2. 当窗口附属为一个Activity时,则会根据AppTokenAnimator的需要适当地增加一个矫正值。在动画完结后,mAnimLayer会被重新赋值为WindowState.mLayer,使得窗口回到其应有的位置。

矫正值说明:它来自AppTokenAnimator所使用的Animation。当Animation要求动画对象的ZOrder必须位于其他对象之上时(Animation.getZAdjustment()的返回值为Animation.ZORDER_TOP),这个矫正值(1000)很大,于是窗口在动画过程中会显示在其他同主序的窗口之上。相反,如果要求ZOrder必须位于其他对象之下时,矫正为(-1000)很小,于是窗口会显示在其他同主序的窗口之下。

3 总结

窗口根据自己的类型得出其主序及子序,然后addWindowToListInOrderLocked()根据主序、子序以及其所属的Activity的顺序,按照升序排列在DisplayContent的mWindows列表中。然后assignLayersLocked()为mWindows中的所有窗口分配最终的显示次序

猜你喜欢

转载自blog.csdn.net/vviccc/article/details/93909911