Android View的工做原理(上)

1、前言

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

2、理解ViewRoot和DecorView

在正式講解View的工做原理以前,咱們先了解一下ViewRoot,ViewRoot的實現類是ViewRootImpl,它是鏈接WindowManager和DecorView的紐帶,View的三大流程都是經過ViewRoot來完成的,它是在ActivityThread中被初始化的。View的繪製流程是從ViewRoot的performTraversals開始的,經歷三個步驟後最終呈如今界面的view,大體以下: 面試

performTraversals會依次調用perfornMeasure、performLayout、performDraw,這三個分別完成頂級View的measure、layout、draw,performMeasure再去調用measure,最後去調用onMeasure完成子view的測量,子view會再去調用measure,依次遞歸下去,直到因此的子view measure完畢。

下面來簡單講一下DecorView,以下圖:bash

DecorView是全部View的頂級View,它裏面有個LinearLayout,分爲title bar和content,咱們常常在onCreat裏面用到的setContentView方法就是爲這個content設置佈局的,也就是說,咱們寫的佈局都塞進了這個content,哦。。。。,明白了,這就是爲啥要叫setContentView而不叫setView了吧。

3、理解一下MeasureSpec

3.1 MeasureSpec的概念

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

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方法,post

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方法ui

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。咱們把上述規則總結到一張表裏面,方便記憶: spa

這裏不是我本身創造的一張表,而僅僅是對上述過程的一種解釋而已。可是有一種特殊的狀況咱們要注意一下, 就是當子view是warp_content的時候,無論父類是啥,結果都是同樣的,這就會有問題,怎麼辦呢,這就交給子view的onMeasure去處理吧,因此在自定義view的時候若是view 的params設置爲wrap_content的時候,咱們就要去實現onMeasure方法。具體的後面會講。

4、View的measure過程

view的三大過程當中,measure是最複雜的,由於每每要肯定一個view的大小,要經歷好屢次測量才能ok。measure過程要分狀況來看,View和ViewGroup,由於ViewGroup不只僅要測量本身還要測量子元素,一層一層傳遞下去。code

4.一、單個View的measure

View裏面的measure是final方法,這就意味着該類不容許被繼承,measure裏面調用了onMeasure方法,也就是說measure的工做就是在onMeasure裏面完成的,看看o n M e asure方法:orm

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.三、關於measure可能會遇到的坑

View的measure通常是很複雜的,某些狀況下得屢次測量,因此爲了保險起見,咱們應該在layout結束後再去獲取View的寬和高。在實際需求中,好比,在activity中,你怎麼獲取某個view的width和height呢?有的人確定會說,很簡答啊,直接在oncreat中去調用getWidth和getHeight,這確定是不行的,大家能夠去試試,這裏獲取到的極有多是空值,這是由於View的繪製和Activity生命週期不存在同步的關係,沒法保證在哪個週期View的測量工做已經完成了,因此不靠譜。這裏簡單提一下幾種常見的解決方案,可是不展開講解了:

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

五、總結

1.對於measure過程,咱們只要瞭解view和ViewGropup的大體流程就能夠了,尤爲注意 在自定義view 的時候要重寫onMeasure方法,而且給wrap_content佈局賦默認值。

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

相關文章
相關標籤/搜索