1. 程式人生 > >從原始碼的角度解析 view 的測量流程

從原始碼的角度解析 view 的測量流程

首先,我們在這裡提出兩個疑問:

  1. viewGroup 是如何遍歷所有的子 view, 並進行測量的? 這個遍歷最開始的起點是哪?
  2. viewGroup 為何沒有重寫 onMeasure 方法?  measureChildren , measure  , onMeasure 這些方法之間的呼叫關係是什麼?

在此說明一下, 此篇文章需要讀者瞭解 Android 視窗機制, 並且知道 ViewRootImp, WindowManger , Window , DecorView , PhoneWindow 等等這些東西是什麼. 下面我們開始探索!


1 :viewGroup 是如何遍歷所有的子 view?

我們知道 viewGroup 沒有重寫 onMeasure 方法, 但是有一個 measureChildren 方法, 我們看看這個方法的原始碼:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) { // 注意此處
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec); // 注意此處
            }
        }
    }

方法很簡單, 就是一個迴圈, 遍歷呼叫 measureChild 方法, 跟進看看:

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec); // 注意此處
    }

這個方法思路也很明確, 通過 getChildMeasureSpec 來獲取 childWidthMeasureSpec 的值, 然後傳入給 child measure 方法.

到了這裡, 你可能會覺得 viewGroup 應該就是通過 measureChildren 方法來遍歷所有的子 view, 然後呼叫 子 view 的 measure , 整個遍歷和測量流程就這樣結束了.

一開始我也這樣覺得, 但是通過搜尋各種 viewGroup 的實現子類你會發現, 這個方法根本就沒有在任何一個地方得到呼叫, 不信你可以去試試, 是不是很鬱悶?  其了個怪了.

到了這裡我們再來想想, 如果 viewGroup 的子 view 還是一個 viewGroup 呢? 那麼按照上面這個流程, 它會把這個 viewGroup 當做一個 view 來進行測量, 並呼叫 measure 方法, 我們看看 measure 方法.(方法有點長, 看關鍵的一行)

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
         ......
 
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec); // 注意這裡
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

          ......

    }

我們看到呼叫了 onMeasure 方法, 那麼問題來了, 既然 viewGroup 沒有重寫 onMeasure 方法, 那麼在 measure 內部呼叫的方法不是 view 的 onMeasure 方法嗎? view 的測量和 ViewGroup 的測量邏輯很明顯不一樣啊, 為什麼會這樣呼叫呢?  這裡只能說別忘了 ViewGroup 是個抽象類, 是不可能有直接例項物件的. 那這就好說了, 我們看看 FrameLayout 的 onMeasure 方法.

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) { // 1
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); // 2
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        ......
    }

我們看到, 註釋一處有個迴圈,遍歷子元素的, 然後註釋 2 處呼叫了 measureChildWithMargins 方法,  跟進看看 :

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
    }

看到這個方法有沒有感覺和 measureChild 方法很像? 邏輯幾乎一模一樣.

到了這裡讀者可能明白了, ViewGroup 並不是通過 measureChildren 方法來遍歷所有子 view 的, 而是通過 不同的 ViewGroup 實現子類重寫 onMeasure 方法來進行遍歷所有的子 View ,並呼叫其 measure 方法, 然後如果 子 view 依然是個 ViewGroup , 呼叫 measure 方法會時, 會呼叫不同的 ViewGroup 的 onMeasure 方法的實現, 然後在這個方法內部又去遍歷並呼叫所有 子 view 的 measure 方法, 如此反覆便完成了所有的 view 的遍歷.

那麼你可能會問了 , 那既然這樣, 官方為什麼還要給 viewGroup 提供一個 measureChildren 方法呢? 細心的你可能會發現, 這個方法是 protected , 那麼也即是說只能在其子類中使用, 那麼在我們自定義 viewGroup 時, 是不是也得在 onMeasure 方法內部遍歷所有的 子 view? 那這個方法就派上用場了, 所以說這個方法是官方提供的一個讓我們快速遍歷所有 子 view 的一個便利而已, 而系統實現的 viewGroup 並沒有用到這個方法.

這個遍歷最開始的起點是哪?

如果讀者瞭解 Android 視窗機制的話, 我們便知道, 在一個 activity  中會有一個 window, 這個 phoneWindow 便是它的唯一實現子類. 然後我們在 phoneWindow 內部發現了一個 DecorView , 並且明確的註釋到這是 window 中的頂級 view.

// This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

那麼肯定就可以確定這個遍歷的起點是從 DecorView 開始的, 口說無憑, 下面我們從原始碼一步步跟進來證明這個結論:

首先在這裡說一下, activity 持有的 window 物件是 phoneWindow, 然而我們如果要和這個 window 進行互動, 增加,移除或者更新 一個 view , 那麼只能通過官方提供的 windoManager 介面. 而這個介面的實現類為 WindowManagerImp, 而我們可以通過檢視原始碼發現, 這個  WindowManagerImp 的絕大部分方法都是由一個 WindowManagerGlobal 成員變數來代理實現的,(這裡的原始碼讀者可以自行檢視). 我們進入到這個 WindowManagerGlobal 內看看其 addView 方法:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
 
        ......

        ViewRootImpl root; // 1
        View panelParentView = null;

        ......

        root = new ViewRootImpl(view.getContext(), display); // 2

        view.setLayoutParams(wparams);

         // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView); // 3
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }
 

我們看到, 在這個 方法內部具體的實現細節則是通過呼叫 root.setView 方法. 我們看看 viewRootImp 這個類的說明.

/**
 * The top of a view hierarchy, implementing the needed protocol between View
 * and the WindowManager.  This is for the most part an internal implementation
 * detail of {@link WindowManagerGlobal}.
 *
 * {@hide}
 */

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks 

看到, 官方說明到, 這個類是 WindowManagerGlobal 內部的大部分實現細節, 它是這個view 層級的最頂部.

到了這裡我們就可以解釋為什麼說 viewRootImp 是 WindowManager 和 DecorView 之間的橋樑了, 因為和 window 互動( window 互動其實就是和 DecorView 互動) 要通過 WindowManger,  而 windowManger 最底層的實現細節是 viewRootImp .

到了這裡我們也可以解釋為什麼是由 ViewRootImp 來控制著整個 view 層級測量, 佈局 和繪製了.

接下來我們看看其 viewRootImp 的 performMeasure 方法:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
 

我們看到, 呼叫了一個 mView 成員變數的 measure 方法, 這個成員變數沒有任何註釋, 無奈, 我麼只能通過檢視其在哪賦值的來推斷它是什麼, 搜尋後發現, 他在 viewRootImp 的 setView 方法內部進行了賦值:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;

            }
         ......
         }

而我們剛剛看到 在 WindowManagerGlobal 的 addView 方法內部的 註釋 3 處呼叫了 此方法, 現在我們只需知道 WindowManagerGlobal 的 addView 方法在哪呼叫並且傳入的第一個 view 引數是什麼便能揭曉真相!

最後, 通過搜尋原始碼, 終於在 ActivityThread 內部的 handleResumeActivity 方法內發現了這個方法的呼叫,

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ......

        // TODO Push resumeArgs into the activity for consideration
        r = performResumeActivity(token, clearHide, reason); //1

         if (r != null) {
            final Activity a = r.activity;

        ......


        if (r.window == null && !a.mFinished && willBeVisible) {
                ......

                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView(); //3

                ViewManager wm = a.getWindowManager(); // 4
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;

                ......

                if (r.mPreserveWindow) {

                    ......

                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l); // 5
                    } 
                   ......
                }
 

我們看到, 註釋 1 處執行了 activity 的 resume 方法之後, 在註釋 3 處獲取了 DecorView, 在註釋 4 處獲取了 wm(WindowManger 繼承了 ViewManger介面), 然後 在註釋 5 處呼叫了 add view 方法, 明顯的看到 引數是 DecorView, 第二個引數是 LayouParams, 你可能會看到引數套不上, 這裡只傳入了兩個, 而 WindowManagerGlobal 的 addView 方法有四個, 這裡我們看看 WindowMangerImp 的 addView 方法就一了百了了:

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

到了這裡, 我們證明了 view 層級的 measure 遍歷的確是從 DecorView 開始的, 而且你細心的話你會發現還是在 performResumeActivity 之後才開始的, 這樣可以解釋為什麼 onCreate, onResume 方法內部獲取不到 view 的寬高資訊了.


2 : viewGroup 為何沒有重寫 onMeasure 方法?

前面我們說到, 不同的 viewGroup 會有不同的測量實現方案, 比如 LinearLayout 和 FrameLayout 的測量細節是明顯不一樣的, 所以這個方法放在了具體的實現子類中進行了重寫, viewGroup 無法做到統一的重寫.


measureChildren , measure  , onMeasure 這些方法之間的呼叫關係是什麼?

我們在開頭的時候分析了, measure  方法內部會呼叫到 onMeasure 方法, 然後 measureChildren 方法在系統實現的這些 viewGroup 中沒有任何一個地方呼叫, 這個方法是用在我們自定義的 viewGroup 的 onMeasure 方法內部的.


以上便是全部的分析過程, 如果有誤,歡迎評論指正,謝謝!!!