Android View的工作原理(上)

wangchao05發表於2019-05-12

一、前言

眾所周知,android為我們提供大量的基礎控制元件,這些控制元件完成基本功能是沒有問題的,也比較全面,但是對於一些比較精緻的產品,不僅僅是基礎功能實現就OK,它們往往要很炫的效果,這就需要自定義view了,好了不多說了,直接開始主題,View的繪製分為measure、layout、draw,其中測量是最複雜的,我們單獨來講,佈局和繪製將在下一篇文章去講解。

二、理解ViewRoot和DecorView

在正式講解View的工作原理之前,我們先了解一下ViewRoot,ViewRoot的實現類是ViewRootImpl,它是連線WindowManager和DecorView的紐帶,View的三大流程都是通過ViewRoot來完成的,它是在ActivityThread中被初始化的。View的繪製流程是從ViewRoot的performTraversals開始的,經歷三個步驟後最終呈現在介面的view,大致如下:

Android View的工作原理(上)
performTraversals會依次呼叫perfornMeasure、performLayout、performDraw,這三個分別完成頂級View的measure、layout、draw,performMeasure再去呼叫measure,最後去呼叫onMeasure完成子view的測量,子view會再去呼叫measure,依次遞迴下去,直到所以的子view measure完畢。

下面來簡單講一下DecorView,如下圖:

Android View的工作原理(上)
DecorView是所有View的頂級View,它裡面有個LinearLayout,分為title bar和content,我們經常在onCreat裡面用到的setContentView方法就是為這個content設定佈局的,也就是說,我們寫的佈局都塞進了這個content,哦。。。。,明白了,這就是為啥要叫setContentView而不叫setView了吧。

三、理解一下MeasureSpec

3.1 MeasureSpec的概念

可以說,這個概念是貫穿了整個View繪製的所有流程,是的,表面上看它就是一個尺寸規格,也就是決定View的大小,它絕大部分都可以決定View的大小,當然也不是它一個人說了算,畢竟有些ViewGroup的LayoutParams也對子view的大小有影響。

Android View的工作原理(上)
MeasureSpec代表一個32位的int值,其中高兩位是mode,低30位是size,其中mode的三種值,分別是:

  • UNSPECIFIED:父容器不對View做任何限制,要多大就給多大,這種主要用於系統內部,應用層開發一般用不到。
  • AT_MOST:就是子View的值根據自己定義的大小來給定,但是不可以超過父類的大小,相當於LayoutParams的wrap_content。
  • EXACTLY:父類已經檢測到了子View的精確大小了,這時候View的大小就是SpecSize,它對應LayoutParams的match_parent和具體值這兩種情況。

3.2 MeasureSpec和LayoutParams的對應關係

上面提到過了,View的大小是由MeasureSpec來決定的,我們一般會給view設定LayoutParams引數,這個params引數會在父容器的MeasureSpec約束的情況下轉換為對應的MeasureSpec,這個Spec會最終確定view的測量大小,也就是說view的大小是由父容器的MeasureSpec和view的LayoutParams共同決定的,MeasureSpec一旦確定後,onMeasure就可以確定view的測量寬高了。

View的measure過程是由ViewGroup的measure傳遞來的,這裡看一下ViewGroup的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);
    }
複製程式碼

可以看到,在測量子view的時候會去獲取子view的MeasureSpec,這裡詳細看一下getChildMeasureSpec方法

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } 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;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.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;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
複製程式碼

這個方法有點長,但是很簡單,它就是根據父容器的MeasureSpec和子view的LayoutParams來確定子view的MeasureSpec。我們把上述規則總結到一張表裡面,方便記憶:

Android View的工作原理(上)
這裡不是我自己創造的一張表,而僅僅是對上述過程的一種解釋而已。但是有一種特殊的情況我們要注意一下, 就是當子view是warp_content的時候,不管父類是啥,結果都是一樣的,這就會有問題,怎麼辦呢,這就交給子view的onMeasure去處理吧,所以在自定義view的時候如果view 的params設定為wrap_content的時候,我們就要去實現onMeasure方法。具體的後面會講。

四、View的measure過程

view的三大過程中,measure是最複雜的,因為往往要確定一個view的大小,要經歷好多次測量才能ok。measure過程要分情況來看,View和ViewGroup,因為ViewGroup不僅僅要測量自己還要測量子元素,一層一層傳遞下去。

4.1、單個View的measure

View裡面的measure是final方法,這就意味著該類不允許被繼承,measure裡面呼叫了onMeasure方法,也就是說measure的工作就是在onMeasure裡面完成的,看看o n M e asure方法:

   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
複製程式碼

程式碼很簡單,setMeasuredDimension設定測量的值,主要的是getDefaultSize方法

    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;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
複製程式碼

getDefaultsize就是返回測量後的大小,這裡注意是測量後的大小,因為view的大小最終確定是在layout後,有時候layout也會對view的大小造成影響,不過絕大部分getDefaultsize就是最終view的大小。

注意的點: 從getDefaultsize方法可以看到,view的大小由specSize來決定,所以,直接繼承View的自定義控制元件需要重寫onMeasure方法並且設定wrap_content時的自身大小,否則在佈局中的wrap_content和match_parent就沒有什麼區別了,從上面的表格也可以清晰的看到,這種情況是我們不希望看法的,怎麼解決呢?很簡單,我們設定一個預設值就可以了

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //如果view在佈局中使用wrap_content ,這時候就是AT_MOST,我們需要在onmeasure裡面做特殊處理,否則和match_parent就沒有區別了
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(500, 300);
        }else if (heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize, 300);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(500, heightSpecSize);
        }
    }
複製程式碼

具體的預設值是多少,我們根據自己的情況來定。

4.2 ViewGroup的measure過程

對於ViewGroup的measure過程,它會更加複雜一點,因為它不僅要measure自己,還要measure子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);
            }
        }
    }
複製程式碼

上面的方法很明瞭,就是對每一個子元素進行measure,我們看看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);
    }
複製程式碼

這也太簡單了,就是獲取子元素的measure spec值,然後呼叫view的measure操作,這個和單獨的view就沒啥區別了,就這樣一直迭代下去,直到單個的view測量結束。這是測量子元素的過程,那麼ViewGroup怎麼測量自己的呢。

其實ViewGroup並沒有定義其測量的具體過程,因為它是一個抽象類,其測量過程onMeasure交給了其子類去實現了,比如LinearLayout類就有自己專門的onMeasure方法,這也是符合邏輯的,因為沒個Layout都有自己的特性,我們不可能在ViewGroup統一去處理。 我們以LinearLayout為例,看如下程式碼:

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }
複製程式碼

看垂直方向的,水平方向的類似:

 void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;
        ...

        final int count = getVirtualChildCount();

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        boolean matchWidth = false;
        boolean skippedMeasure = false;

        final int baselineChildIndex = mBaselineAlignedChildIndex;
        final boolean useLargestChild = mUseLargestChild;

        int largestChildHeight = Integer.MIN_VALUE;
        int consumedExcessSpace = 0;

        int nonSkippedChildCount = 0;

        // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            nonSkippedChildCount++;
            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
      
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
            
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {
                /*
                 * Widths of weighted Views are bogus if we end up
                 * remeasuring, so keep them separate.
                 */
                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }

        if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
            mTotalLength += mDividerHeight;
        }


        if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
            maxWidth = alternativeMaxWidth;
        }
       ...
        maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

        if (matchWidth) {
            forceUniformWidth(count, heightMeasureSpec);
        }
    }
複製程式碼

這個方法非常長,系統會遍歷每個子元素,並且呼叫子元素的measureChildBeforeLayout方法:

    void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
    }
複製程式碼

這個方法內部又在執行measure子元素的操作,當子元素全部測量完畢後,Linearlayout才會去測量自己的大小。

注意的點 : 測量的過程是從父類開始分發,遞迴的測量子元素,最後再測量父類。layout的過程是恰好相反的,我們後面再講。

4.3、關於measure可能會遇到的坑

View的measure一般是很複雜的,某些情況下得多次測量,所以為了保險起見,我們應該在layout結束後再去獲取View的寬和高。在實際需求中,比如,在activity中,你怎麼獲取某個view的width和height呢?有的人肯定會說,很簡答啊,直接在oncreat中去呼叫getWidth和getHeight,這肯定是不行的,你們可以去試試,這裡獲取到的極有可能是空值,這是因為View的繪製和Activity生命週期不存在同步的關係,無法保證在哪一個週期View的測量工作已經完成了,所以不靠譜。這裡簡單提一下幾種常見的解決方案,但是不展開講解了:

  • Activity的onWindowsFocusChanged,在這個裡面去獲取寬和高。
  • view.post(runnable),等到訊息佇列開始執行的時候,view肯定是ready狀態了。
  • ViewTreeObserve 重寫addOnGlobalLayoutListener方法。

5、總結

1.對於measure過程,我們只要瞭解view和ViewGropup的大致流程就可以了,尤其注意 在自定義view 的時候要重寫onMeasure方法,並且給wrap_content佈局賦預設值。

2.獲取某個view寬和高的時機,這個很重要,在面試中經常被考察到。

相關文章