Android - View 绘制流程

View 绘制流程

简介

我们知道,在 Android 中,View 绘制主要包含 3 大流程:

  • measure(测量):主要用于确定 View 的测量宽/高。

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

  • draw(绘制):结合前面两步结果,将 View 真正绘制到屏幕上。

Android 中,主要有两种视图:ViewViewGroup,其中:

  • View:就是一个独立的视图
  • ViewGroup:一个容器组件,该容器可容纳多个子视图,即ViewGroup可容纳多个ViewViewGroup,且支持嵌套。

虽然ViewGroup继承于View,但是在 View 绘制三大流程中,某些流程需要区分ViewViewGroup,它们之间的操作并不完全相同,比如:

  • ViewViewGroup都需要进行 measure,确定各自的测量宽/高。View只需直接测量自身即可,而ViewGroup通常都必须先测量所有子View,最后才能测量自己
  • 通常ViewGroup先定位自己的位置(layout),然后再定位其子View 位置(onLayout
  • View需要进行 draw 过程,而ViewGroup通常不需要(当然也可以进行绘制),因为ViewGroup更多作为容器存在,起存储放置功能

measure 流程

对 View 进行测量,主要包含两个步骤:

  1. 求取 View 的测量规格MeasureSpec
  2. 依据上一步求得的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;

    // 生成测量规格
    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);
    }
    ...
}

MeasureSpecView的一个公有静态内部类,它是一个 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 在创建时,可以指定不同的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_PARENTLayoutParams.WRAP_CONTENT具体数值

LayoutParams会受到父容器的MeasureSpec的影响,测量过程会依据两者之间的相互约束最终生成子View 的MeasureSpec,完成 View 的测量规格。

简而言之,View 的MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同决定(DecorViewMeasureSpec是由自身的LayoutParams和屏幕尺寸共同决定,参考后文)。也因此,如果要求取子View 的MeasureSpec,那么首先就需要知道父容器的MeasureSpec,层层逆推而上,即最终就是需要知道顶层View(即DecorView)的MeasureSpec,这样才能一层层传递下来,这整个过程需要结合Activity的启动过程进行分析。

Activity 视图基本结构

我们知道,在 Android 中,Activity是作为视图组件存在,主要就是在手机上显示视图界面,可以供用户操作,Activity就是 Andorid 中与用户直接交互最多的系统组件。

Activity的基本视图层次结构如下所示:

Android - Acvitity 视图架构

Activity中,实际承载视图的组件是Window(更具体来说为PhoneWindow),顶层View 是DecorView,它是一个FrameLayoutDecorView内部是一个LinearLayout,该LinearLayout由两部分组成(不同 Android 版本或主题稍有差异):TitleViewContentView,其中,TitleView就是标题栏,也就是我们常说的TitleBarActionBarContentView就是内容栏,它也是一个FrameLayout,主要用于承载我们的自定义根布局,即当我们调用setContentView(...)时,其实就是把我们自定义的布局设置到该ContentView中。

Activity启动完成后,最终就会渲染出上述层次结构的视图。

DecorView 测量规格

因此,如果我们要求取得到子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时,此时不对这两种模式进行区分,一律将 View 的大小设置为测量大小(即 SpecSize)。
      :实际上,这里可以看到,默认情况下,View 不区分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 测量规格的具体过程:

    // 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 的最终测量规格
        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
    3. 如果父容器的测量模式为UNSPECIFIED:即父容器大小无限制,此时:

      • 如果 子View 的LayoutParams为具体数值:则表示 子View 已明确设置了具体大小,因此,此时 子View 的测量大小即为自己设置的值,即childDimension,测量模式为EXACTLY
      • 如果 子View 的LayoutParamsMATCH_PARENT:表示 子View 的大小撑满父容器,由于父容器大小无限制,因此,子View 的大小也是无限制的,所以,子View 的测量模式为UNSPECIFIED,测量大小未知,通常设置为0,表示无限。
      • 如果 子View 的LayoutParamsWRAP_CONTENT:表示 子View 自适应内容大小,由于父容器大小无限制,因此,子View 的测量大小也是无限制的,所以其模式为UNSPECIFIED,测量大小无限,通常使用0进行表示。

    上述的逻辑总结如下图所示:(:图片来源于互联网,侵删)

    ViewGroup#getChildMeasureSpec

    :前面我们一直强调:子View 的MeasureSpec是由其LayoutParams和父容器的MeasureSpec共同约束构造而成,其实这部分逻辑就是ViewGroup#getChildMeasureSpec(...)方法负责的,可以很清晰看到,子View 的MeasureSpec就是在父容器MeasureSpec约束下,与其自身LayoutParams共同协商决定的。

综上,无论是对View的测量还是ViewGroup的测量,都是由View#measure(int widthMeasureSpec, int heightMeasureSpec)方法负责,然后真正执行 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(...)内部转发到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(...)内部,会创建一个ViewRootImpl实例,然后调用ViewRootImpl.setView(...)ViewRootImplDecorView关联到一起:

// 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() {
            // 开始执行绘制
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    ...
    void scheduleTraversals() {
        if (!mTraversalScheduled) { // 同一帧内不会多次调用遍历
            mTraversalScheduled = true;
            // 发送一个同步屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 将 UI 绘制任务发送到 Choreographer,回调触发 mTraversalRunnable,执行绘制操作
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ...
        }
    }
    ...
    void doTraversal() {
        ...
        performTraversals();
        ...
    }
    ...
}

ViewRootImpl.scheduleTraversals()内部主要做了两件事:

  1. 调用MessageQueue.postSyncBarrier()方法发送一个同步屏障,同步屏障可以拦截Looper对同步消息的获取与分发,即加入同步屏障后,此时Looper只会获取和处理异步消息,如果没有异步消息,则进入阻塞状态。
  2. 通过Choreographer.postCallback(...)发送一个Choreographer.CALLBACK_TRAVERSAL的异步视图渲染消息。因为前面已经发送了一个同步屏障,因此此处的视图绘制渲染消息会优先被处理。

Choreographer.postCallback(...)会申请一次 VSYNC 中断信号,当 VSYNC 信号到达时,便会回调Choreographer.doFrame(...)方法,内部会触发已经添加的回调任务,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(...)内部通过mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)发送的异步视图渲染消息就会得到回调,即回调mTraversalRunnable.run()方法,最终会执行doTraversal()方法,而doTraversal()内部又会调用performTraversals()方法,该方法才是真正开始执行 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 绘制流程,总结一下上述流程,如下图所示:

View 绘制起始流程

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 中占比最大的测量宽/高,作为自己的最终测量大小。

performLayout

ViewRootImpl#performMeasure(...)完成对DecorView的测量后,接下来执行的是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_PARENTdesiredWindowWidthdesiredWindowHeight为屏幕宽/高,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);
            // 不放置状态为 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(...),最终执行的还是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(...)负责。默认的测量规则为:如果 View 的布局参数为LayoutParams.WRAP_CONTENTLayoutParams.MATCH_PARENT,那么其测量大小为 SpecSize;如果其布局参数为LayoutParams.UNSPECIFIED,那么其测量大小为android:minWidth/android:minHeight和其背景之间的较大值。

    自定义View 通常覆写onMeasure(...)方法,在其内一般会对WRAP_CONTENT预设一个默认值,区分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(...)进行绑定关联的,后续 View 绘制的三大流程都是均有ViewRootImpl负责执行的。

  3. 对 View 的测量流程中,最关键的一步是求取 View 的MeasureSpec,View 的MeasureSpec是在其父容器MeasureSpec的约束下,结合自己的LayoutParams共同测量得到的,具体的测量逻辑由ViewGroup#getChildMeasureSpec(...)负责。
    DecorViewMeasureSpec取决于自己的LayoutParams和屏幕尺寸,具体的测量逻辑位于ViewRootImpl#getRootMeasureSpec(...)

最后,稍微总结一下 View 绘制的整个流程:

  1. 首先,当 Activity 启动时,会触发调用到ActivityThread#handleResumeActivity(..),其内部会经历一系列过程,生成DecorViewViewRootImpl等实例,最后通过ViewRootImpl#setView(decor,MATCH_PARENT)设置 Activity 根View。

    ViewRootImpl#setView(...)内容通过将其成员属性ViewRootImpl#mView指向DecorView,完成两者之间的关联。

  2. ViewRootImpl成功关联DecorView后,其内部会设置同步屏障并发送一个CALLBACK_TRAVERSAL异步渲染消息,在下一次 VSYNC 信号到来时,CALLBACK_TRAVERSAL就会得到响应,从而最终触发执行ViewRootImpl#performTraversals(...),真正开始执行 View 绘制流程。

  3. ViewRootImpl#performTraversals(...)内部会依次调用ViewRootImpl#performMeasure(...)ViewRootImpl#performLayout(...)ViewRootImpl#performDraw(...)三大绘制流程,其中:

    • performMeasure(..):内部主要就是对DecorView执行测量流程: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(...),该方法内部并未对绘制流程做任何修改,因此最终执行的是View#draw(...),所以主要就是依次完成对DecorView背景子View(dispatchDraw(...)视图装饰(滚动条、前景等) 的绘制。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容