Android Window 如何確定大小 onMeasure()多次執行原因

前言

之前系統地分析了View Measure 過程:
Android 自定義View之Measure過程
我們知道父布局根據自身和子布局的要求給子布局生成測量模式和測量尺寸,并封裝在MeasureSpec 對象里,最終傳遞給子布局讓它最后確定自身的尺寸。
很自然就會想到,既然子布局是從父布局拿的測量結果,父布局又從它的父布局拿測量結果,最終到ViewTree的頂點根View是誰測量的呢?
循著這個問題,從源碼角度一探究竟。

系列文章:

Window/WindowManager 不可不知之事
Android Window 如何確定大小/onMeasure()多次執行原因

通過本篇文章,你將了解到:

1、Window 尺寸測量
2、根View 尺寸測量
3、Window、ViewRootImpl、View 三者關系

1、Window 尺寸測量

一個小Demo

通過WindowManager.addView(xx)展示一個懸浮窗:

    private void showView() {
        //獲取WindowManager實例
        wm = (WindowManager) App.getApplication().getSystemService(Context.WINDOW_SERVICE);

        //設置LayoutParams屬性
        layoutParams = new WindowManager.LayoutParams();
        //寬高尺寸
        layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
        layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
        layoutParams.format = PixelFormat.TRANSPARENT;
        //設置背景陰暗
        layoutParams.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
        layoutParams.dimAmount = 0.6f;

        //Window類型
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        } else {
            layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        }

        //構造TextView
        TextView myView = new TextView(this);
        myView.setText("hello window");
        //設置背景為紅色
        myView.setBackgroundResource(R.color.colorRed);
        FrameLayout.LayoutParams myParam = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 400);
        myParam.gravity = Gravity.CENTER;
        myView.setLayoutParams(myParam);

        //myFrameLayout 作為rootView
        FrameLayout myFrameLayout = new FrameLayout(this);
        //設置背景為綠色
        myFrameLayout.setBackgroundColor(Color.GREEN);
        myFrameLayout.addView(myView);

        //添加到window
        wm.addView(myFrameLayout, layoutParams);
    }

上述代碼簡單概述如下:

1、構造TextView,設置其背景為紅色
2、構造FrameLayout,設置其背景為綠色
3、將TextView作為子View添加到FrameLayout
4、將FrameLayout作為RootView(根View)添加到Window里

懸浮窗展示完整Demo請移步:Window/WindowManager 不可不知之事

注意到

wm.addView(myFrameLayout, layoutParams);

layoutParams 里重點關注寬、高字段的值,我們知道這是給Window的尺寸約束,以寬為例,設置不同的值,看看其效果:
1、wrap_content

layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;

image.png

可以看出:RootView(FrameLayout) 包裹著TextView,兩者寬度一致。

2、match_parent

layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;

image.png

可以看出:RootView(FrameLayout) 寬充滿屏幕

3、設置具體的值

layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = 800;

image.png

可以看出:RootView(FrameLayout) 寬沒充滿屏幕,屏幕寬1080px。

結合上述三張圖,我們有理由相信,wm.addView(myFrameLayout, layoutParams) 里的layoutParams 是用來約束myFrameLayout(RootView),那么Window尺寸是怎么來的呢?

Window 尺寸的確定

從wm.addView(xx)開始分析,WindowManager 是個接口,其實現類是:WindowManagerImpl。

#WindowManagerImpl.java
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        //賦值token,在啟動Dialog/PopupDialog 會判斷該值
        applyDefaultToken(params);
        //mGlobal 為單例,管理所有的ViewRootImpl、RootView
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

接著看WindowManagerGlobal 的處理:

#WindowManagerGlobal.java
    public void addView(View view, ViewGroup.LayoutParams params,
                        Display display, Window parentWindow) {
        ...
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...
            //構造ViewRootImpl 對象
            root = new ViewRootImpl(view.getContext(), display);

            //view 作為RootView
            //將傳進來的wparams作為該RootView的LayoutParams
            view.setLayoutParams(wparams);

            //記錄對象
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            try {
                //ViewRootImpl 關聯RootView
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                ...
            }
        }
    }

由上可知,在wm.addView(xx)里傳遞進來的LayoutParams設置給了RootView。
繼續來看ViewRootImpl.setView(xx)過程。

#ViewRootImpl.java
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ...
                //將LayoutParams記錄到成員變量 mWindowAttributes 里
                //該變量用來描述Window屬性
                mWindowAttributes.copyFrom(attrs);
                ...
                //開啟View layout 三大流程
                requestLayout();
                ...
                try {
                    ...
                    //IPC 通信,告訴WindowManagerService 要創建Window
                    //將mWindowAttributes 傳入
                    //返回mTmpFrame 表示該Window可以展示的最大尺寸
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                            mTempInsets);
                    //將返回的值記錄到成員變量 mWinFrame 里
                    setFrame(mTmpFrame);
                } catch (RemoteException e) {
                    ...
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }
                ...
            }
        }
    }

上面這段重點關注2個方面:

1、傳入的LayoutParams記錄到成員變量mWindowAttributes,最后用來約束Window。
2、添加Window時返回Window的最大尺寸,最終記錄在成員變量:mWinFrame里。

綜上所述,我們發現:
wm.addView(myFrameLayout, layoutParams) 里的layoutParams不僅約束了RootView,也約束了Window。

2、根View 尺寸測量

既然知道了RootView 的layoutParams,依據我們之前分析過的ViewTree的測量過程:Android 自定義View之Measure過程
可知還需要為RootView生成MeasureSpec對象。
在setView(xx)過程中調用了requestLayout 注冊了回調,當屏幕刷新信號到來之時執行performTraversals()開啟三大流程。

#ViewRootImpl.java
    private void performTraversals() {
        ...
        //之前記錄的Window LayoutParams
        WindowManager.LayoutParams lp = mWindowAttributes;

        //Window需要的大小
        int desiredWindowWidth;
        int desiredWindowHeight;
        ...

        Rect frame = mWinFrame;
        if (mFirst) {
            ...
            if (shouldUseDisplaySize(lp)) {
                ...
            } else {
                //mWinFrame即是之前添加Window時返回的Window最大尺寸
                desiredWindowWidth = mWinFrame.width();
                desiredWindowHeight = mWinFrame.height();
            }
            ...
        } else {
            ...
        }

        ...
        if (layoutRequested) {
            ...
            //從方法名看應該是測量ViewTree -----------(1)
            windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
        }
        ...

        if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
            ...
            try {
                ...
                //重新確定Window尺寸 --------(2)
                relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
                ...
            } catch (RemoteException e) {
            }
            ...
            if (!mStopped || mReportNextDraw) {
                ...
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    ...
                    //再次測量ViewTree -------- (3)
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    ...
                }
            }
        } else {
            ...
        }
        ...
        if (didLayout) {
            //對ViewTree 進行Layout ---------- (4)
            performLayout(lp, mWidth, mHeight);
            ...
        }
        ...
        if (!cancelDraw) {
            ...
            //開始ViewTree Draw過程 ------- (5)
            performDraw();
        } else {
            ...
        }
    }

來看看標注的重點:
(1)
measureHierarchy(xx)

#ViewRootImpl.java
    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                                     final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;
        ...

        //標記是否測量成功
        boolean goodMeasure = false;
        //寬度為wrap_content時
        if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            final DisplayMetrics packageMetrics = res.getDisplayMetrics();
            res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
            int baseSize = 0;
            if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                baseSize = (int)mTmpValue.getDimension(packageMetrics);
            }
            //baseSize 為預置的寬度
            //desiredWindowWidth 想要的寬度是否大于預置寬度
            if (baseSize != 0 && desiredWindowWidth > baseSize) {
                //以baseSize 作為寬度傳入
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                //測量----------------- 第一次
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                //如果ViewTree的子布局需要的寬度大于父布局能給的寬度,則該標記被設置
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    //該標記沒被設置,說明父布局給的尺寸夠用,測量完成
                    goodMeasure = true;
                } else {
                    //父布局不能滿足子布局的需求,嘗試擴大寬度
                    //desiredWindowWidth > baseSize,因此新計算的baseSize要大于原先的baseSize
                    baseSize = (baseSize+desiredWindowWidth)/2;
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                    //拿到后繼續測量----------------- 第二次
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    //繼續檢測是否滿足
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                        goodMeasure = true;
                    }
                }
            }
        }

        //沒測量好,繼續測量
        if (!goodMeasure) {
            //可以看出是為RootView 生成MeasureSpec
            //傳入的參數:能給RootView分配的最大尺寸值以及RootView本身想要的尺寸(記錄在LayoutParams里)
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

            //既然MeasureSpec 有了,那么就可以測量RootView了
            //該過程就是測量整個ViewTree----------------- 第三次
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                //Window尺寸變化了,用于后續判斷執行performMeasure(xx)
                windowSizeMayChange = true;
            }
        }
        ...
        return windowSizeMayChange;
    }

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

            case ViewGroup.LayoutParams.MATCH_PARENT:
                //RootView 希望填充Window,則滿足它,此時它尺寸是確切值
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                break;
            case ViewGroup.LayoutParams.WRAP_CONTENT:
                //RootView 希望根據自身內容來確定尺寸,則設置為AT_MOST 模式
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                break;
            default:
                //RootView 希望直接指定尺寸值,則滿足它,此時它尺寸是確切值
                measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                break;
        }
        return measureSpec;
    }

以上代碼主要做了兩件事:

1、結合Window尺寸,確定RootView 的測量模式和預估測量值(MeasureSpec)
2、根據第一步的結果,發起對ViewTree的測量(從RootView開始)

(2)
經過對ViewTree的測量后,RootView的測量值已經確定了。
來看看relayoutWindow(xx):

#ViewRootImpl.java
    private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
                               boolean insetsPending) throws RemoteException {
        ...
        //重新設置Window大小
        //傳入的尺寸值為RootView的尺寸值
        //返回Window尺寸值存放在 mTmpFrame里
        int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
                (int) (mView.getMeasuredWidth() * appScale + 0.5f),
                (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
                insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
                mTmpFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
                mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
                mPendingMergedConfiguration, mSurfaceControl, mTempInsets);
        //關聯Window和surface
        if (mSurfaceControl.isValid()) {
            mSurface.copyFrom(mSurfaceControl);
        } else {
            destroySurface();
        }
        
        //記錄Window 尺寸
        setFrame(mTmpFrame);
        return relayoutResult;
    }

我們發現:

  • Window的尺寸是依賴于RootView的測量尺寸,并且一般來說appScale=1,也就是說Window尺寸就是RootView的尺寸。
  • 此處也即是解釋了之前的Demo現象。

而(1)步驟的測量ViewTree是為了確定RootView的尺寸從而在此步驟確定Window尺寸。

(3)(4)(5)

這里分析經典問題:onMeasure() 為什么會執行多次?

這三部分即是我們熟知的View 的三大過程,此處值得注意的是:
步驟(1)
在(1)步驟里的measureHierarchy(xx),我們標注了三次測量。

1、第一次:先用預置寬度測量ViewTree,得到測量結果
2、發現第一次測量結果不滿足,因為存在需要寬度比預置寬度大的子布局,于是給子布局更大的寬度,再進行第二次測量
3、發現第二次測量結果仍然不滿足,于是用Window能夠拿到的最大寬度再進行測量

可以看出measureHierarchy(xx)里至少執行一次performMeasure(),最多能執行三次。而調用performMeasure()方法,該方法最終會調用到各個View的onMeasure()。
每次performMeasure() 一定能夠觸發執行onMeasure()嗎?會。
原因:
再次簡單回顧一下measure(xx)代碼:

    public final void measure(xx) {
        ...
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
        //兩個條件滿足其中一個
        //1、需要強制layout
        //2、尺寸發生改變
        if (forceLayout || needsLayout) {
            ...
            onMeasure();
            ...
        }
        ...
    }

從上面可知,能執行onMeasure(),說明上述條件滿足了。
needsLayout肯定是不滿足的了,因為View尺寸沒變過。
那么只能是forceLayout=true了。在第一次測量ViewTree的時候,僅僅只是走了Measure過程,并未走Layout過程。而我們知道PFLAG_FORCE_LAYOUT 標記是在Layout結束后清空的,因此此處PFLAG_FORCE_LAYOUT 標記并沒有清空,當然needsLayout=true滿足條件。
詳細的Measure/Layout/Draw 系列請移步:

步驟(3)
而在(3)步驟又再次測量了ViewTree,此時View/ViewGroup onMeasure() 再次執行。

結合步驟(1)、步驟(3)總結一下:

在步驟(1)里至少執行了一次測量,最多執行三次
而在步驟(3)里也會執行一次測量

當requestLayout的時候,步驟(1)一定會執行,也就是說保底執行一次performMeasure(xx)->onMeasure()。
而步驟(3)在第一次展示View的時候會執行或者窗口尺寸發生變化會執行。
于是我們得出如下結論:

1、第一次展示View的時候,步驟(1) 、(3)一定會執行,因此onMeasure()至少執行兩次
2、后續通過requestLayout()觸發時,不一定執行步驟(3),因此此時onMeasure()可能只會執行一次

這就是onMeasure() 為什么會執行多次的原因
什么時候步驟(1)會執行三次測量?
一般來說,步驟(1)里通常只會走第三次測量,第一次、第二次不會走,因為對于DecorView作為RootView來說,lp.width == ViewGroup.LayoutParams.MATCH_PARENT。不滿足走第一次、第二次的條件。所以通常只會看到onMeasure()執行兩次,而不是多次。
當然我們可以滿足它條件,讓onMeasure()執行5次。

  #展示懸浮窗
    private void testMeasure() {
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
        //一定要是WRAP_CONTENT
        layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;

        //TransView為自定義View
        final TransView transView = new TransView(this);
        windowManager.addView(transView, layoutParams);
    }

    #重寫TransView onMeasure(xx)
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //該View 需要寬度大于屏幕寬度
        int width = resolveSizeAndState(10000, widthMeasureSpec, 0);
        setMeasuredDimension(width, View.MeasureSpec.getSize(heightMeasureSpec));
    }

TransView 為自定義View,TransView 作為RootView。
以上代碼主要做了兩件事:

1、約束Window的寬為WRAP_CONTENT
2、讓其子View(TransView)申請大于屏幕寬度的寬度

想要驗證執行多少次,在TransView onMeasure(xx)打印即可。

另外,除了上述原因導致onMeasure()執行多次外,onMeasure()有可能執行更多次,比如FrameLayout在測量子布局的時候,在某些條件下會再次觸發child.measure()過程,此時算起來子布局的onMeasure()執行次數可能就會更多了,有興趣可以看看FrameLayout->onLayout(xx)。

onMeasure()為什么要執行兩次

我們知道了會執行兩次的原因,為什么這么設計呢?
不考慮特殊情況,View在第一次展示的時候會執行兩次onMeasure(xx)。
前面提到過只要執行了requestLayout(),步驟(1)一定會執行。
步驟(1)執行的目的是為了獲取RootView的測量值,而RootView的測量值會在relayoutWindow(xx)用來重新確定Window的寬高,而步驟(3)的執行在relayoutWindow(xx)之后,因此步驟(1)就有執行的必要了。

至此,我們知道了RootView與Window的尺寸是如何確定了。

3、Window、ViewRootImpl、View 三者關系

上邊涉及到了Window、RootView,所用的方法基本都是ViewRootImpl里提供的。那么三者到底是個什么關系呢?
RootView需要添加到Window里才能展示,但是Window并不是直接管理RootView,而是通過ViewRootImpl進行管理。


image.png

ViewRootImpl可以看做是Window、RootView的中間者,負責協調這兩者。

那么Window與RootView又是什么關系的?
你可能已經發現了,addToDisplay(xx)并沒有傳入RootView,那么RootView是如何添加到Window里呢?
實際上,這里的添加說法比較擬人化。
在relayoutWindow(xx)里,傳入了mSurfaceControl,返回后就與Surface mSurface 建立了聯系。也就是說底層的Surface與Java 層的Surface關聯起來了。而通過Surface,能夠拿到Canvas。而每個View的繪制都需要關聯Canvas,以此類推,View就與Surface關聯了,那么在View上進行的繪制都會反饋到Surface上。這也就是說View添加到了Window。


image.png

本文基于Android 10.0

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容