当前位置: 移动技术网 > 移动技术>移动开发>Android > Android View 测量流程(Measure)全面解析

Android View 测量流程(Measure)全面解析

2019年07月24日  | 移动技术网移动技术  | 我要评论

前言

文章,笔者主要讲述了decorview以及viewrootimpl相关的作用,这里回顾一下上一章所说的内容:decorview是视图的顶级view,我们添加的布局文件是它的一个子布局,而viewrootimpl则负责渲染视图,它调用了一个performtraveals方法使得viewtree开始三大工作流程,然后使得view展现在我们面前。本篇文章主要内容是:详细讲述view的测量(measure)流程,主要以源码的形式呈现,源码均取自android api 21.

从viewrootimpl#performtraveals说起

我们直接从这个方法说起,因为它是整个工作流程的核心,我们看看它的源码:

private void performtraversals() {
  ...

 if (!mstopped) {
  int childwidthmeasurespec = getrootmeasurespec(mwidth, lp.width); // 1
  int childheightmeasurespec = getrootmeasurespec(mheight, lp.height);
  performmeasure(childwidthmeasurespec, childheightmeasurespec); 
  }
 }

 if (didlayout) {
  performlayout(lp, desiredwindowwidth, desiredwindowheight);
  ...
 }


 if (!canceldraw && !newsurface) {
  if (!skipdraw || mreportnextdraw) {
  if (mpendingtransitions != null && mpendingtransitions.size() > 0) {
   for (int i = 0; i < mpendingtransitions.size(); ++i) {
   mpendingtransitions.get(i).startchanginganimations();
   }
   mpendingtransitions.clear();
  }

  performdraw();
  }
 } 
 ...
}

方法非常长,这里做了精简,我们看到它里面主要执行了三个方法,分别是performmeasure、performlayout、performdraw这三个方法,在这三个方法内部又会分别调用measure、layout、draw这三个方法来进行不同的流程。我们先来看看performmeasure(childwidthmeasurespec, childheightmeasurespec)这个方法,它传入两个参数,分别是childwidthmeasurespec和childheightmeasure,那么这两个参数代表什么意思呢?要想了解这两个参数的意思,我们就要先了解measurespec。

理解measurespec

measurespec是view类的一个内部类,我们先看看官方文档对measurespec类的描述:a measurespec encapsulates the layout requirements passed from parent to child. each measurespec represents a requirement for either the width or the height. a measurespec is comprised of a size and a mode.它的意思就是说,该类封装了一个view的规格尺寸,包括view的宽和高的信息,但是要注意,measurespec并不是指view的测量宽高,这是不同的,是根据measuespec而测出测量宽高。
measurespec的作用在于:在measure流程中,系统会将view的layoutparams根据父容器所施加的规则转换成对应的measurespec,然后在onmeasure方法中根据这个measurespec来确定view的测量宽高。
我们来看看这个类的源码:

public static class measurespec {
 private static final int mode_shift = 30;
 private static final int mode_mask = 0x3 << mode_shift;

 /**
  * unspecified 模式:
  * 父view不对子view有任何限制,子view需要多大就多大
  */ 
 public static final int unspecified = 0 << mode_shift;

 /**
  * exactyly 模式:
  * 父view已经测量出子viwe所需要的精确大小,这时候view的最终大小
  * 就是specsize所指定的值。对应于match_parent和精确数值这两种模式
  */ 
 public static final int exactly = 1 << mode_shift;

 /**
  * at_most 模式:
  * 子view的最终大小是父view指定的specsize值,并且子view的大小不能大于这个值,
  * 即对应wrap_content这种模式
  */ 
 public static final int at_most = 2 << mode_shift;

 //将size和mode打包成一个32位的int型数值
 //高2位表示specmode,测量模式,低30位表示specsize,某种测量模式下的规格大小
 public static int makemeasurespec(int size, int mode) {
  if (susebrokenmakemeasurespec) {
  return size + mode;
  } else {
  return (size & ~mode_mask) | (mode & mode_mask);
  }
 }

 //将32位的measurespec解包,返回specmode,测量模式
 public static int getmode(int measurespec) {
  return (measurespec & mode_mask);
 }

 //将32位的measurespec解包,返回specsize,某种测量模式下的规格大小
 public static int getsize(int measurespec) {
  return (measurespec & ~mode_mask);
 }
 //...
 }

可以看出,该类的思路是相当清晰的,对于每一个view,包括decorview,都持有一个measurespec,而该measurespec则保存了该view的尺寸规格。在view的测量流程中,通过makemeasurespec来保存宽高信息,在其他流程通过getmode或getsize得到模式和宽高。那么问题来了,上面提到measurespec是layoutparams和父容器的模式所共同影响的,那么,对于decorview来说,它已经是顶层view了,没有父容器,那么它的measurespec怎么来的呢?

为了解决这个疑问,我们回到viewrootimpl#performtraveals方法,看①号代码处,调用了getrootmeasurespec(desiredwindowwidth,lp.width)方法,其中desiredwindowwidth就是屏幕的尺寸,并把返回结果赋值给childwidthmeasurespec成员变量(childheightmeasurespec同理),因此childwidthmeasurespec(childheightmeasurespec)应该保存了decorview的measurespec,那么我们看一下viewrootimpl#getrootmeasurespec方法的实现:

/**
 * @param windowsize
 *  the available width or height of the window
 *
 * @param rootdimension
 *  the layout params for one dimension (width or height) of the
 *  window.
 *
 * @return the measure spec to use to measure the root view.
 */
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;
 //省略...

 }
 return measurespec;
}

思路也很清晰,根据不同的模式来设置measurespec,如果是layoutparams.match_parent模式,则是窗口的大小,wrap_content模式则是大小不确定,但是不能超过窗口的大小等等。

那么到目前为止,就已经获得了一份decorview的measurespec,它代表着根view的规格、尺寸,在接下来的measure流程中,就是根据已获得的根view的measurespec来逐层测量各个子view。我们顺着①号代码往下走,来到performmeasure方法,看看它做了什么工作,viewrootimpl#performmeasure:

private void performmeasure(int childwidthmeasurespec, int childheightmeasurespec) {
 trace.tracebegin(trace.trace_tag_view, "measure");
 try {
 mview.measure(childwidthmeasurespec, childheightmeasurespec);
 } finally {
 trace.traceend(trace.trace_tag_view);
 }
}

方法很简单,直接调用了mview.measure,这里的mview就是decorview,也就是说,从顶级view开始了测量流程,那么我们直接进入measure流程。

measure 测量流程

viewgroup的测量流程

由于decorview继承自framelayout,是phonewindow的一个内部类,而framelayout没有measure方法,因此调用的是父类view的measure方法,我们直接看它的源码,view#measure:

public final void measure(int widthmeasurespec, int heightmeasurespec) {
 boolean optical = islayoutmodeoptical(this);
 if (optical != islayoutmodeoptical(mparent)) {
 ...
 if ((mprivateflags & pflag_force_layout) == pflag_force_layout ||
  widthmeasurespec != moldwidthmeasurespec ||
  heightmeasurespec != moldheightmeasurespec) {
  ...
  if (cacheindex < 0 || signoremeasurecache) {
  // measure ourselves, this should set the measured dimension flag back
  onmeasure(widthmeasurespec, heightmeasurespec);
  mprivateflags3 &= ~pflag3_measure_needed_before_layout;
  } 
 ...
}

可以看到,它在内部调用了onmeasure方法,由于decorview是framelayout子类,因此它实际上调用的是decorview#onmeasure方法。在该方法内部,主要是进行了一些判断,这里不展开来看了,到最后会调用到super.onmeasure方法,即framelayout#onmeasure方法。

由于不同的viewgroup有着不同的性质,那么它们的onmeasure必然是不同的,因此这里不可能把所有布局方式的onmeasure方法都分析一遍,因此这里选择了framelayout的onmeasure方法来进行分析,其它的布局方式读者可以自行分析。那么我们继续来看看这个方法:

@override
protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
 //获取当前布局内的子view数量
 int count = getchildcount();

 //判断当前布局的宽高是否是match_parent模式或者指定一个精确的大小,如果是则置measurematchparent为false.
 final boolean measurematchparentchildren =
  measurespec.getmode(widthmeasurespec) != measurespec.exactly ||
  measurespec.getmode(heightmeasurespec) != measurespec.exactly;
 mmatchparentchildren.clear();

 int maxheight = 0;
 int maxwidth = 0;
 int childstate = 0;

 //遍历所有类型不为gone的子view
 for (int i = 0; i < count; i++) {
 final view child = getchildat(i);
 if (mmeasureallchildren || child.getvisibility() != gone) {
  //对每一个子view进行测量
  measurechildwithmargins(child, widthmeasurespec, 0, heightmeasurespec, 0);
  final layoutparams lp = (layoutparams) child.getlayoutparams();
  //寻找子view中宽高的最大者,因为如果framelayout是wrap_content属性
  //那么它的大小取决于子view中的最大者
  maxwidth = math.max(maxwidth,
   child.getmeasuredwidth() + lp.leftmargin + lp.rightmargin);
  maxheight = math.max(maxheight,
   child.getmeasuredheight() + lp.topmargin + lp.bottommargin);
  childstate = combinemeasuredstates(childstate, child.getmeasuredstate());
  //如果framelayout是wrap_content模式,那么往mmatchparentchildren中添加
  //宽或者高为match_parent的子view,因为该子view的最终测量大小会受到framelayout的最终测量大小影响
  if (measurematchparentchildren) {
  if (lp.width == layoutparams.match_parent ||
   lp.height == layoutparams.match_parent) {
   mmatchparentchildren.add(child);
  }
  }
 }
 }

 // account for padding too
 maxwidth += getpaddingleftwithforeground() + getpaddingrightwithforeground();
 maxheight += getpaddingtopwithforeground() + getpaddingbottomwithforeground();

 // check against our minimum height and width
 maxheight = math.max(maxheight, getsuggestedminimumheight());
 maxwidth = math.max(maxwidth, getsuggestedminimumwidth());

 // check against our foreground's minimum height and width
 final drawable drawable = getforeground();
 if (drawable != null) {
 maxheight = math.max(maxheight, drawable.getminimumheight());
 maxwidth = math.max(maxwidth, drawable.getminimumwidth());
 }

 //保存测量结果
 setmeasureddimension(resolvesizeandstate(maxwidth, widthmeasurespec, childstate),
  resolvesizeandstate(maxheight, heightmeasurespec,
   childstate << measured_height_state_shift));

 //子view中设置为match_parent的个数
 count = mmatchparentchildren.size();
 //只有framelayout的模式为wrap_content的时候才会执行下列语句
 if (count > 1) {
 for (int i = 0; i < count; i++) {
  final view child = mmatchparentchildren.get(i);
  final marginlayoutparams lp = (marginlayoutparams) child.getlayoutparams();

  //对framelayout的宽度规格设置,因为这会影响子view的测量
  final int childwidthmeasurespec;

  /**
  * 如果子view的宽度是match_parent属性,那么对当前framelayout的measurespec修改:
  * 把widthmeasurespec的宽度规格修改为:总宽度 - padding - margin,这样做的意思是:
  * 对于子viw来说,如果要match_parent,那么它可以覆盖的范围是framelayout的测量宽度
  * 减去padding和margin后剩下的空间。
  *
  * 以下两点的结论,可以查看getchildmeasurespec()方法:
  *
  * 如果子view的宽度是一个确定的值,比如50dp,那么framelayout的widthmeasurespec的宽度规格修改为:
  * specsize为子view的宽度,即50dp,specmode为exactly模式
  * 
  * 如果子view的宽度是wrap_content属性,那么framelayout的widthmeasurespec的宽度规格修改为:
  * specsize为子view的宽度减去padding减去margin,specmode为at_most模式
  */
  if (lp.width == layoutparams.match_parent) {
  final int width = math.max(0, getmeasuredwidth()
   - getpaddingleftwithforeground() - getpaddingrightwithforeground()
   - lp.leftmargin - lp.rightmargin);
  childwidthmeasurespec = measurespec.makemeasurespec(
   width, measurespec.exactly);
  } else {
  childwidthmeasurespec = getchildmeasurespec(widthmeasurespec,
   getpaddingleftwithforeground() + getpaddingrightwithforeground() +
   lp.leftmargin + lp.rightmargin,
   lp.width);
  }
  //同理对高度进行相同的处理,这里省略...

  //对于这部分的子view需要重新进行measure过程
  child.measure(childwidthmeasurespec, childheightmeasurespec);
 }
 }
}

由以上的framelayout的onmeasure过程可以看出,它还是做了相当多的工作的,这里简单总结一下:首先,framelayout根据它的measurespec来对每一个子view进行测量,即调用measurechildwithmargin方法,这个方法下面会详细说明;对于每一个测量完成的子view,会寻找其中最大的宽高,那么framelayout的测量宽高会受到这个子view的最大宽高的影响(wrap_content模式),接着调用setmeasuredimension方法,把framelayout的测量宽高保存。最后则是特殊情况的处理,即当framelayout为wrap_content属性时,如果其子view是match_parent属性的话,则要重新设置framelayout的测量规格,然后重新对该部分view测量。

在上面提到setmeasuredimension方法,该方法用于保存测量结果,在上面的源码里面,该方法的参数接收的是resolvesizeandstate方法的返回值,那么我们直接看view#resolvesizeandstate方法:

public static int resolvesizeandstate(int size, int measurespec, int childmeasuredstate) {
 final int specmode = measurespec.getmode(measurespec);
 final int specsize = measurespec.getsize(measurespec);
 final int result;
 switch (specmode) {
 case measurespec.at_most:
  if (specsize < size) {
  result = specsize | measured_state_too_small;
  } else {
  result = size;
  }
  break;
 case measurespec.exactly:
  result = specsize;
  break;
 case measurespec.unspecified:
 default:
  result = size;
 }
 return result | (childmeasuredstate & measured_state_mask);
}

可以看到该方法的思路是相当清晰的,当specmode是exactly时,那么直接返回measurespec里面的宽高规格,作为最终的测量宽高;当specmode时at_most时,那么取measurespec的宽高规格和size的最小值。(注:这里的size,对于framelayout来说,是其最大子view的测量宽高)。

小结:那么到目前为止,以decorview为切入点,把viewgroup的测量流程详细地分析了一遍,在viewrootimpl#performtraversals中获得decorview的尺寸,然后在performmeasure方法中开始测量流程,对于不同的layout布局有着不同的实现方式,但大体上是在onmeasure方法中,对每一个子view进行遍历,根据viewgroup的measurespec及子view的layoutparams来确定自身的测量宽高,然后最后根据所有子view的测量宽高信息再确定父容器的测量宽高。

那么接下来,我们继续分析对于一个子view来说,是怎么进行测量的。

view的测量流程

还记得我们上面在framelayout测量内提到的measurechildwithmargin方法,它接收的主要参数是子view以及父容器的measurespec,所以它的作用就是对子view进行测量,那么我们直接看这个方法,viewgroup#measurechildwithmargins:

protected void measurechildwithmargins(view child,
 int parentwidthmeasurespec, int widthused,
 int parentheightmeasurespec, int heightused) {
 final marginlayoutparams lp = (marginlayoutparams) child.getlayoutparams();

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

 child.measure(childwidthmeasurespec, childheightmeasurespec); // 1
}

由源码可知,里面调用了getchildmeasurespec方法,把父容器的measurespec以及自身的layoutparams属性传递进去来获取子view的measurespec,这也印证了“子view的measurespec由父容器的measurespec和自身的layoutparams共同决定”这个结论。那么,我们一起来看看viewgroup#getchildmeasurespec方法:

public static int getchildmeasurespec(int spec, int padding, int childdimension) {
 int specmode = measurespec.getmode(spec);
 int specsize = measurespec.getsize(spec);

 //size表示子view可用空间:父容器尺寸减去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:
 //省略..具体可自行参考源码
 break;

 // parent asked to see how big we want to be
 case measurespec.unspecified:
 //省略...具体可自行参考源码
 break;
 }
 return measurespec.makemeasurespec(resultsize, resultmode);
}

上面方法也非常容易理解,大概是根据不同的父容器的模式及子view的layoutparams来决定子view的规格尺寸模式等。那么,这里根据上面的逻辑,列出不同的父容器的measurespec和子view的layoutparams的组合情况下所出现的不同的子view的measurespec:

(注:该表格呈现形式参考自《android 开发艺术探索》 任玉刚 著)

当子view的measurespec获得后,我们返回measurechildwithmargins方法,接着就会执行①号代码:child.measure方法,意味着,绘制流程已经从viewgroup转移到子view中了,可以看到传递的参数正是我们刚才获取的子view的measurespec,接着会调用view#measure,这在上面说过了,这里不再赘述,然后在measure方法,会调用onmeasure方法,当然了,对于不同类型的view,其onmeasure方法是不同的,但是对于不同的view,即使是自定义view,我们在重写的onmeasure方法内,也一定会调用到view#onmeasure方法的,因此我们看看它的源码:

protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
 setmeasureddimension(getdefaultsize(getsuggestedminimumwidth(), widthmeasurespec),
  getdefaultsize(getsuggestedminimumheight(), heightmeasurespec));
}

显然,这里调用了setmeasuredimension方法,上面说过该方法的作用是设置测量宽高,而测量宽高则是从getdefaultsize中获取,我们继续看看这个方法view#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;
}

好吧,又是类似的代码,根据不同模式来设置不同的测量宽高,我们直接看at_most和exactly模式,它直接把specsize返回了,即view在这两种模式下的测量宽高直接取决于specsize规格。也即是说,对于一个直接继承自view的自定义view来说,它的wrap_content和match_parent属性的效果是一样的,因此如果要实现自定义view的wrap_content,则要重写onmeasure方法,对wrap_content属性进行处理。
接着,我们看unspecified模式,这个模式可能比较少见,一般用于系统内部测量,它直接返回的是size,而不是specsize,那么size从哪里来的呢?再往上看一层,它来自于getsuggestedminimumwidth()或getsuggestedminimumheight(),我们选取其中一个方法,看看源码,view#getsuggestedminimumwidth:

protected int getsuggestedminimumwidth() {
 return (mbackground == null) ? mminwidth : max(mminwidth, mbackground.getminimumwidth());
}

从以上逻辑可以看出,当view没有设置背景的时候,返回mminwidth,该值对应于android:minwidth属性;如果设置了背景,那么返回mminwidth和mbackground.getminimumwidth中的最大值。那么mbackground.getminimumwidth又是什么呢?其实它代表了背景的原始宽度,比如对于一个bitmap来说,它的原始宽度就是图片的尺寸。到此,子view的测量流程也完成了。

总结

这里简单概括一下整个流程:测量始于decorview,通过不断的遍历子view的measure方法,根据viewgroup的measurespec及子view的layoutparams来决定子view的measurespec,进一步获取子view的测量宽高,然后逐层返回,不断保存viewgroup的测量宽高。

从文章开始到现在,view的测量流程已经全部分析完毕,view的measure流程是三大流程中最复杂的一个流程,其中的measurespec贯穿了整个测量流程,占有非常重要的地位,希望读者仔细体会这个流程,最后希望这篇文章能帮助你对view的测量流程有进一步的了解,谢谢阅读。

更多阅读
android view 布局流程(layout)完全解析
android view 绘制流程(draw) 完全解析

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网