View measure源碼分析

View繪制流程

一圖勝千言


DecorView添加至窗口的流程

這里說明下Acitivity的onResume是在handleResumeActivity之前執行的,所以在onResume中獲取View的寬高為0。
實際上通用的繪制流程應該是從WindowManager#addView開始。

View measure()分析

首先View的Measure方法聲明為final,子類無法繼承,故關于View多態的實現就只能在onMeasure方法中實現

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
   //1.判斷View本身LayoutMode是否是視覺邊界布局,僅用于檢測ViewGroup
   boolean optical = isLayoutModeOptical(this);
   //2.判斷父容器是否是視覺布局邊界,是則重新調整測量規格(mParent可能是ViewRootImpl)
        if (optical != isLayoutModeOptical(mParent)) {
            //View的background需要設置.9背景圖才會生效,否則insets的left、right全為0
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
   // Suppress sign extension for the low bytes,禁止低位進行符號擴展
  //3.根據當前測量規格生成一個與之對應的key(相同的測量規格產生的測量值肯定一樣的),供后續索引緩存測量值
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
  //4.初始化測量緩存稀疏數組,該數組可自行擴容,只是初始化為2
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); 
  //5.如果強制layout(eg.view.forceLayout())或者本次測量規格與上次測量規格不同,進入該if語句
        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {
 final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
           // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();
  //6.如果需要強制layout則進行重新測量,反之則從緩存中查詢是否有與目前測量規格對應的key,
    //如果有則取用緩存中的測量值,反之則執行onMeasure方法重新測量,未索引到返回一個負數
  int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
  //7.targetSDK小于Kitkat版本(API20),sIgnoreMeasureCache則為true
        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;
            }
    // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }
            //8.標志位賦值,表明需要在layout方法中執行onlayout方法
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
       //9.緩存本次測量規格,及測量寬高
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
       (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}


  /**
     * Return true if o is a ViewGroup that is laying out using optical bounds.
     * @hide
     */
    public static boolean isLayoutModeOptical(Object o) {
        return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
    }

關于LayoutMode請參考Android LayoutMode需要翻墻
關于MeasureSpec請參考Android MeasureSpec
小結:
View的measure方法只是一個測量優化者,主要做了2級測量優化:
1.如果flag不為強制Layout或者與上次測量規格相比未改變,那么將不會進行重新測量(執行onMeasure方法),直接使用上次的測量值;
2.如果滿足非強制測量條件,即前后測量規格發生變化,則會先根據目前測量規格生成的key索引緩存數據,索引到就無需進行重新測量;如果targetSDK小于API 20則二級測量優化無效,依舊會重新測量,不會采用緩存測量值。
3.View#requestLayout 只會讓該View及其父容器重新走一遍,如果該View是ViewGroup,其里面的子View測量優化還是依舊有效的

View onMeasure()分析

View的onMeasure方法比較簡單,目的是將測量值賦給mMeasuredWidth和mMeasuredHeight

  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

 public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        //未指定則使用最小值
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        //解析測量規格獲得寬、高,這就是為何View無論你填match_parent還是wrap_content,它始終是填滿父容器的原因
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

 private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        //表明測量尺寸已經設置,與measure方法中的first clears the measured dimension flag相呼應
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

小結:
View的onMeasure方法才是真正的測量者,它根據測量規格以及其他條件來決定自己最終的測量大小。
需要注意,自定義View重寫該方法時,務必保證調用setMeasuredDimension()將測量寬、高存起來,measure方法分析中有提到,如果不調用該方法將會拋出非法狀態異常。

ViewGroup onMeasure分析

ViewGroup繼承至View實現了ViewParent接口,是一個抽象類。前面也提到View的measure方法不能被繼承,所以ViewGroup沒有measure方法。查看源碼發現它并沒有重寫onMeasure方法,那就去看看其實現類是否有重寫,就看最簡單的實現類FrameLayout。

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        //判斷是否要再次測量layout_width/height屬性為match_parent的child
        //即FrameLayout的layout_width/height屬性為wrap_content,則會再次測量屬性為match_parent的child
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();
        //首次遍歷測量子控件
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //GONE類型child不測量,這就是為何GONE不會占用位置,因為沒有測量
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                ..................
                }
            }
        ..................
        //保存自身的測量寬高
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        //再次測量layout_width/height屬性為match_parent的child,部分View三次執行onMeasure的原因
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                  ..........
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
}

進入MeasureChildWithMargins函數

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //根據ViewGroup自身的測量規格生成child的測量規格
        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方法,并將child的測量規格傳遞給child
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

進入getChildMeasureSpec方法,看看到底是如何測量child的規格的

 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //獲取父容器的測量模式、測量大小
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        //計算出父容器允許child的最大尺寸
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // 父容器測量模式為精確模式
        case MeasureSpec.EXACTLY:
        //如果child的layout_width/height為具體的數值eg.20dp,那么child的測量規格就為大小20dp,模式為精確模式
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            }
 //如果child的layout_width/height為MATCH_PARENT,那么child的測量規格就為大小父容器允許的最大值,模式為精確模式
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } 
 //如果child的layout_width/height為WRAP_CONTENT,那么child的測量規格就為大小父容器允許的最大值,模式為AT_MOST
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

小結:
ViewGroup的測量主要是根據其自身測量規格,結合child的LayoutParams進行判斷分析,生成一個child的測量規格信息,傳遞給child的measure方法。所謂的測量規格即是一個建議,建議view的寬、高應該為多少,至于采取與否完全取決于view自己(嗯應該是取決于程序員O(∩_∩)O!)。
另ViewGroup提供了三個測量方法供我們使用,在實際運用中可以偷偷懶,不用自己去實現測量邏輯:

  1. measureChildWithMargins 測量單個child,margin參數有效
  2. measureChild 測量單個child,margin參數無效
  3. measureChildren 測量所有child內部調用measureChild

延伸閱讀:
【View為什么會至少執行2次onMeasure、onLayout】暫未發布

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

推薦閱讀更多精彩內容