Android - View 繪制流程

image.png

簡介
我們知道,在 Android 中,View 繪制主要包含 3 大流程:

measure(測量):主要用于確定 View 的測量寬/高。

layout(布局):主要用于確定 View 在父容器中的放置位置。

draw(繪制):結合前面兩步結果,將 View 真正繪制到屏幕上。

Android 中,主要有兩種視圖:View和ViewGroup,其中:

View:就是一個獨立的視圖
ViewGroup:一個容器組件,該容器可容納多個子視圖,即ViewGroup可容納多個View或ViewGroup,且支持嵌套。
雖然ViewGroup繼承于View,但是在 View 繪制三大流程中,某些流程需要區(qū)分View和ViewGroup,它們之間的操作并不完全相同,比如:

View和ViewGroup都需要進行 measure,確定各自的測量寬/高。View只需直接測量自身即可,而ViewGroup通常都必須先測量所有子View,最后才能測量自己
通常ViewGroup先定位自己的位置(layout),然后再定位其子View 位置(onLayout)
View需要進行 draw 過程,而ViewGroup通常不需要(當然也可以進行繪制),因為ViewGroup更多作為容器存在,起存儲放置功能
measure 流程
對 View 進行測量,主要包含兩個步驟:

求取 View 的測量規(guī)格MeasureSpec。
依據上一步求得的MeasureSpec,對 View 進行測量,求取得到 View 的最終測量寬/高。
MeasureSpec
對于第一個步驟,即求取 View 的MeasureSpec,首先我們來看下MeasureSpec的源碼定義:

// frameworks/base/core/java/android/view/View.java
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    // 生成測量規(guī)格
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    // 獲取測量模式
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    // 獲取測量大小
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    ...
}

MeasureSpec是View的一個公有靜態(tài)內部類,它是一個 32 位的int值,高 2 位表示 SpecMode(測量模式),低 30 位表示 SpecSize(測量尺寸/測量大?。?。
MeasureSpec將兩個數據打包到一個int值上,可以減少對象內存分配,并且其提供了相應的工具方法可以很方便地讓我們從一個int值中抽取出 View 的 SpecMode 和 SpecSize。

一個MeasureSpec表達的是:該 View 在該種測量模式(SpecMode)下對應的測量尺寸(SpecSize)。其中,SpecMode 有三種類型:

UNSPECIFIED:表示父容器對子View 未施加任何限制,子View 尺寸想多大就多大。

EXACTLY:如果子View 的模式為EXACTLY,則表示子View 已設置了確切的測量尺寸,或者父容器已檢測出子View 所需要的確切大小。
這種模式對應于LayoutParams.MATCH_PARENT和子View 設置具體數值兩種情況。

AT_MOST:表示自適應內容,在該種模式下,View 的最大尺寸不能超過父容器的 SpecSize,因此也稱這種模式為 最大值模式。
這種模式對應于LayoutParams.WRAP_CONTENT。

LayoutParams
對 View 進行測量,最關鍵的一步就是計算得到 View 的MeasureSpec,子View 在創(chuàng)建時,可以指定不同的LayoutParams(布局參數),LayoutParams的源碼主要內容如下所示:

// frameworks/base/core/java/android/view/ViewGroup.java
public static class LayoutParams {
    ...
    /**
     * Special value for the height or width requested by a View.
     * MATCH_PARENT means that the view wants to be as big as its parent,
     * minus the parent's padding, if any. Introduced in API Level 8.
     */
    public static final int MATCH_PARENT = -1;

    /**
     * Special value for the height or width requested by a View.
     * WRAP_CONTENT means that the view wants to be just large enough to fit
     * its own internal content, taking its own padding into account.
     */
    public static final int WRAP_CONTENT = -2;

    /**
     * Information about how wide the view wants to be. Can be one of the
     * constants FILL_PARENT (replaced by MATCH_PARENT
     * in API Level 8) or WRAP_CONTENT, or an exact size.
     */
    public int width;

    /**
     * Information about how tall the view wants to be. Can be one of the
     * constants FILL_PARENT (replaced by MATCH_PARENT
     * in API Level 8) or WRAP_CONTENT, or an exact size.
     */
    public int height;
    ...
}

其中:

  • LayoutParams.MATCH_PARENT:表示子View 的尺寸與父容器一樣大(注:需要減去父容器padding部分空間,讓父容器padding生效)
  • LayoutParams.WRAP_CONTENT:表示子View 的尺寸自適應其內容大小(注:需要包含子View 本身的padding空間)
  • width/height:表示 View 的設置寬/高,即layout_widthlayout_height設置的值,其值有三種選擇:LayoutParams.MATCH_PARENT、LayoutParams.WRAP_CONTENT具體數值。

LayoutParams會受到父容器的MeasureSpec的影響,測量過程會依據兩者之間的相互約束最終生成子View 的MeasureSpec,完成 View 的測量規(guī)格。

簡而言之,View 的MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同決定(DecorViewMeasureSpec是由自身的LayoutParams和屏幕尺寸共同決定,參考后文)。也因此,如果要求取子View 的MeasureSpec,那么首先就需要知道父容器的MeasureSpec,層層逆推而上,即最終就是需要知道頂層View(即DecorView)的MeasureSpec,這樣才能一層層傳遞下來,這整個過程需要結合Activity的啟動過程進行分析。

Activity 視圖基本結構

我們知道,在 Android 中,Activity是作為視圖組件存在,主要就是在手機上顯示視圖界面,可以供用戶操作,Activity就是 Andorid 中與用戶直接交互最多的系統組件。

Activity的基本視圖層次結構如下所示:

image

Activity中,實際承載視圖的組件是Window(更具體來說為PhoneWindow),頂層View 是DecorView,它是一個FrameLayout,DecorView內部是一個LinearLayout,該LinearLayout由兩部分組成(不同 Android 版本或主題稍有差異):TitleViewContentView,其中,TitleView就是標題欄,也就是我們常說的TitleBarActionBar,ContentView就是內容欄,它也是一個FrameLayout,主要用于承載我們的自定義根布局,即當我們調用setContentView(...)時,其實就是把我們自定義的布局設置到該ContentView中。

Activity啟動完成后,最終就會渲染出上述層次結構的視圖。

DecorView 測量規(guī)格

因此,如果我們要求取得到子View 的MeasureSpec,那么第一步就是求取得到頂層View(即DecorView)的MeasureSpec。大致過程如下所示:

  1. Activity啟動過程中,會調用到ActivityThread.handleResumeActivity(...),該方法就是 View 視圖繪制的起始之處:

    // frameworks/base/core/java/android/app/ActivityThread.java
    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
        ...
        ActivityClientRecord r = performResumeActivity(token, clearHide);
        ...
        // 此處的 window 為與 Activity 綁定的 PhoneWindow,即 Activity.mWindow
        r.window = r.activity.getWindow();
        // PhoneWindow 綁定的頂層視圖:DecorView
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        // 獲取與 Activity 綁定的 WindowManager,實際上是 PhoneWindow 的 WindowManager
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        ...
        // 添加 DecorView 到 PhoneWindow 上(相當于設置 Activity 根視圖)
        wm.addView(decor, l);
        ...
    }
    
    

    其中,r.window.getDecorView()實際調用的是PhoneWindow.getDecorView(),其會返回頂層DecorView(不存在時會自動實例化):

    // frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
    public class PhoneWindow extends Window implements MenuBuilder.Callback {
        // This is the top-level view of the window, containing the window decor.
        private DecorView mDecor;
        ...
    
        @Override
        public final View getDecorView() {
            if (mDecor == null) {
                installDecor();
            }
            return mDecor;
        }
    
        private void installDecor() {
            if (mDecor == null) {
                mDecor = generateDecor();
                ...
            }
            ...
        }
    
        protected DecorView generateDecor() {
            // 實例化 DecorView
            return new DecorView(getContext(), -1);
        }
        ...
    }
    
    

    然后,r.window.getAttributes()實際調用的是Window.getAttributes()

    // frameworks/base/core/java/android/view/Window.java
    public abstract class Window {
        private final WindowManager.LayoutParams mWindowAttributes =
            new WindowManager.LayoutParams();
        ...
    
        public final WindowManager.LayoutParams getAttributes() {
            return mWindowAttributes;
        }
    }
    // frameworks/base/core/java/android/view/WindowManager.java
    public interface WindowManager extends ViewManager {
        ...
        public static class LayoutParams extends ViewGroup.LayoutParams
                implements Parcelable {
            public LayoutParams() {
                // DecorView 的布局參數為 MATCH_PARENT
                super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
                ...
            }
        }
    }
    
    

    這里可以看到,此處r.window.getAttributes()返回的是一個WindowManager.LayoutParams實例,對應的最終寬/高布局參數為LayoutParams.MATCH_PARENT,最后通過wm.addView(decor,l)DecorView添加到WindowManager上(最終其實是設置到ViewRootImpl上),所以DecorView的布局參數為MATCH_PARENT。

  2. View 的繪制流程真正開始的地方為ViewRootImpl.performTraversals(),在其中,有如下代碼片段:

    // frameworks/base/core/java/android/view/ViewRootImpl.java
    private void performTraversals() {
        ...
        int desiredWindowWidth;
        int desiredWindowHeight;
        ...
        // Ask host how big it wants to be
        windowSizeMayChange |= measureHierarchy(host, lp, res,
                desiredWindowWidth, desiredWindowHeight);
        ...
    }
    
    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
        final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        ...
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
    }
    
    

    此處的desiredWindowWidthdesiredWindowHeight是屏幕的尺寸,內部最終會調用到ViewRootImpl.getRootMeasureSpec(...),其源碼如下所示:

    // frameworks/base/core/java/android/view/ViewRootImpl.java
    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;
    }
    
    

    ViewRootImpl.getRootMeasureSpec(...)見名知意,其實就是用來獲取頂層View(即DecorView)的MeasureSpec,其邏輯如下:

    1. DecorViewLayoutParamsMATCH_PARENT時,說明DecorView的大小與屏幕一樣大,而又由于屏幕大小是確定的,因此,其 SpecMode 為EXACTLY,SpecSize 為windowSize,;
    2. DecorViewLayoutParamsWRAP_CONTENT時,說明DecorView自適應內容大小,因此它的大小不確定,但是最大不能超過屏幕大小,故其 SpecMode 為AT_MOST,SpecSize 為windowSize;
    3. 其余情況為DecorView設置了具體數值大小或UNSPECIFIED,故以DecorView為主,其 SpecMode 為EXACTLY,SpecSize 就是自己設置的值,即rootDimension;

    結合我們上面的分析,由于DecorViewLayoutParamsMATCH_PARENT,因此,DecorViewMeasureSpec最終為:MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY),即DecorView的 SpecMode 為EXACTLY,SpecSize 為屏幕大小。

默認測量(measure)

經過上述步驟求取得到 View 的MeasureSpec后,接下來就可以真正對 View 進行測量,求取 View 的最終測量寬/高:

Android 內部對視圖進行測量的過程是由View#measure(int, int)方法負責的,但是對于ViewViewGroup,其具體測量過程有所差異。

因此,對于測量過程,我們分別對ViewViewGroup進行分析:

  • View測量View的測量過程由View.measure(...)方法負責,其源碼如下所示:

    // frameworks/base/core/java/android/view/View.java
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        // measure ourselves, this should set the measured dimension flag back
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ...
    }
    
    

    View#measure(int, int)中參數widthMeasureSpecheightMeasureSpec是由父容器傳遞進來的,具體的測量過程請參考后文內容。

    需要注意的是,View#measure(int, int)是一個final方法,因此其不可被覆寫,實際真正測量 View 自身使用的是View#onMeasure(int, int)方法,如下所示:

    // frameworks/base/core/java/android/view/View.java
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    
    

    onMeasure(...)主要做了三件事:

    1. 首先通過getSuggestedMinimumWidth()/getSuggestedMinimumHeight()方法獲取得到 View 的推薦最小測量寬/高:

      // frameworks/base/core/java/android/view/View.java
      protected int getSuggestedMinimumWidth() {
          return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
      }
      
      protected int getSuggestedMinimumHeight() {
          return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
      }
      
      

      這兩個方法的實現原理是一致的,這里就只分析getSuggestedMinimumWidth()方法實現,該方法內部是一個三目運算符,可以很清晰看出,當 View 沒有設置背景時,它的寬度就為mMinWidthmMinWidth就是android:minWidth這個屬性對應設置的值(未設置android:minWidth時,其值默認為0),當 View 設置了背景時,它的寬度就是mMinWidthmBackground.getMinimumWidth()之中的較大值,其中,mBackground.getMinimumWidth()源碼如下:

      // frameworks/base/graphics/java/android/graphics/drawable/Drawable.java
      /*
       * @return The minimum width suggested by this Drawable. If this Drawable
       *         doesn't have a suggested minimum width, 0 is returned.
       */
      public int getMinimumWidth() {
          final int intrinsicWidth = getIntrinsicWidth();
          return intrinsicWidth > 0 ? intrinsicWidth : 0;
      }
      
      // 不同子類可實現具體大小
      public int getIntrinsicWidth() {
          return -1;
      }
      
      

      Drawable.getMinimumWidth()就是返回 Drawable 的原始寬度,如果該 Drawable 未設置寬度,則返回0。

      綜上,getSuggestedMinimumWidth()/getSuggestedMinimumHeight()其實就是用于獲取 View 的最小測量寬/高,其具體邏輯為:當 View 沒有設置背景時,其最小寬/高為android:minWidth/android:mMinHeight所指定的值,當 View 設置了背景時,其最小測量寬/高為android:minWidth/android:minHeight與其背景圖片寬/高的較大值。

      簡而言之,View 的最小測量寬/高為android:minWidth/android:minHeight和其背景寬/高之間的較大值。

    2. 通過getDefaultSize(...)獲取到 View 的默認測量寬/高,具體獲取過程如下所示:

      // frameworks/base/core/java/android/view/View.java
      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;
      }
      
      

      此處的size是通過getSuggestedMinimumWidth()/getSuggestedMinimumHeight()方法獲取得到系統建議 View 的最小測量寬/高。

      參數measureSpec是經由View.measure(...)->View.onMeasure(...)->View.getDefaultSize(...)調用鏈傳遞進來的,表示的是當前 View 的MeasureSpec。

      getDefaultSize(...)內部首先會獲取 View 的測量模式和測量大小,然后當 View 的測量模式為UNSPECIFIED時,也即未限制 View 的大小,因此此時 View 的大小就是其原生大小(也即android:minWidth或背景圖片大小),當 View 的測量模式為AT_MOSTEXACTLY時,此時不對這兩種模式進行區(qū)分,一律將 View 的大小設置為測量大?。?SpecSize)。
      :實際上,這里可以看到,默認情況下,View 不區(qū)分AT_MOSTEXACTLY,也即,當自定義 View 時,LayoutParams.WRAP_CONTENTLayoutParams.MATCH_PARENT效果是一樣的,均為MATCH_PARENT的效果,原因是 子View 的MeasureSpec是由父容器傳遞進來的,父容器是通過ViewGroup#getChildMeasureSpec(...)方法獲取得到 子View 的MeasureSpec,在該方法內部,子View 的測量模式無論是AT_MOST或是EXACTLY,其測量大小都為父容器大?。ù_定的說,是父容器剩余空間大?。?,因此其效果就等同于MATCH_PARENT,具體源碼詳情分析請參考后文。

      總之,一般自定義 View 時,都需要覆寫onMeasure(...),并為其LayoutParams.WRAP_CONTENT設置一個默認大小,如下所示:

       @Override
       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      
           // 先進行默認測量
           super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      
           // 默認大小依據自己靈活配置,這里為 400px
           int defaultSize = 400;
      
           // 獲取默認測量寬/高
           int width = this.getMeasuredWidth();
           int height = this.getMeasuredHeight();
      
           // 獲取 View 的布局參數
           ViewGroup.LayoutParams lp = this.getLayoutParams();
      
           // 寬度為自適應,則設置一個默認大小
           if(lp.width == ViewGroup.LayoutParams.WARP_CONTENT) {
               width = defaultSize;
           }
      
           // 高度為自適應,則設置一個默認大小
           if(lp.height == ViewGroup.LayoutParams.WARP_CONTENT) {
               height = defaultSize;
           }
      
           this.setMeasuredDimension(width, height);
       }
      
      
    3. 獲取到 View 的測量寬/高后,通過setMeasuredDimension(...)記錄 View 的測量寬/高:

      // frameworks/base/core/java/android/view/View.java
      protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
          ...
          setMeasuredDimensionRaw(measuredWidth, measuredHeight);
      }
      
      // 記錄測量寬/高
      private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
          mMeasuredWidth = measuredWidth;
          mMeasuredHeight = measuredHeight;
      
          mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
      }
      
      

      setMeasuredDimension(...)其實就是將 View 的最終測量寬/高設置到View.mMeasuredWidth/View.mMeasuredHeight屬性中,完成測量過程。

  • ViewGroup測量ViewGroup是一個抽象類,其繼承于View

    public abstract class ViewGroup extends View implements ViewParent, ViewManager {...}
    
    

    ViewGroup的測量過程也是由View.measure(...)負責,因此實際負責測量的是ViewGroup.onMeasure(...)方法,但是由于ViewGroup的作用是用于容納子View,如果想測量ViewGroup,則必須先測量其子View,而又由于不同的ViewGroup有不同的布局特性,因此無法抽象出一套標準的測量流程,所以ViewGroup本身沒有覆寫onMeasure(...)方法(交由具體自定義ViewGroup覆寫),但是它提供了一些測量子View 的輔助方法,比如:measureChildren(...)、measureChildrenWithMargins(...)、measureChild(...)、getChildMeasureSpec(...)等等,自定義ViewGroup可借助這些輔助方法,在onMeasure(...)中完成子View 的測量,然后最終才能完成自己的測量。

    我們隨便選擇一個輔助方法,比如ViewGroup#measureChildWithMargins(...),查看其源碼:

    // android/view/ViewGroup.java
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        // 獲取 子View 的 LayoutParams
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
        // 獲取 子View 的 MeasureSpec
        // 父容器已使用的空間為:自身已使用空間 + 自身的 padding + 子View的 margin
        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);
    
        // 測量子View
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
    

    代碼非常簡潔易懂,其核心就是先獲取得到 子View 的MeasureSpecgetChildMeasureSpec(...)),然后就可以對 子View 進行測量(child.measure(...))。

    View#measure(...)的測量詳情上述我們已經介紹過了,這里我們主要來看下ViewGroup#getChildMeasureSpec(...)獲取 子View 測量規(guī)格的具體過程:

    // android/view/ViewGroup.java
    /**
     *
     * @param spec 父容器的 MeasureSpec
     * @param padding 父容器已使用的空間(比如:父View自身的 padding + 子View的 margin)
     * @param childDimension 子View的 LayoutParams
     * @return 子View 的 MeasureSpec
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // 當前View(即父容器)的測量模式
        int specMode = MeasureSpec.getMode(spec);
        // 父容器的測量大小
        int specSize = MeasureSpec.getSize(spec);
    
        // 父容器剩余可用空間
        int size = Math.max(0, specSize - padding);
    
        // 子View 最終測量大小
        int resultSize = 0;
        // 子View 最終測量模式
        int resultMode = 0;
    
        switch (specMode) {
        // Parent has imposed an exact size on us
        // 父容器大小已確定
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) { 
            // 子View 設置了具體大?。ň_數值)
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) { 
            // 子View 大小撐滿父容器
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // 子View 自適應內容大小
                // 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;
        }
        // 子View 的最終測量規(guī)格
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    
    

    getChildMeasureSpec(...)其實就是ViewGroup對其內部 子View 的默認測量過程,其核心邏輯為:

    1. 如果父容器的測量模式為EXACTLY:即父容器測量大小是確切的,且其剩余空間精確為size,此時:

      • 如果 子View 的LayoutParams為具體數值:則表示 子View 已明確設置了具體大小,因此,此時 子View 的測量大小即為自己設置的值,即childDimension,測量模式為EXACTLY。
      • 如果 子View 的LayoutParamsMATCH_PARENT:表示 子View 的大小撐滿父容器,由于父容器是EXACTLY,即大小已知,因此,子View 也是大小已知,故其測量模式為EXACTLY,且其測量大小就是父容器剩余空間大小,具體為size
      • 如果 子View 的LayoutParamsWRAP_CONTENT:表示 子View 自適應內容大小,但是其尺寸最大不能超過父容器剩余空間,因此其測量模式為AT_MOST,測量大小為父容器剩余空間size。
    2. 如果父容器的測量模式為AT_MOST:即父容器自適應其內容大小,也即父容器大小不確定,此時:

      • 如果 子View 的LayoutParams為具體數值:則表示 子View 已明確設置了具體大小,因此,此時 子View 的測量大小即為自己設置的值,即childDimension,測量模式為EXACTLY。
      • 如果 子View 的LayoutParamsMATCH_PARENT:表示 子View 的大小撐滿父容器,由于父容器是AT_MOST,即大小未知,因此,子View 也是大小未知,即其測量模式為AT_MOST,且其測量大小不超過父容器剩余空間大小size。
      • 如果 子View 的LayoutParamsWRAP_CONTENT:表示 子View 自適應內容大小,但是其尺寸最大不能超過父容器剩余空間,因此其測量模式為AT_MOST,測量大小為父容器剩余空間size。
    1. 如果父容器的測量模式為UNSPECIFIED:即父容器大小無限制,此時:

      • 如果 子View 的LayoutParams為具體數值:則表示 子View 已明確設置了具體大小,因此,此時 子View 的測量大小即為自己設置的值,即childDimension,測量模式為EXACTLY
      • 如果 子View 的LayoutParamsMATCH_PARENT:表示 子View 的大小撐滿父容器,由于父容器大小無限制,因此,子View 的大小也是無限制的,所以,子View 的測量模式為UNSPECIFIED,測量大小未知,通常設置為0,表示無限。
      • 如果 子View 的LayoutParamsWRAP_CONTENT:表示 子View 自適應內容大小,由于父容器大小無限制,因此,子View 的測量大小也是無限制的,所以其模式為UNSPECIFIED,測量大小無限,通常使用0進行表示。

    上述的邏輯總結如下圖所示:(:圖片來源于互聯網,侵刪)

    image

    :前面我們一直強調:子View 的MeasureSpec是由其LayoutParams和父容器的MeasureSpec共同約束構造而成,其實這部分邏輯就是ViewGroup#getChildMeasureSpec(...)方法負責的,可以很清晰看到,子View 的MeasureSpec就是在父容器MeasureSpec約束下,與其自身LayoutParams共同協商決定的。

綜上,無論是對View的測量還是ViewGroup的測量,都是由View#measure(int widthMeasureSpec, int heightMeasureSpec)方法負責,然后真正執(zhí)行 View 測量的是 View 的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法。

具體來說,View直接在onMeasure(...)中測量并設置自己的最終測量寬/高。在默認測量情況下,View的測量寬/高由其父容器的MeasureSpec和自身的LayoutParams共同決定,當View自身的測量模式為LayoutParams.UNSPECIFIED時,其測量寬/高為android:minWidth/android:minHeight和其背景寬/高之間的較大值,其余情況皆為自身MeasureSpec指定的測量尺寸。

而對于ViewGroup來說,由于布局特性的豐富性,只能自己手動覆寫onMeasure(...)方法,實現自定義測量過程,但是總的思想都是先測量 子View 大小,最終才能確定自己的測量大小。

layout 流程

當確定了 View 的測量大小后,接下來就可以來確定 View 的布局位置了,也即將 View 放置到屏幕具體哪個位置。

View layout

View 的布局過程由View#layout(...)負責,其源碼如下:

// android/view/View.java
/**
 * @param l Left position, relative to parent
 * @param t Top position, relative to parent
 * @param r Right position, relative to parent
 * @param b Bottom position, relative to parent
 */
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
    ...
    setFrame(l, t, r, b);
    ...
    onLayout(changed, l, t, r, b);
    ...
}

View#layout(...)主要就做了兩件事:

  1. setFrame(...):首先通過View#setFrame(...)來確定自己的布局位置,其源碼如下:

    // android/view/View.java
    protected boolean setFrame(int left, int top, int right, int bottom) {
        ...
        // Invalidate our old position
        invalidate(sizeChanged);
    
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
    }
    
    

    setFrame(...)其實就是更新記錄 View 的四個頂點位置,這樣 View 在父容器中的坐標位置就確定了。

  2. onLayout(...)setFrame(...)是用于確定 View 自身的布局位置,而onLayout(...)主要用于確定 子View 的布局位置:

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
    
    

    由于 View 不包含子組件,因此其onLayout是一個空實現。

ViewGroup layout

ViewGroup 的布局流程由ViewGroup#layout(...)負責,其源碼如下:

// android/view/ViewGroup.java
@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    ...
    @Override
    public final void layout(int l, int t, int r, int b) {
        ...
        super.layout(l, t, r, b);
        ...
    }

可以看到,ViewGroup#layout(...)最終也是通過View#layout(...)完成自身的布局過程,一個注意的點是,ViewGroup#layout(...)是一個final方法,因此子類無法覆寫該方法,主要是ViewGroup#layout(...)方法內部對子視圖動畫效果進行了相關設置。

由于ViewGroup#layout(...)內部最終調用的還是View#layout(...),因此,ViewGroup#onLayout(...)就會得到回調,用于處理 子View 的布局放置,其源碼如下:

// android/view/ViewGroup.java
@Override
protected abstract void onLayout(boolean changed,
        int l, int t, int r, int b);

由于不同的ViewGroup,其布局特性不同,因此ViewGroup#onLayout(...)是一個抽象方法,交由ViewGroup子類依據自己的布局特性,擺放其 子View 的位置。

draw 流程

當 View 的測量大小,布局位置都確定后,就可以最終將該 View 繪制到屏幕上了。

View 的繪制過程由View#draw(...)方法負責,其源碼如下:

// android/view/View.java
public void draw(Canvas canvas) {
    ...
    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1\. Draw the background
     *      2\. If necessary, save the canvas' layers to prepare for fading
     *      3\. Draw view's content
     *      4\. Draw children
     *      5\. If necessary, draw the fading edges and restore layers
     *      6\. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    drawBackground(canvas);

    // skip step 2 & 5 if possible (common case)
    ...
    // Step 2, save the canvas' layers
    if (drawTop) {
        canvas.saveLayer(left, top, right, top + length, null, flags);
    }

    if (drawBottom) {
        canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
    }

    if (drawLeft) {
        canvas.saveLayer(left, top, left + length, bottom, null, flags);
    }

    if (drawRight) {
        canvas.saveLayer(right - length, top, right, bottom, null, flags);
    }
    ...
    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas);

    // Step 5, draw the fade effect and restore layers
    ...
    if (drawTop) {
        ...
        canvas.drawRect(left, top, right, top + length, p);
    }

    if (drawBottom) {
        ...
        canvas.drawRect(left, bottom - length, right, bottom, p);
    }

    if (drawLeft) {
        ...
        canvas.drawRect(left, top, left + length, bottom, p);
    }

    if (drawRight) {
        ...
        canvas.drawRect(right - length, top, right, bottom, p);
    }
    ...
    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
}

其實注釋已經寫的很清楚了,View#draw(...)主要做了以下 6 件事:

  1. 繪制背景:drawBackground(...)

  2. 如果有必要的話,保存畫布圖層:Canvas.saveLayer(...)

  3. 繪制自己onDraw(...),其源碼如下:

    // android/view/View.java
    protected void onDraw(Canvas canvas) {
    }
    
    

    View#onDraw(...)是一個空方法,因為每個 View 的繪制都是不同的,自定義 View 時,通常會覆寫該方法,手動繪制該 View 內容。

  4. 繪制子ViewdispatchDraw(...),其源碼如下:

    // android/view/View.java
    protected void dispatchDraw(Canvas canvas) {
    }
    
    

    由于 View 沒有子元素,因此其dispatchDraw是一個空實現。

    查看下ViewGroup#dispatchDraw(...),其源碼如下:

    // android/view/ViewGroup.java
    @Override
    protected void dispatchDraw(Canvas canvas) {
        ...
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        ...
        for (int i = 0; i < childrenCount; i++) {
            ...
            more |= drawChild(canvas, child, drawingTime);
            ...
        }
        ...
    }
    
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
    
    

    可以看到,其內部主要就是遍歷子View,最后通過child.draw(...)讓子View自己進行繪制。

  5. 如果有必要的話,繪制淡化效果并恢復圖層:Canvas.drawRect(...)

  6. 繪制裝飾:onDrawForeground(...),其源碼如下:

    // android/view/View.java
    public void onDrawForeground(Canvas canvas) {
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);
    
        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        ...
        foreground.draw(canvas);
        }
    }
    
    

    其實主要就是繪制滾動條,前景圖片等視圖相關的裝飾。

繪制起始流程
我們知道,在Activity啟動過程中,會調用到ActivityThread.handleResumeActivity(...),該方法就是 View 視圖繪制的起始之處:

// frameworks/base/core/java/android/app/ActivityThread.java
final void handleResumeActivity(IBinder token,
       boolean clearHide, boolean isForward, boolean reallyResume) {
   ...
   // 回調 Activity.onResume() 方法
   ActivityClientRecord r = performResumeActivity(token, clearHide);
   ...
   // 獲取當前 Activity 實例
   final Activity a = r.activity;
   ...
   // 此處的 window 為與 Activity 綁定的 PhoneWindow,即 Activity.mWindow
   r.window = r.activity.getWindow();
   // PhoneWindow 綁定的頂層視圖:DecorView
   View decor = r.window.getDecorView();
   decor.setVisibility(View.INVISIBLE);
   // 獲取與 Activity 綁定的 WindowManager,實際上是 PhoneWindow 的 WindowManager
   ViewManager wm = a.getWindowManager();
   WindowManager.LayoutParams l = r.window.getAttributes();
   ...
   // 添加 DecorView 到 PhoneWindow 上(相當于設置 Activity 根視圖)
   wm.addView(decor, l);
   ...
}

可以看到,ActivityThread.handleResumeActivity(...)主要就是獲取到當前Activity綁定的ViewManager,最后調用ViewManager.addView(...)方法將DecorView設置到PhoneWindow上,也即設置到當前Activity上。ViewManager是一個接口,WindowManager繼承ViewManager,而WindowManagerImpl實現了接口WindowManager,此處的ViewManager.addView(...)實際上調用的是WindowManagerImpl.addView(...),源碼如下所示:

// frameworks/base/core/java/android/view/WindowManagerImpl.java
public final class WindowManagerImpl implements WindowManager {
   private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
   ...
   @Override
   public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
       applyDefaultToken(params);
       mGlobal.addView(view, params, mDisplay, mParentWindow);
   }
   ...
}
WindowManagerImpl.addView(...)內部轉發(fā)到WindowManagerGlobal.addView(...):

// frameworks/base/core/java/android/view/WindowManagerGlobal.java
public final class WindowManagerGlobal {
   ...
   public void addView(View view, ViewGroup.LayoutParams params,
           Display display, Window parentWindow) {
       ...
       ViewRootImpl root;
       ...
       // 實例化一個 ViewRootImpl
       root = new ViewRootImpl(view.getContext(), display);
       ...
       // 將 ViewRootImpl 與 DecorView 關聯到一起
       root.setView(view, wparams, panelParentView);
       ...
   }
   ...
}

在WindowManagerGlobal.addView(...)內部,會創(chuàng)建一個ViewRootImpl實例,然后調用ViewRootImpl.setView(...)將ViewRootImpl與DecorView關聯到一起:

// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
       View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
   ...
   public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
       ...
       // 將 DecorView 綁定到 ViewRootImpl.mView 屬性上
       mView = view;
       ...
       mWindowAttributes.copyFrom(attrs);
       ...
       // Schedule the first layout -before- adding to the window
       // manager, to make sure we do the relayout before receiving
       // any other events from the system.
       requestLayout();
       ...
   }
   ...
   @Override
   public void requestLayout() {
       if (!mHandlingLayoutInLayoutRequest) {
           // 檢查是否處于主線程
           checkThread();
           ...
           scheduleTraversals();
       }
   }
   ...
}

ViewRootImpl.setView(...)內部首先關聯了傳遞過來的DecorView(通過屬性mView指向DecorView即可建立關聯),然后最終調用requestLayout(),而requestLayout()內部又會調用方法scheduleTraversals():

// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
       View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
   ...
   Choreographer mChoreographer;
   ...
   final class TraversalRunnable implements Runnable {
       @Override
       public void run() {
           // 開始執(zhí)行繪制
           doTraversal();
       }
   }
   final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
   ...
   void scheduleTraversals() {
       if (!mTraversalScheduled) { // 同一幀內不會多次調用遍歷
           mTraversalScheduled = true;
           // 發(fā)送一個同步屏障
           mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
           // 將 UI 繪制任務發(fā)送到 Choreographer,回調觸發(fā) mTraversalRunnable,執(zhí)行繪制操作
           mChoreographer.postCallback(
                   Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
           ...
       }
   }
   ...
   void doTraversal() {
       ...
       performTraversals();
       ...
   }
   ...
}

ViewRootImpl.scheduleTraversals()內部主要做了兩件事:

調用MessageQueue.postSyncBarrier()方法發(fā)送一個同步屏障,同步屏障可以攔截Looper對同步消息的獲取與分發(fā),即加入同步屏障后,此時Looper只會獲取和處理異步消息,如果沒有異步消息,則進入阻塞狀態(tài)。
通過Choreographer.postCallback(...)發(fā)送一個Choreographer.CALLBACK_TRAVERSAL的異步視圖渲染消息。因為前面已經發(fā)送了一個同步屏障,因此此處的視圖繪制渲染消息會優(yōu)先被處理。
Choreographer.postCallback(...)會申請一次 VSYNC 中斷信號,當 VSYNC 信號到達時,便會回調Choreographer.doFrame(...)方法,內部會觸發(fā)已經添加的回調任務,Choreographer的回調任務有以下四種類型:

// 回調 INPUT 任務
doCallbacks(Choreographer.CALLBACK_INPUT, mframeTimeNanos);
// 回調 ANIMATION
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 回調 View 繪制任務 TRAVERSAL
doCallbacks(Choreographer,CALLBACK_TRAVERSAL, frameTimeNanos);
// API Level 23 新增,COMMIT 
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);

因此,ViewRootImpl.scheduleTraversals(...)內部通過

<meta charset="utf-8">

mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)發(fā)送的異步視圖渲染消息就會得到回調,即回調mTraversalRunnable.run()方法,最終會執(zhí)行doTraversal()方法,而doTraversal()內部又會調用performTraversals()方法,該方法才是真正開始執(zhí)行 View 繪制流程的地方,其源碼如下所示:

// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    ...
    private void performTraversals() {
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        ...
        // Ask host how big it wants to be
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ...
        performDraw();
        ...
    }
    ...
}

綜上,performTraversals()會依次調用performMeasure(...)、performLayout(...)performDraw()三個方法,這三個方法會依次完成頂層View(即DecorView)的測量(measure)、布局(layout)和繪制(draw)流程,具體詳情請參考后文。

到此,我們才真正進入 View 繪制流程,總結一下上述流程,如下圖所示:

image

performMeasure

書接前文,我們知道,真正開始 View 繪制流程是ViewRootImpl.performTraversals(),該方法內部首先進行的是performMeasure(...)流程:

// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        // 調用 DecorView.measure(...) 
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

此處的mView其實就是DecorView,其賦值指向在ViewRootImpl.setView(...)中進行,可以看到,performMeasure(...)實際調用的是DecorView.measure(...),所以最終會回調DecorView#onMeasure(...)方法,其源碼如下:

// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
    ...
    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
        ...
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            ...
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            ...
        }
    ...
}

可以看到,DecorView#onMeasure(...)內部將測量過程交由其父類,即FrameLayout進行處理,那我們看下FrameLayout#onMeasure(...)源碼:

// frameworks/base/core/java/android/widget/FrameLayout.java
public class FrameLayout extends ViewGroup {
    ...
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 獲取 子View 數量 
        int count = getChildCount();
        ...
        // 最大高度
        int maxHeight = 0;
        // 最大寬度
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            // 獲取 子View
            final View child = getChildAt(i);
            // 只對可見的 子View 進行測量
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                // 測量子View
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                // 獲取 子View 的布局參數
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                // 獲取當前子View的寬度,包含其外邊距,記錄子View的最大寬度
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                // 記錄子View的最大高度
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                ...
            }
        }

        // Account for padding too
        // 最大寬度包含前景偏移量:padding
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        // 最大高度包含前景偏移量:padding
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        // 比較子View 和 系統建議的 子View 最小高度,獲取兩者中的較大值
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        // 比較子View 和 系統建議的 子View 最小寬度,獲取兩者中的較大值
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            // 子View 高度和 前景圖片高度比較,記錄其中較大值
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            // 子View 高度和 前景圖片寬度比較,記錄其中較大值
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        // 記錄測量結果
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        ...
    }
    ...
}

FrameLayout的布局特性為:所有 子View 層疊在一起,所以FrameLayout的測量寬/高就是其所有 子View 中最大的寬和高,因此FrameLayout#onMeasure(...)的核心邏輯就是遍歷其所有子View,然后通過measureChildWithMargins(...)(該方法前面內容已詳細介紹)測量子View,然后就可以獲取 子View 的寬/高,記錄其中最大的寬/高值,作為自己的測量寬/高。

經過以上步驟,DecorView的測量就已經完成了。

綜上,ViewRootImpl#performMeasure(...)其實就是對DecorView的測量過程(DecorView#measure(...)),DecorView是一個FrameLayout,其測量過程主要由FrameLayout#onMeasure(...)負責,內部主要測量邏輯是先遍歷所有子View,讓 子View 先自己進行測量(child.measure(...)),然后就可以獲取 子View 的測量大小,記錄所有 子View 中占比最大的測量寬/高,作為自己的最終測量大小。

<meta charset="utf-8">

performLayout

ViewRootImpl#performMeasure(...)完成對DecorView的測量后,接下來執(zhí)行的是ViewRootImpl#performLayout(...),其源碼如下:

// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    ...
    // cache mView since it is used so much below...
    final View host = mView;
    ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
}

其中,參數lpwidthheight均為MATCH_PARENT,desiredWindowWidthdesiredWindowHeight為屏幕寬/高,mViewDecorView

所以,performLayout(...)內部其實就是調用DecorView#layout(...),前面 layout 流程中介紹過,ViewGroup#layout(...)內部最終會通過View#layout(...)進行布局,而View#layout(...)內部最終通過View#setFrame(...)方法記錄四個頂點位置,這樣DecorView自己的布局位置就已確定了,即host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight())

確定了DecorView自身的布局位置后,接下來就是要布局其 子View 了,因此,這里最終回調的是DecorView#onLayout(...)方法,其源碼如下所示:

// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
    ...
    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
        ...
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            ...
        }
    ...
}

DecorView#onLayout(...)內部轉交給FrameLayout#onLayout(...)進行 子View 布局操作,其源碼如下:

// frameworks/base/core/java/android/widget/FrameLayout.java
public class FrameLayout extends ViewGroup {
    ...
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // 布局子View
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom,
                                  boolean forceLeftGravity) {
        // 獲取 子View 數量
        final int count = getChildCount();

        // 左邊可放置起始點坐標
        final int parentLeft = getPaddingLeftWithForeground();
        // 右邊可放置終點坐標
        final int parentRight = right - left - getPaddingRightWithForeground();

        // 頂部可放置起始點坐標
        final int parentTop = getPaddingTopWithForeground();
        // 底部可放置終點坐標
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        // 遍歷 子View
        for (int i = 0; i < count; i++) {
            // 獲取 子View
            final View child = getChildAt(i);
            // 不放置狀態(tài)為 GONE 的子View
            if (child.getVisibility() != GONE) {
                // 獲取 子View 布局參數
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                // 獲取 子View 測量寬/高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                // 當前 子View 的布局左邊界
                int childLeft;
                // 當前 子View 的布局右邊界
                int childTop;
                ...
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
    ...
}

FrameLayout#onLayout(...)內部是通過FrameLayout#layoutChildren(...)進行 子View 的布局操作,其主要邏輯就是遍歷所有 子View,計算得到 子View 的四個頂點位置坐標,最后將結果傳遞給child.layout(...),讓 子View 記錄自己在父容器中的布局位置,完成 子View 的布局過程。

綜上,ViewRootImpl#performLayout(...)就是對DecorView的布局過程,此過程會遞歸計算各個 子View 的布局位置,調用 子View 的布局方法,完成各個 子View 的布局。

performDraw

完成了performMeasure(...)performLayout(...)后,最后一步就是performDraw(...)過程,其源碼如下:

// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performDraw() {
    ...
    draw(fullRedrawNeeded);
    ...
}

private void draw(boolean fullRedrawNeeded) {
    ...
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
        return;
    }
    ...
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
        boolean scalingRequired, Rect dirty) {
    ...
    mView.draw(canvas);
    ...
}

可以看到,ViewRootImpl#performDraw()內部會經由ViewRootImpl#draw(...)ViewRootImpl#drawSoftware(...),最終執(zhí)行的還是DecorView#draw(...)過程,其源碼如下:

// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
    ...
    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
        @Override
        public void draw(Canvas canvas) {
            super.draw(canvas);

            if (mMenuBackground != null) {
                mMenuBackground.draw(canvas);
            }
        }
    ...
}

由于FrameLayout沒有覆寫draw(...)方法,因此,super.draw(...)最終調用的是View#draw(...)方法,所以DecorView默認采用的就是 View 的繪制方法,具體繪制詳情上文已介紹過了,主要就是對DecorView的背景、內容、子View、滾動條等裝飾視圖進行繪制。

至此,View 繪制的整個流程已基本介紹完畢。

總結

View 的繪制主要有以下一些核心內容:

  1. 三大流程:View 繪制主要包含如下三大流程:

    • measure:測量流程,主要負責對 View 進行測量,其核心邏輯位于View#measure(...),真正的測量處理由View#onMeasure(...)負責。默認的測量規(guī)則為:如果 View 的布局參數為LayoutParams.WRAP_CONTENTLayoutParams.MATCH_PARENT,那么其測量大小為 SpecSize;如果其布局參數為LayoutParams.UNSPECIFIED,那么其測量大小為android:minWidth/android:minHeight和其背景之間的較大值。

    自定義View 通常覆寫onMeasure(...)方法,在其內一般會對WRAP_CONTENT預設一個默認值,區(qū)分WARP_CONTENTMATCH_PARENT效果,最終完成自己的測量寬/高。而ViewGrouponMeasure(...)方法中,通常都是先測量子View,收集到相應數據后,才能最終測量自己。

    • layout:布局流程,主要完成對 View 的位置放置,其核心邏輯位于View#layout(...),該方法內部主要通過View#setFrame(...)記錄自己的四個頂點坐標(記錄與對應成員變量中即可),完成自己的位置放置,最后會回調View#onLayout(...)方法,在其內完成對 子View 的布局放置。

      :不同于 measure 流程首先對 子View 進行測量,最后才測量自己,layout 流程首先是先定位自己的布局位置,然后才處理放置 子View 的布局位置。

    • draw:繪制流程,就是將 View 繪制到屏幕上,其核心邏輯位于View#draw(...),主要就是對 背景、自身內容(onDraw(...)子View(dispatchDraw(...)裝飾(滾動條、前景等) 進行繪制。

      :通常自定義View 覆寫onDraw(...)方法,完成自己的繪制即可,ViewGroup 一般充當容器使用,因此通常無需覆寫onDraw(...)。

  2. Activity 的根視圖(即DecorView)最終是綁定到ViewRootImpl,具體是由ViewRootImpl#setView(...)進行綁定關聯的,后續(xù) View 繪制的三大流程都是均有ViewRootImpl負責執(zhí)行的。

  3. 對 View 的測量流程中,最關鍵的一步是求取 View 的MeasureSpec,View 的MeasureSpec是在其父容器MeasureSpec的約束下,結合自己的LayoutParams共同測量得到的,具體的測量邏輯由ViewGroup#getChildMeasureSpec(...)負責。
    DecorViewMeasureSpec取決于自己的LayoutParams和屏幕尺寸,具體的測量邏輯位于ViewRootImpl#getRootMeasureSpec(...)。

最后,稍微總結一下 View 繪制的整個流程:

  1. 首先,當 Activity 啟動時,會觸發(fā)調用到ActivityThread#handleResumeActivity(..),其內部會經歷一系列過程,生成DecorViewViewRootImpl等實例,最后通過ViewRootImpl#setView(decor,MATCH_PARENT)設置 Activity 根View。

    ViewRootImpl#setView(...)內容通過將其成員屬性ViewRootImpl#mView指向DecorView,完成兩者之間的關聯。

  2. ViewRootImpl成功關聯DecorView后,其內部會設置同步屏障并發(fā)送一個CALLBACK_TRAVERSAL異步渲染消息,在下一次 VSYNC 信號到來時,CALLBACK_TRAVERSAL就會得到響應,從而最終觸發(fā)執(zhí)行ViewRootImpl#performTraversals(...),真正開始執(zhí)行 View 繪制流程。

  3. ViewRootImpl#performTraversals(...)內部會依次調用ViewRootImpl#performMeasure(...)ViewRootImpl#performLayout(...)ViewRootImpl#performDraw(...)三大繪制流程,其中:

    • performMeasure(..):內部主要就是對DecorView執(zhí)行測量流程:DecorView#measure(...)。DecorView是一個FrameLayout,其布局特性是層疊布局,所占的空間就是其 子View 占比最大的寬/高,因此其測量邏輯(onMeasure(...))是先對所有 子View 進行測量,具體是通過ViewGroup#measureChildWithMargins(...)方法對 子View 進行測量,子View 測量完成后,記錄最大的寬/高,設置為自己的測量大?。ㄍㄟ^View#setMeasuredDimension(...)),如此便完成了DecorView的測量流程。

    • performLayout(...):內部其實就是調用DecorView#layout(...),如此便完成了DecorView的布局位置,最后會回調DecorView#onLayout(...),負責 子View 的布局放置,核心邏輯就是計算出各個 子View 的坐標位置,最后通過child.layout(...)完成 子View 布局。

    • performDraw():內部最終調用到的是DecorView#draw(...),該方法內部并未對繪制流程做任何修改,因此最終執(zhí)行的是View#draw(...),所以主要就是依次完成對DecorView背景、子View(dispatchDraw(...)視圖裝飾(滾動條、前景等) 的繪制。

作者:Whyn
鏈接:http://www.lxweimin.com/p/ee5d3bb5ab90
來源:簡書
著作權歸作者所有。商業(yè)轉載請聯系作者獲得授權,非商業(yè)轉載請注明出處。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容

  • 簡介 我們知道,在 Android 中,View 繪制主要包含 3 大流程: measure(測量):主要用于確定...
    Whyn閱讀 4,981評論 1 14
  • View的繪制和事件處理是兩個重要的主題,上一篇《圖解 Android事件分發(fā)機制》已經把事件的分發(fā)機制講得比較詳...
    Kelin閱讀 119,924評論 100 845
  • View的加載流程view布局一直貫穿于整個android應用中,不管是activity還是fragment都給我...
    ZEKI安卓學弟閱讀 273評論 0 0
  • 標簽: Android 源碼解析 View 關于View的繪制流程,或者說 View 的工作流程(說繪制流程容易讓...
    koguma閱讀 1,986評論 1 18
  • 最近重新看了一下任玉剛大佬的《Android 開發(fā)藝術探索》,寫了篇筆記,分享給大家。 1. ViewRootIm...
    燈不利多閱讀 977評論 0 5