一篇文章理解Android 視圖樹的測量過程

好久沒有寫文章了,最近公司社招窗口重新打開了,又忙着面試,在面試過程中發現自己已經有些不知道問候選人什麼問題了...大寫的尷尬。特別是現在很多同學準(各)備(種)充(背)分(書),通常我剛問請你描述一下AndroidView的測量過程,候選人已經開始如長江流水滔滔不絕地背書,怎麼去甄別他們是真懂還是短時間突擊?短時間突擊不是不可以,我們需要的人才是真正能夠理解這個過程的人,知其然而且知其所以然,這樣在真正項目中遇到問題的時候,你才能快速定位到問題。基於此,我只好把這塊東西的源碼再過一遍,其實今天的這篇文章是我14年發表在公司內部wiki上面的博文,稍微整理一下,放出來跟大家分享一下吧。

如果你能回答出來如下的問題,那麼這篇文章可能對你沒有太大的幫助,你可以略過了。也歡迎大家在評論中提出自己的答案,我們可以一起討論討論。

  • 自定義一個ViewGroup需不需要重寫onMeasure?爲什麼?
  • 我們在一個ViewGroup容器中(比如LinearLayout)加入一個View(android.view.View)爲啥設置match_parent和設置wrap_content效果一樣?
  • View.getWidthView.getMeasureWidth的值在整個繪製流程中是否一樣?在繪製完成之後兩個值是否一樣?
  • 如果一個自定義View需要支持wrap_content設置的值,那麼它需要做什麼?
  • 如果我給一個View設定了一個layout_width="100px",那麼是否在任何佈局裏面它都會展示成100個像素?

帶着這些問題,我們進入今天的正文:

1 測量流程主線

AndroidView的測量是一個比較複雜的過程,但是在Android中所有跟視圖樹相關的內容,請你記住一條原則,他們都是從根佈局開始,然後遍歷到葉子View,抓住這根主線之後,我們來看看AndroidMeasure的過程吧。

首先,整個過程的開始都是在ViewRootImpl中開始的,至於ViewRootImpl是個什麼東東,它實際上是在ActivityWindow中間的一個代理層,系統消息都是通過發送到ViewRootImpl,來觸發整個視圖樹的響應,ok,你瞭解到這裏就行了,如果需要詳細知道系統消息具體是怎麼流轉到ViewRootImpl,建議你在網上搜索一下吧,很多文章都是描述這個過程,ViewRootImpl有個方法performTraversals(),它是整個視圖樹進行繪製的入口,我們常說的繪製三大流程都是在這裏觸發的。

private void performTraversals() {  
  //省略代碼
  //measure入口
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);        
  //省略代碼
  performLayout(lp, desiredWindowWidth, desiredWindowHeight);     
   //省略代碼
  performDraw();                                                      
}

既然performMeasure是入口,那麼它具體是怎麼做的呢?因爲ViewRootImpl會持有ActivityDecorView,所以在performMeasure中,它就會直接去調用DecorViewmeasure方法,我們知道DecorView是整個Activity的視圖樹的根佈局,通常情況下它是一個FrameLayout(所以它自然是一個ViewGroup),所以這裏就開啓了從上往下的遍歷measure過程。

注意,這篇文章基於的SDK版本可能是5.x,如果你是4.x或者更老的版本,ViewRootImpl裏面是沒有這裏所說的performMeasure performLayout performDraw方法的,它是直接調用Decorviewmeasure layout draw方法,本質上沒有啥區別。

View中和測量過程相關的方法有三個,measureonMeasuresetMeasuredDimension。相應的,View的測量過程有三步:

  • 由父View調用public final void measure(int widthMeasureSpec, int heightMeasureSpec),如果是最外層的DecorView,我們前面已經說了它是通過ViewRootImpl觸發的。這個方法定義成final,表示Android不希望開發者改變整個視圖樹的measure流程。
  • measure調用onMeasure(int widthMeasureSpec, int heightMeasureSpec),這裏是View實現測量的核心邏輯,開發者可以重寫這個方法,達到修改viewmeasure效果的作用。ViewGroup基本上肯定需要自定義這個方法。注意,在這個方法中必須調用setMeasuredDimension,否則會報異常。
    3、onMeasure中必須要調用setMeasuredDimension(int measuredWidth, int measuredHeight),設置測量的結果。

根據前面說的,我們大概已經知道了視圖樹的整體測量流程:



下面我們再來看看整個測量過程中的具體的細節。

2 測量過程的細節

既然我們已經找到測量視圖樹的入口了,是不是就可以開始接着往下擼源代碼了呢?稍等,我們先來了解一下整個測量流程中一個非常重要的類:View.MeasureSpec

2.1 View.MeasureSpec

在測量過程中,你可以看到父View和子View之間的數據傳遞就是普通的int類型,比如measure(int widthMeasureSpec, int heightMeasureSpec)的函數原型。在Android中,這個int類型其實包含了兩部分信息:大小(specSize)和模式(mode),mode指的是父View期望子View按照某種建議去測量,specSize是具體的大小。其中高兩位表示mode、低三十位表示specSize。爲了避免我們自己去進行這些移位操作,Android提供了一個工具類MeasureSpec,可以方便的根據它去操作,生成一個包含modespecSize的int值。

mode有三種類型:

  • EXACTLY:父View希望子View直接使用傳給子view給的specSize。(當然,子view按不按這個來,具體子View的onMeasure說了算)
  • AT_MOST:父View希望子View最多隻能是specSize中指定的大小,子View需要保證不會超過specSize
  • UNSPECIFIED:父View對子View沒有要求,你想怎麼來,看你自己的脾氣。

大家都知道,在AndroidView其實並不是一個抽象類,也就是我們可以直接new出來一些View的實例,那麼View肯定也處理了measure過程,我們看看它是怎麼做的吧:

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

我們可以看到View的默認實現很簡單,直接調用了setMeasuredDimension()設置測量的結果。其中getSuggestedMinimumWidthgetSuggestedMinimumHeight都是我們通常給View設置的最小寬高,比如android:minWidth="23dp"。我們接着來看看getDefaultSize()這個函數,跟進去看看:

public static int getDefaultSize(int size, int measureSpec) {
    //size 的值就是外面傳進來的最小值
    int result = size;
    //父View傳給子View的模式
    int specMode = MeasureSpec.getMode(measureSpec);
    //父View傳給子View的大小
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
     case MeasureSpec.UNSPECIFIED:
        // 代碼1
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        // 代碼2
        result = specSize;
        break;
    }
    return result;
}

我們看到View默認實現的測量還是挺簡單的,代碼1處,如果父View指定的ModeUNSPECIFIEDView直接返回它自己最小值。代碼2處,AT_MOSTEXACTLY都是直接返回父View傳遞進來的值。

看到這裏,我相信你已經有點蒙逼了,最大的疑惑是父View傳進來的ModeSize是怎麼算的?下面我就來解決這個疑惑吧,我們把代碼切到ViewGroup中來。

2.2 ViewGroup的Measure流程

我們注意到ViewGroup它是一個抽象類,所以我們並不能直接new一個ViewGroup實例,那我們繼承一個試試:

 public class MyView extends ViewGroup {
    public MyView(Context context, AttributeSet attrs) {
      super(context, attrs);
    }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for(int i=0;i<getChildCount();i++) {
        View child = getChildAt(i);
        child.layout(l,t,child.getMeasuredWidth(),child.getMeasuredHeight());
    }
  }
}

這裏爲了演示方便,我們直接把MyView中所有的子View放到了左上角(onLayout中處理),xml中這樣指定:

  <com.chuyun932.learn.view.MyView
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:background="#ff0000"
      xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView android:layout_width="match_parent"
          android:layout_height="100dp"
          android:background="#00ff00"
          android:text="測試"
          android:id="@+id/test"
    />
</com.chuyun932.learn.view.MyView>

我們看一下頁面run起來的結果:

我們明明給TextView設置了match_parent100dp的高度,結果View並沒有顯示到界面中間來,你能解釋爲什麼嗎?因爲MyView並沒有重寫ViewonMeasure,所以在View的默認實現中,它只會去measure自己(當前是MyView),所有MyView的子View都得不到measure的機會,所以他們的getMeasureWidth都是0,那麼在Layout階段我們依據measure的值去佈局的時候,自然也就不會給它分配佈局空間了。

雖然ViewGroup沒有實現omMeasure的過程,但是它提供了兩個工具方法:measureChildren()getChildMeasureSpec()

 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);
        }
    }
  }

measureChildren中,ViewGroup對每一個不爲GONEView調用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);
}

measureChild也很簡單,我們終於找到調用Viewmeasure的入口了,參數產生的位置就在getChildMeasureSpec中,所以這就是我們今天這篇文章的核心內容啦,我們單獨起一節來說它。

2.3 getChildMeasureSpec生成參數

我們在一開始就說了,在視圖樹從根開始進行遍歷的過程中,傳遞的參數就是int類型的變量,它有兩個含義,mode和大小,那我們下面來看看ViewGroup中提供的工具方法是如何產生給子View的參數的吧。

首先,getChildMeasureSpec()的輸入就很有意思,第一個參數是外面傳遞給當前這個ViewGroup的參數;第二個參數是當前ViewGrouppadding值,第三個參數是子Viewlayoutparams.layout_heightlayoutparams.layout_width

我們設置layout_height的方式一共有三種,match_parentwrap_content和直接給一個值。match_parentwarp_content都是一個負值,所以我們判斷第三個參數是否 > 0,就可以知道子View是否設定了一個確切的值。在ViewGroup實現的時候,這三種方式其實就會影響測量流程中的MODE

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //拿到ViewGroup的父View傳遞進來的mode和size,其實就是當前ViewGroup的measure參數
    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) {
      case MeasureSpec.EXACTLY:
        //如果子View設置了一個確定值
        if (childDimension >= 0) {
            //直接給它確切值,模式是EXACTLY
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //直接給它當前ViewGroup的大小,模式是EXACTLY
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // 代碼1   如果子View設置的是wrap_content,那麼把當前GroupView的大小給它,然後告訴它最大是這麼多了
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    //如果外面傳給ViewGroup的mode是給最大值
    case MeasureSpec.AT_MOST:
        //如果子View設置了一個確定值,那麼還是直接給子View它期望的值
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //告訴子view你最大也就這麼大
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 父View讓我們自己決定你有多大
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) 
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上面這個方法很長,但是我們能看出來一些規律:

1、 如果子View設置了layout_width(height),那麼一般情況下ViewGroup會直接按照它需要的寬高設置spec大小,同時modeEXACTLY,也就是說對於一個行爲良好的ViewGroup,它不應該去改變這個約定。但是。。。不應該!=不能。
2、如果子View設置了我們設置了layout_width(height)="wrap_content",那麼子傳遞給子View的就是當前ViewGroup的大小,同時指定modeAT_MOST,告訴子View你自己去measure你自己,但是不能超過我的大小。
3、什麼時候用UNSPECIFIED
要說清這個事情,我先賣個關子,我們先來說明另外一個東東。堅持看到這裏的你有沒有一個疑問,最頂層的DecorView measure()方法的參數是誰傳遞給它的?是什麼?

2.4 DecorView 測量入口參數

根據前面的分析,你要找這個入口上哪裏看代碼?沒錯,就是ViewRootImpl中去,我們看到執行視圖樹的measure過程的函數其實也是接收兩個int參數:

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);   //直接調用DecorView的measure方法!!入口

baseSizedesiredWindowHeight參數其實是當前Window的大小,lp在這裏是Window.LayoutParams,是Activity設置的WindowLayoutParams,當然一般情況下都是match_parent。那我們看看getRootMeasureSpec做了什麼:

private int getRootMeasureSpec(int windowSize, int rootDimension) { 
    int measureSpec; 
    switch (rootDimension) { 
      case ViewGroup.LayoutParams.MATCH_PARENT: 
          measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); 
          break; 
      case ViewGroup.LayoutParams.WRAP_CONTENT: 
          measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); 
          break; 
      default: 
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); 
        break; 
    } 
  return measureSpec; 
}

一般Activity都是設置的Window的屬性都是match_parent,那麼這裏傳遞給DecorView的參數就是 window的大小+ EXACTLYmode

這個時候,我們再來看看UNSPECIFIED的話題,你會發現在整個視圖樹測量中,正常情況下我們完全走不到UNSPECIFIED這個分支,爲什麼?因爲頂層傳入的mode就是EXACTLYViewGroup默認實現在傳遞給子View的時候,只有外面傳給自己是UNSPECIFIED的時候,它纔會傳遞UNSPECIFIED給子View。那爲什麼存在UNSPECIFIED這個模式呢?

從ViewGroup的角度來看,如果一個子View設置了match_parentwrap_content,前者我直接吧自己的大小傳遞給子View,並指定modeEXACTLY;後者我還是把自己的大小傳給子View,並告訴它你最大不能超過我這個值。除了這兩種場景,你想想還有別使用場景嗎?

比如在ScrollView中,ScrollView能包含一個LinearLayout的子View,這個時候其實LinearLayoutmeasure自己的時候,其實就不需要參考父View的大小,所以ScrollView會給它的子Viewmode設置成UNSPECIFIED

2.5 View的Measure過程

我們前面說了這麼多,主要解析了ViewGroup傳遞參數給子View,那麼子View拿到這個參數之後,就會去走自己的onMeasure,所以父View和子View的測量其實是協商的過程,父View給你建議了,子View怎麼實現?當然最好是按照父View的建議來測量唄,我們來舉個反例吧:

public class MyView extends View {
  public MyView(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(100,100);
  }
}

這是個傲嬌的View,它在自己的onMeasure方法中直接設置了自己的measure結果,直接忽略了父View給的建議,這樣的後果是什麼?你在xml中給MyView設置的layout_width layout_height屬性都完全失效,比如:你設置了Layout_width="50px",父View調用MyView測量的時候,它看到設置了layout_width="50px",那麼父View傳遞給MyViewmeasure參數肯定是:modeEXACTLYspecSize50,但是MyView在自己的onMeasure裏面壓根就不考慮父View的建議,所以所有給它設置的Layout_widthheight都是無效的。

總結

你可以看到Android中將一個View展示到頁面上是一件多麼複雜的過程,measure只是萬里長征第一步。其實在Andriod中,視圖樹的很多通知和操作都是基於父View和子View協商完成的,測量過程也是如此,後面有時間我會整理一下LayoutDraw過程,敬請期待。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章