Android View的绘制流程(五)-Measure

982 阅读5分钟

「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战

相关文章:
Android View的绘制流程(一)-绘制流程以及Activity视图介绍
Android View的绘制流程(二)-Activity视图-DecorView
Android View的绘制流程(三)-Activity视图-WindowManager
Android View的绘制流程(四)-Activity视图-ViewRootImpl
Android View的绘制流程(五)-Measure
Android View的绘制流程(六)-Layout
Android View的绘制流程(七)-Draw

前面几篇文章介绍了Activity视图,从创建DecorView到传递给WindowManager再到ViewRootImpl绘制的这个过程的主体源码介绍,整体的绘制流程就是这样,接下来就开始View的绘制了,上一篇结尾已经说了,绘制的过程分为:

  • measure: 判断是否需要重新计算 View 的大小,需要的话则计算;
  • layout: 判断是否需要重新计算 View 的位置,需要的话则计算;
  • draw: 判断是否需要重新绘制 View,需要的话则重绘制。 今天主要介绍测量Mearsure

Measure

在介绍Mearsure之前,先说下 MeasureSpec,MeasureSpec 封装了父布局传递给子布局的布局要求,它通过一个 32 位 int 类型的值来表示,该值包含了两种信息,高两位表示的是 SpecMode(测量模式),低 30 位表示的是 SpecSize(测量的具体大小)。下面看下源码:

/**  
 * 三种SpecMode: 
 * 1.UNSPECIFIED 
 * 父 ViewGroup 没有对子View施加任何约束,子 view 可以是任意大小。这种情况比较少见,主要用于系统内部多次measure的情形,
 * 用到的一般都是可以滚动的容器中的子view,比如ListView、GridView、RecyclerView中某些情况下的子view就是这种模式。
 * 一般来说,我们不需要关注此模式。
 * 2.EXACTLY 
 * 该 view 必须使用父 ViewGroup 给其指定的尺寸。对应 match_parent 或者具体数值(比如30dp)
 * 3.AT_MOST 
 * 该 View 最大可以取父ViewGroup给其指定的尺寸。对应wrap_content
 *  
 * MeasureSpec使用了二进制去减少对象的分配。 
 */  
public class MeasureSpec {  
        // 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和第二高位也就是32和31位做标志位)  
        private static final int MODE_SHIFT = 30;  
 
        // 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)  
        // (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)  
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
 
        // 0向左进位30,就是00 00000000000(00后跟30个0)  
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
        // 1向左进位30,就是01 00000000000(01后跟30个0)  
        public static final int EXACTLY     = 1 << MODE_SHIFT;  
        // 2向左进位30,就是10 00000000000(10后跟30个0)  
        public static final int AT_MOST     = 2 << MODE_SHIFT;  
 
        /** 
         * 根据提供的size和mode得到一个详细的测量结果 
         */  
        // 第一个return:
        // measureSpec = size + mode;   (注意:二进制的加法,不是十进制的加法!)  
        // 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值  
        // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100  
        // 
        // 第二个return:
        // size &; ~MODE_MASK就是取size 的后30位,mode &amp; MODE_MASK就是取mode的前两位,最后执行或运算,得出来的数字,前面2位包含代表mode,后面30位代表size
        public static int makeMeasureSpec(int size, int mode) {  
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode &  MODE_MASK);
            }
        }  
 
        /** 
         * 获得SpecMode
         */  
        // mode = measureSpec &amp; MODE_MASK;  
        // MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。  
        // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值  
        public static int getMode(int measureSpec) {  
            return (measureSpec & MODE_MASK);  
        }  
 
        /** 
         * 获得SpecSize 
         */  
        // size = measureSpec &  ~MODE_MASK;  
        // 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size  
        public static int getSize(int measureSpec) {  
            return (measureSpec &  ~MODE_MASK);  
        }  
}  

我们在布局中用到的wrap_content和match_parent,代表的值分别是-2和-1。

既然是测量,那肯定需要知道绘制的View的尺寸大小,也就是开始创建的DecorView的大小,上一篇文章中我们可以看到绘制的方法调用的是performTraversals,看下源码:

//Activity窗口的宽度和高度
int desiredWindowWidth;
int desiredWindowHeight;
...
//用来保存窗口宽度和高度,来自于全局变量mWinFrame,这个mWinFrame保存了窗口最新尺寸
Rect frame = mWinFrame;
//构造方法里mFirst赋值为true,意思是第一次执行遍历吗    
if (mFirst) {
    //是否需要重绘
    mFullRedrawNeeded = true;
    //是否需要重新确定Layout
    mLayoutRequested = true;
    
    // 这里又包含两种情况:是否包括状态栏
    
    //判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度
    if (shouldUseDisplaySize(lp)) {
        // NOTE -- system code, won't try to do compat mode.
        Point size = new Point();
        mDisplay.getRealSize(size);
        desiredWindowWidth = size.x;
        desiredWindowHeight = size.y;
    } else {
        //宽度和高度为整个屏幕的值
        Configuration config = mContext.getResources().getConfiguration();
        desiredWindowWidth = dipToPx(config.screenWidthDp);
        desiredWindowHeight = dipToPx(config.screenHeightDp);
    }
    ...
 else{
    
        // 这是window的长和宽改变了的情况,需要对改变的进行数据记录
    
        //如果不是第一次进来这个方法,它的当前宽度和高度就从之前的mWinFrame获取
        desiredWindowWidth = frame.width();
        desiredWindowHeight = frame.height();
        /**
         * mWidth和mHeight是由WindowManagerService服务计算出的窗口大小,
         * 如果这次测量的窗口大小与这两个值不同,说明WMS单方面改变了窗口的尺寸
         */
        if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
            if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
            //需要进行完整的重绘以适应新的窗口尺寸
            mFullRedrawNeeded = true;
            //需要对控件树进行重新布局
            mLayoutRequested = true;
            //window窗口大小改变
            windowSizeMayChange = true;
        }
 }
    ...
    // 进行预测量
    if (layoutRequested){
        ...
        if (mFirst) {
            // 视图窗口当前是否处于触摸模式。
            mAttachInfo.mInTouchMode = !mAddedTouchMode;
            //确保这个Window的触摸模式已经被设置
            ensureTouchModeLocally(mAddedTouchMode);
        } else {
            //六个if语句,判断insects值和上一次比有什么变化,不同的话就改变insetsChanged
            //insects值包括了一些屏幕需要预留的区域、记录一些被遮挡的区域等信息
            if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
                    insetsChanged = true;
            }
            ...
            
          //  这里有一种情况,我们在写dialog时,会手动添加布局,当设定宽高为Wrap_content时,会把屏幕的宽高进行赋值,给出尽量长的宽度
            
            /**
             * 如果当前窗口的根布局的width或height被指定为 WRAP_CONTENT 时,
             * 比如Dialog,那我们还是给它尽量大的长宽,这里是将屏幕长宽赋值给它
             */
            if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                    || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                windowSizeMayChange = true;
                //判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度
                if (shouldUseDisplaySize(lp)) {
                    // NOTE -- system code, won't try to do compat mode.
                    Point size = new Point();
                    mDisplay.getRealSize(size);
                    desiredWindowWidth = size.x;
                    desiredWindowHeight = size.y;
                } else {
                    Configuration config = res.getConfiguration();
                    desiredWindowWidth = dipToPx(config.screenWidthDp);
                    desiredWindowHeight = dipToPx(config.screenHeightDp);
                }
            }
        }
    }
}

上面的代码比较多,主要是2个意思,首先判断是否是第一次测量大小,如果是,判断是否有状态栏,根据状态栏来确定屏幕的高度和显示高度,其次,如果不是第一次,那么从 mWinFrame 获取,并和之前保存的长宽高进行比较,不相等的话就需要重新测量确定高度。

确定了DecorView的尺寸后,就开始调用measureHierarchy来确定MeasureSpec,具体怎么去确定这个MeasureSpec我们不需要太多关心,但是想了解也可以去看下源码,但是MeasureSpec 的计算方法需要知道一下:

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

该方法主要是根据 View 的 MeasureSpec 是根据宽高的参数来划分的。

  • MATCH_PARENT :精确模式,大小就是窗口的大小;
  • WRAP_CONTENT :最大模式,大小不定,但是不能超过窗口的大小;
  • 固定大小:精确模式,大小就是指定的具体宽高,比如100dp。 对于 DecorView 来说就是走第一个 case,到这里 DecorView 的 MeasureSpec 就确定了,从 MeasureSpec 可以得出 DecorView 的宽高的约束信息。DecorView的MeasureSpec确定了,那么接下来开始获取子View的MeasureSpec。
    当父 ViewGroup 对子 View 进行测量时,会调用 View 类的 measure 方法,这是一个 final 方法,无法被重写。ViewGroup 会传入自己的 widthMeasureSpec 和  heightMeasureSpec,分别表示父 View 对子 View 的宽度和高度的一些限制条件。尤其是当 ViewGroup 是 WRAP_CONTENT 的时候,需要优先测量子 View,只有子 View 宽高确定,ViewGroup 才能确定自己到底需要多大的宽高。

当 DecorView 的 MeasureSpec 确定以后,ViewRootImpl 内部会调用 performMeasure 方法:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

该方法传入的是对 DecorView 的 MeasureSpec,其中 mView 就是 DecorView 的实例,接下来看 measure() 的具体逻辑:

/**
 * 调用这个方法来算出一个View应该为多大。参数为父View对其宽高的约束信息。
 * 实际的测量工作在onMeasure()方法中进行
 */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  ......

  // Suppress sign extension for the low bytes
   long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
   if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

// 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT标记,则强制重新布局
  // 比如调用View.requestLayout()会在mPrivateFlags中加入此标记
  final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
  final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
      || heightMeasureSpec != mOldHeightMeasureSpec;
  final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
      && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
  final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
      && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
  final boolean needsLayout = specChanged
      && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

  // 需要重新布局  
  if (forceLayout || needsLayout) {

    // first clears the measured dimension flag 标记为未测量状态
    mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
    // 对阿拉伯语、希伯来语等从右到左书写、布局的语言进行特殊处理
    resolveRtlPropertiesIfNeeded();

// 先尝试从缓从中获取,若forceLayout为true或是缓存中不存在或是
    // 忽略缓存,则调用onMeasure()重新进行测量工作
    int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
      // measure ourselves, this should set the measured dimension flag back
      onMeasure(widthMeasureSpec, heightMeasureSpec);
      . . .
    } else {
      // 缓存命中,直接从缓存中取值即可,不必再测量
      long value = mMeasureCache.valueAt(cacheIndex);
      // Casting a long to int drops the high 32 bits, no mask needed
      setMeasuredDimensionRaw((int) (value >> 32), (int) value);
      mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;

    }

   // 如果自定义的View重写了onMeasure方法,但是没有调用setMeasuredDimension()方法就会在这里抛出错误;

   // flag not set, setMeasuredDimension() was not invoked, we raise
   // an exception to warn the developer
   if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
       throw new IllegalStateException("View with id " + getId() + ": "
              + getClass().getName() + "#onMeasure() did not set the"
              + " measured dimension by calling"
              + " setMeasuredDimension()");
   } 


      //到了这里,View已经测量完了并且将测量的结果保存在View的mMeasuredWidth和mMeasuredHeight中,将标志位置为可以layout的状态

    mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;

  }
  mOldWidthMeasureSpec = widthMeasureSpec;
  mOldHeightMeasureSpec = heightMeasureSpec;
 // 保存到缓存中
  mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
      (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

上面的代码主要就是测量的View的尺寸,通过boolean值去控制是否要测量,需不需要从缓存中读取值,最终调用 onMeasure() 方法去完成实际的测量工作,并且把测量结果值保存到缓存中,通过mPrivateFlags值保存测量状态。

获取子View的MearsureSpec:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // 父 view 的 mode 和 size
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
     // 去掉 padding
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

该方法主要是获取子 View 的 MeasureSpec,然后调用 child.measure() 来完成子 View 的测量,从上面代码可以看出子View的MeasureSpec是由其直接父 View 的 MeasureSpec 和 View 自身的属性 LayoutParams (LayoutParams 有宽高尺寸值等信息)共同决定。代码总结出来就是:
当子View的宽高固定,那么View的大小就由LayoutParams 中大小;

当子View的宽高为match,父元素为精度模式(EXACTLY),那么 View 的 MeasureSpec 也是精准模式他的大小不会超过父容器的剩余空间;

当子view为wrap,不管父元素是精准模式还是最大化模式(AT_MOST),View 的 MeasureSpec 总是为最大化模式并且大小不超过父容器的剩余空间。

通过图片表示如下:

image.png

View.measure()  代码逻辑前面已经分析过了,最终会调用 onMeasuere 方法,下面看下 View.onMeasuere() 的代码:

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

上面方法中调用了 方法中调用了 setMeasuredDimension()方法,setMeasuredDimension()又调用了 getDefaultSize() 方法。getDefaultSize() 又调用了getSuggestedMinimumWidth()和 getSuggestedMinimumHeight()。

getSuggestedMinimumWidth和getSuggestedMinimumHeight的逻辑是一样的,主要是获取最小宽高。

 下面看下 getDefaultSize() 的代码逻辑:

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

从源码可以看出来,getDefaultSize中将 wrap_content 跟 match_parent 等同起来,getDefaultSize是View的onMeasure()中默认调用的,也就是说,自定义View设置了wrap和match的效果是一样的,如果自定义View想要实现wrap,那么就需要重写onMeasure,这样说来,是不是很熟悉,在我们开发过程中,最早对ListView做重写,是不是实现了onMeasure方法,解决listView显示不全的问题。

子View绘制完后,还需要计算childState,这个就不多说了,最终是调用resolveSizeAndState()这个方法,源码就不贴了,获取了状态值,就可以拿到View的尺寸了:

 // 确定父 View 的宽高
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

想了解resolveSizeAndState可以参考网上的介绍,到这里View的测量就算全部介绍完了,下一篇开始介绍View 的 layout 和 draw 过程。