1. 程式人生 > >View的工作原理之Measure過程原始碼學習(二)

View的工作原理之Measure過程原始碼學習(二)

 

        上一篇文章從Android程式啟動過程講解了Activity、PhoneWindow以及ViewRoot與DecorView的聯絡。本篇文章詳細講述一下DecorView的measure過程。

        在瞭解measure過程過程之前需要先了解MeasureSpec這個類, MeasureSpec是一個32位的int值,高2位表示SpecMode(測量模式),低30位表示SpecSize(測量大小)。MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的物件記憶體分配,為方便操作,提供了打包和解包方法。MeasureSpec很大程度上決定一個View的尺寸規格,之所以說是很大程度而不是完全決定,是因為View的measure過程還受到父容器的影響。在測量過程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec來測量View的寬高。MeasureSpec部分原始碼如下所示。

//View.java  24515行
 private static final int MODE_SHIFT = 30;
 private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
 public static final int UNSPECIFIED = 0 << MODE_SHIFT;
 public static final int EXACTLY     = 1 << MODE_SHIFT;
 public static final int AT_MOST     = 2 << MODE_SHIFT;

 public static int getMode(int measureSpec) {
     //noinspection ResourceType
     return (measureSpec & MODE_MASK);
 }

 public static int getSize(int measureSpec) {
     return (measureSpec & ~MODE_MASK);
 }

        如上程式碼,SpecMode有三種不同的值,分別是UNSPECIFIED,EXACTLY ,AT_MOST 。這裡特別指出EXACTLY的值是數字1左移30位結果1073741824,AT_MOST的值數字2左移30位結果是-2147483648。為啥特別指出這兩個值,在後面程式碼會講到。在UNSPECIFIED這種模式下,父容器不限制View的大小,要多大給多大,但這種情況一般使用者系統內部,對於我們的開發工作幾乎使用不到,所以不需深入瞭解。EXACTLY模式是精確模式,在這種模式下,父容器檢測出View的精確大小,View的最終大小就是SpecSize的值。AT_MOST模式,在這種模式下,父容器給定一個SpecSize的值,View的大小不能大於這個值,具體多大需要看View的具體實現。

        瞭解完MeasureSpec之後,我們開始進入DecorView的measure過程。前一篇文章說到:在ViewRootImpl中有一個方法performTraversals,在這個方法中,DecorView進行了measure,layout,draw三個過程,分別對應performMeasure,performLayout和performDraw三個方法。performTraversals方法中呼叫方法performMeasure,這個方法的程式碼如下

//ViewRootImpl.java  2404行
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方法,前一篇文章也說明了,這個mView就是DecorView。因為measure方法是final型別,所以最終呼叫的是View中的measure方法,而View中的measure方法會呼叫實現類的onMeasure中的方法,也就是DecorView的onMeasure。所以DecorView的onMeasure方法獲得的引數就是performMeasure方法的引數。這個引數是什麼呢?來看下面程式碼。

//Build.java 576行
  public static final int JELLY_BEAN_MR1 = 17;
 //View.java 4545行
  sUseBrokenMakeMeasureSpec = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
   
 //View.java  24563行
  public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
      if (sUseBrokenMakeMeasureSpec) {//判斷SDK版本是否17以下
           return size + mode;
       } else {
           return (size & ~MODE_MASK) | (mode & MODE_MASK);
       }
  }

          先來看makeMeasureSpec方法的實現,這裡是將測量模式mode和測量大小size結合起來組成一個32位的int型數值。這裡需要注意,測量模式mode都是左移了30位的,具體參考上文的MeasureSpec部分。方法對SDK版本是否在17上下做了區分,原因我看了一下注釋,好像是因為SDK17以下使用兩個值相加的話,會使RelativeLayout在滾動佈局中(ScrollView或者HorizontalScrollView)出現錯誤。因為SDK17以上,使用了位運算來獲得MeasureSpec。

//ViewRootImpl.java  375行
 final Rect mWinFrame; // frame given by window manager.

//ViewRootImpl.java  1628行
Rect frame = mWinFrame;

//ViewRootImpl.java  2070行
// !!FIXME!! This next section handles the case where we did not get the
// window size we asked for. We should avoid this by getting a maximum size from
// the window session beforehand.
if (mWidth != frame.width() || mHeight != frame.height()) {
    mWidth = frame.width();
    mHeight = frame.height();
}

//ViewRootImpl.java  1594行
WindowManager.LayoutParams lp = mWindowAttributes;

//ViewRootImpl.java  2145行
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

//ViewRootImpl.java  2155行
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

       上方的程式碼,performMeasure方法的引數來自getRootMeasureSpec,getRootMeasureSpec方法的引數,我已經在上方程式碼中,展示了他們的來源,mWidth和mHeight是mWinFrame的寬高,結合註釋可以知道,這也就是視窗的大小了。lp的話,沒有翻到最終來源,但是也很容易猜出,這應該是給予DecorView的佈局約束。由此,DecorView的測量規格值,是有視窗大小和系統給予它的佈局約束WindowManager.LayoutParams來共同決定了。繼續看getRootMeasureSpec方法的原始碼。

//ViewRootImpl.java  2633行
 private static int getRootMeasureSpec(int windowSize, int rootDimension) {
     int measureSpec;
     switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
     }
     return measureSpec;
 }

       在getRootMeasureSpec方法中,很直觀的可以看到,它是根據系統給到DecorView的佈局約束WindowManager.LayoutParams的值來確定DecorView的測量規格值MeasureSpec的。這裡需要額外記住一點的是ViewGroup.LayoutParams.MATCH_PARENT的值是-1,ViewGroup.LayoutParams.WRAP_CONTENT的值是-2

  1. 如果佈局約束LayoutParams中,width或者height的值是LayoutParams.MATCH_PARENT,那麼測量模式就是EXACTLY,精確模式,大小是視窗的寬度或高度;
  2. 如果佈局約束LayoutParams中,width或者height的值是LayoutParams.WRAP_CONTENT,那麼測量模式就是AT_MOST,最大模式,大小是視窗的寬度或高度;
  3. 如果佈局約束LayoutParams中,width或者height的值是具體的數值,那麼測量模式就是EXACTLY,精確模式,寬度或高度大小給定的具體大小;

        至此,DecorView的onMeasure方法引數的來源就清楚了,其實就是DecorView的寬高的測量規格值。檢視DecorView的onMeasure方法原始碼得知,因為DecorView和子元素之間存在一些需要處理的間距,所以真正傳遞給子元素的測量規格值需要進行處理,這就是onMeasure方法的功能。DecorView的onMeasure方法還呼叫super.onMeasure(widthMeasureSpec, heightMeasureSpec);。因為DecorView繼承自FrameLayout,所以這裡將呼叫FrameLayout的onMeasure方法,在這個方法中,會獲取子元素的個數,然後遍歷子元素的measure過程,這樣就把measure過程從DecorView傳到了子元素去。而子元素的measure過程,如果這個子元素不是ViewGroup,而是普通的View,那麼就執行自己的measure過程就行了,如果是ViewGroup的話,還需要像FrameLayout一樣,迴圈呼叫子元素的measure過程,如此一級一級往下,所有View的measure過程都會被執行。這個在下篇文章中將會詳細講解。

       DecorView的Measure過程到這就完成了。