【Android源码阅读系列一】一个bug引发的血案:阅读Android源码 MeasureSpec类(API版本:23)

来源:互联网 发布:中美贸易顺差数据 编辑:程序博客网 时间:2024/04/18 14:41

【1 引言】

本文来源于一个bug,后来越走越远跑偏了,从LinearLayouy-》View 》-MeasureSpec-》位运算-》计算机的编码(原码反码补码)这已经到计算机组成原理了~~于是权当做一次笔记记录:

起源的bug:

使用流式布局时( 关于流式布局可见:http://blog.csdn.net/zxt0601/article/details/50533658),布局内item太多,经过看log,已经达到四个屏幕的高度,而最外层没有嵌套ScrollView,导致无法滑动~内容显示不全,后来我在最外层嵌套了ScrollView发现还是无法滑动,我就怀疑是我自定义的流式布局onMeasure()方法没有写好,经过log分析,原来是在MeasureSpec为UNSPECIFIED 时,我返回的是父控件允许的最大的高度(Match_parent),应该是返回该View想要的高度(wrap_content,四个屏幕的高度), ok bug虽然解决了,但是我就好奇了系统的源码是怎么写的。我知道LinearLayout的vertical模式,如果内容太多,外面套一个ScrollView,是可以滑动的,说明它是可以适应内容的高度的。

ok,那就看呗~原本是想趁机好好看一下LinearLayout 的vertical里是怎么onMeasure的,结果看进去发现它调用了一个 View类的  resolveSizeAndState(int size, int measureSpec, int childMeasuredState) 方法,返回想要的高度,ok 那我们继续看~,它内部当然免不了调用MeasureSpec.getMode(measureSpec)  MeasureSpec.getSize(measureSpec) 方法,这两个方法 和 MeasureSpec类的另外一个方法 makeMeasureSpec()我们应该都不陌生,在resolveSizeAndState()方法里,经过一番比较,最终返回一个值作为高度。我又点进去MeasureSpec里查看,发现里面各种位运算,好吧 位运算,我都有点忘了~那么我就查查资料先好好了解位运算吧。结果看看位运算,算来算去感觉和我记忆里不太一样了,于是我又去看了原码反码补码。。。一路就这么任性的跑偏了,来到了计算机组成原理的范畴。。。

这里插一句,其实不止这一个类,系统源码里大量使用到了位运算,是因为位运算比较高效。虽然用其他运算符也能实现同样的效果,可是效率却不如位运算来的高。那位运算有啥缺点呢。就是可读性差了点,我们日常“凡人”开发,难免要组员维护 甚至后人维护你的代码,如果都用位运算,别人阅读你的代码的难度难免会增加。

ok~让我们逆转时光,回到我们故事的起点,LinearLayout。Start!

===================================================================

【2 LinearLayout的onMeasure()】

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    if (mOrientation == VERTICAL) {        measureVertical(widthMeasureSpec, heightMeasureSpec);    } else {        measureHorizontal(widthMeasureSpec, heightMeasureSpec);    }}
判断如果竖直方向就走measureVertical(widthMeasureSpec, heightMeasureSpec);,否则走measureHorizontal(widthMeasureSpec, heightMeasureSpec);(如果源码都能这么简单直接,那就太好啦。)

/** * Measures the children when the orientation of this LinearLayout is set * to {@link #VERTICAL}. * * @param widthMeasureSpec Horizontal space requirements as imposed by the parent. * @param heightMeasureSpec Vertical space requirements as imposed by the parent. * * @see #getOrientation() * @see #setOrientation(int) * @see #onMeasure(int, int) */void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
tips:其实阅读源码时,不要小看方法头部的注释哦~一般都能给我们一些重要的提示信息。

这里简单翻译一下:当然LinearLayout的方向(orientation)设置为Vertical时,便调用这个方法测量子view。第一个参数widthMeasureSpec,是由父控件传递的水平方向的空间要求,第二个参数heightMeasureSpec  也是由父控件传递的,竖直方向的空间要求。

这里有个结论可以记一下先:view 的widthMeasureSpec 和heightMeasureSpec  是由view自己设置的width和height 和 父控件的width height 共同决定的。

measureVertical方法前面几句是定义变量,

然后就是熟悉的,MeasureSpec.getMode()获得水平 和 竖直方向上的测量模式。ok 逃不掉的,点getMode方法进去看吧~

final int widthMode = MeasureSpec.getMode(widthMeasureSpec);final int heightMode = MeasureSpec.getMode(heightMeasureSpec);


===================================================================

【3 MeasureSpec】

这个方法位于View->MeasureSpec->

/** * Extracts the mode from the supplied measure specification. * * @param measureSpec the measure specification to extract the mode from * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, *         {@link android.view.View.MeasureSpec#AT_MOST} or *         {@link android.view.View.MeasureSpec#EXACTLY} */public static int getMode(int measureSpec) {    return (measureSpec & MODE_MASK);}

方法体代码倒是少,就用了&与操作 位运算,有点晕晕的,还是看看注释写的是什么意思吧:通过提供的测量Spec 提取测量模式,关于return 值,就是自定义View里onMeasure方法判断的那三种:关于这三个常量的定义,在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;

UNSPECIFIED     (测量规范模式:父控件没有对控件强加任何约束。控件可以是它希望的任何大小。),那么从这我们也可以得出我们文首,我今天遇到的bug,在这种模式下,应该返回我们计算的高度,而非父布局的最大高度。  

EXACTLY     (测量规格模式:父控件已经为控件确定一个确切的大小。不管控件想要多大。一般是设置了明确的值或者是MATCH_PARENT)。

 AT_MOST   (测量规格模式:表示子布局限制在一个最大值内,一般为WARP_CONTENT) 

那么这个位运算是干嘛的,<<我记得是向左移位,看一下MODE_SHIFT 的定义,是30.那就是向左移30位,为什么是30位呢?这个疑问先丢这里,前有 & ,后有<<。我们可以好好去看一下位运算了。

===================================================================

【4 位运算铺垫知识】

关于位运算,说实话,我和同事讨论了一下,可能我们的段位比较低,一致认为是比较难理解,难一眼看出值,在稍微复杂一点的位运算时,我和她都是要用草稿纸写出来才能得到正确的结果。

了解位运算前,我们先复习一下一些java和计算机的基础知识。


以下是java的基本数据类型,以及在内存中所占的位数。(另外,一个字节等于8位)。记住int 为32,第6节会用到。

 数据类型                           所占位数
      byte                                       8 
      boolean                                8
      short                                    16
      int                                         32 
      long                                      64 
      float                                      32 
      double                                  64 
      char                                     16


在计算机中,参与运算的是二进制数的补码形式,(为什么用补码,就不深究了,我挖不动了,因为我感觉已经离题很远了。。。,当初老师讲的已经忘记= =!,网上搜到的结论如下,采用补码进行运算有两个好处,一个就是刚才所说的统一加减法;二就是可以让符号位作为数值直接参加运算,而最后仍然可以得到正确的结果符)

而二进制数有四种表现形式:原码,反码,补码,移码。

以byte类型的变量来举例(只有8位 ),(参考自 http://blog.csdn.net/liushuijinger/article/details/7429197)


原码:

就是一个数的二进制形式

X=+3 , [X]原= 0000 0011    X=-3, [X]原= 1000 0011
位数不够的用0补全。

其中,最高位为符号位:正数为0,负数为1。剩下的n-1位表示该数的绝对值,

PS:正数的原、反、补码都一样:0的原码跟反码都有两个,因为这里0被分为+0和-0。


反码:

反码就是在原码的基础上,符号位不变其他位按位取反 (就是0变1,1变0)就可以了。

X=-3,[X]原= 1000 0011 ,[X]反=1111 1100


补码:

补码就是在反码的基础上+1.

X=-3,[X]原= 1000 0011 ,[X]反=1111 1100,[X]补=1111 1101


移码:(其实我上大学时考试都没用过移码。。。不知道什么用 如果有知道的 可以评论告诉我一下 谢谢)

不管正负数,只要将其补码的符号位取反即可。

X=-3,[X]原= 1000 0011 ,[X]反=1111 1100,[X]补=1111 1101,[X]移=01111 1101



===================================================================

【5 位运算】

通过5,我们一定要记住,

位运算时,参与运算的都是补码,且正数 正反补码相同。

Java的位运算(bitwise operators)直接对整数类型的位进行操作,

由于数据类型所占字节是有限的,而位移的大小却可以任意大小,所以可能存在位移后超过了该数据类型的表示范围,于是有了这样的规定:
如果为int数据类型,且位移位数大于32位,则首先把位移位数对32取模,不然位移超过总位数没意义的。所以4>>32与4>>0是等价的。
如果为long类型,且位移位数大于64位,则首先把位移位数对64取模,若没超过64位则不用对位数取模。
如果为byte、char、short,则会首先将他们扩充到32位,然后的规则就按照int类型来处理。

位运算符具体如下表:

运算符

说明

<<

左移位,在低位处补0

>>

右移位,若为正数则高位补0,若为负数则高位补1

>>>

无符号右移位,无论正负都在高位补0

&

与(AND),对两个整型操作数中对应位执行布尔代数,两个位都为1时输出1,否则0

|

或(OR),对两个整型操作数中对应位执行布尔代数,两个位都为0时输出0,否则1

~

非(NOT),一元运算符。

^

异或(XOR),对两个整型操作数中对应位执行布尔代数,两个位相等0,不等1

<<=

左移位赋值。

>>=

右移位赋值。

>>>=

无符号右移位赋值。

&=

按位与赋值。

|=

按位或赋值。

^=

按位异或赋值。



以  int型变量 -5,为例,

[-5]原=1000 0000 0000 0000 0000 0000 0000 0101,

[-5]补=1111 1111 1111 1111 1111 1111 1111 1010,

[-5]补=1111 1111 1111 1111 1111 1111 1111 1011,

在实际中,位运算比较令人头疼的也就是前三个移位运算,于是写了个demo验证:

public static void main(String[] args) {    /**     * java位运算:     * << 左移位,在低位处补0     * >> 右移位,若为正数则高位补0,若为负数则高位补1     * >>> 无符号右移位,无论正负都在高位补0     * &与(AND),对两个整型操作数中对应位执行布尔代数,两个位都为1时输出1,否则0。     * |或(OR),对两个整型操作数中对应位执行布尔代数,两个位都为0时输出0,否则1。     * ~非(NOT),一元运算符。     * ^异或(XOR),对两个整型操作数中对应位执行布尔代数,两个位相等0,不等1。     */    /**     以  int型变量 -5,为例,     [-5]原=1000 0000 0000 0000 0000 0000 0000 0101,     [-5]补=1111 1111 1111 1111 1111 1111 1111 1010,     [-5]补=1111 1111 1111 1111 1111 1111 1111 1011,     */    // 1、左移( << )    // 1111 1111 1111 1111 1111 1111 1111 1011 然后左移2位后,低位补0://    // 1111 1111 1111 1111 1111 1111 1110 1100 运算结果    // 1000 0000 0000 0000 0000 0000 0001 0100 :运算结果的原码:十进制下为 -  (4+16) = -20    System.out.println("-5 << 2= " + (-5 << 2));// 运行结果是-20    // 2、右移( >> ) 高位补符号位    // 1111 1111 1111 1111 1111 1111 1111 1011 然后右移2位,高位补1:    // 1111 1111 1111 1111 1111 1111 1111 1110 运算结果    // 1000 0000 0000 0000 0000 0000 0001 0010 :运算结果的原码:十进制下为 -  (2) = -2    System.out.println("-5 >> 2= " + (-5 >> 2));// 运行结果是-2    // 3、无符号右移( >>> ) 高位补0    // 1111 1111 1111 1111 1111 1111 1111 1011 右移2位,高位补0    // 0011 1111 1111 1111 1111 1111 1111 1110 运算结果 (符号位是0,运算结果应该是一个很大的正数)    System.out.println("-5 >>> 2= " + (-5 >>> 2));// 结果是1073741822    }
关于其它的位运算,在程序里一般都是正数的位运算,比较简单。就不举例。

好了饶了一圈,已经懵逼了,越走越深,是时候慢慢回去了。

===================================================================

【6 回顾MessureSpec】

这个时候我们再回到第3节,回过头看看MeasureSpec定义的这五个常量:

private static final int MODE_SHIFT = 30;private static final int MODE_MASK  = 0x3 << MODE_SHIFT;public static final int UNSPECIFIED = 0 << MODE_SHIFT;public static final int EXACTLY     = 1 << MODE_SHIFT;public static final int AT_MOST     = 2 << MODE_SHIFT;
MODE_SHIFT为什么是30,我们第四节提过,int类型的位数是32位,32-30=2位, 2位可以表示四个数字,正好就是对应了这四个常量,0 1 2 3 。MODE_MASK,UNSPECIFIED,EXACTLY,AT_MOST,之所以是 0 1 2 3 右移30位

其实MeasureSpec类这么写,就是想用一个int类型,来同时存储测量模式(最高两位,2的2次方=4种信息,3种测量模式,和一个帮助值),和测量值(低30位,测量值可最大取2的30次方-1)。

为了验证我们的结论,我们顺着往下看源码,五个常量定以后,就是makeMeasureSpec方法:

/** * Creates a measure specification based on the supplied size and mode. * * The mode must always be one of the following: * <ul> *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li> *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li> *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li> * </ul> * * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's * implementation was such that the order of arguments did not matter * and overflow in either value could impact the resulting MeasureSpec. * {@link android.widget.RelativeLayout} was affected by this bug. * Apps targeting API levels greater than 17 will get the fixed, more strict * behavior.</p> * * @param size the size of the measure specification * @param mode the mode of the measure specification * @return the measure specification based on size and mode */public static int makeMeasureSpec(int size, int mode) {    if (sUseBrokenMakeMeasureSpec) {        return size + mode;    } else {        return (size & ~MODE_MASK) | (mode & MODE_MASK);    }}

这个方法根据传入的测量大小size 和 测量模式mode,来生成MeasureSpec值。

mode必须是UNSPECIFIED EXACLTY AT_MOST三个常量之一,

这里注释也提到,API17以下 这个方法就是这样实现的,传入参数的顺序(不对?)会导致结果的溢出。RelativeLayout就受到此bug的影响,API17以上,修正了这个bug,它会返回更严格的行为。啥意思?看看代码能不能告诉我们答案。

/** * Use the old (broken) way of building MeasureSpecs. */private static boolean sUseBrokenMakeMeasureSpec = false;
// Older apps may need this compatibility hack for measurement.sUseBrokenMakeMeasureSpec = targetSdkVersion <= JELLY_BEAN_MR1;
这个变量默认是false,在View的初始化函数里 判断,如果版本17及以下,就是true,

如果是true,makeMeasureSpec就直接把size +mode 作为返回结果了。为什么它敢直接+,这么任性!我还以为它会经过复杂的计算返回给我们MeasureSpec值呢,初看源码的话,肯定会有这种想法。

不过这正印证了我们的结论,由于MeasureSpec用高两位存储测量模式,低30位存储测量值,对于size来说,它的高两位是00,对于mode来说,它的低30位全是0,所以它们相加彼此互不冲突,正好可以用一个int来表示两种相关的信息。

不过如果这么粗暴简单的直接相加,的确称不上严格(strict), 虽然在理想状况下(mode 只有高两位有值,低30位都为0, size只有低30位有值,高位全为0),不会出错。但是如果单方面有异常发生,它会导致mode 和 size的值都混乱

例如:

如果size传了个超过30位的的值(假设是31位,01xx xxxx xxxx........),但是mode的值是正确的(为EXACLTY:0100 0000 0.....),按照API17以下的方法,在两者直接相加得到的MeasureSpec里,size由于溢出30位,其值就是剩下的xxxxxx,但是mode由于存储在高2位,它的值也将受到影响,01+01 = 10 ,测量mode将成为AT_MOST的值。


在API大于17的情况下,它返回的值是 return (size & ~MODE_MASK) | (mode & MODE_MASK);

MODE_MASK是高两位为1,其余30位全是0的数,即  11 00 0000 0000 .......

~MODE_MASK,对其取非,则为 高两位为0,其余30全为1的数,即00 11 1111 1111 ......

所以正常情况下,size高两位为0,低30位为测量值, &~MODE_MASK后,不会受到任何影响。而异常情况下,高两位可能不为0,不过就算如此,&~MODE_MASK 后,其高两位一定是00,后30代表测量值,这样就不会发生上例里提到的影响mode的情况。

同理 mode 如果出现异常,经过 &MODE_MASK的位运算后,其后30位一定是0,它的异常也不会波及到size里。

经过&的处理,此时 使用 + 或者 | 都是一样的效果了。只是将mode 和 size合并至一个int里。

这样至少保证,mode size单方面某一个值出错,不会影响到另一个值。

makeMeasureSpec()方法看完了,紧随其后的是makeSafeMeasureSpec()方法。

/** * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED * will automatically get a size of 0. Older apps expect this. * * @hide internal use only for compatibility with system widgets and older apps */public static int makeSafeMeasureSpec(int size, int mode) {    if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {        return 0;    }    return makeMeasureSpec(size, mode);}
这是一个hide的方法,这个方法 和makeMeasureSpec相似,但是如果测量模式是UNSPECIFIED,它会直接返回size为0,它只是为了兼容系统部件和旧的app,内部使用的。查看了一下变量的定义,和赋值,这应该是6.0新增的方法:

/** * Always return a size of 0 for MeasureSpec values with a mode of UNSPECIFIED */static boolean sUseZeroUnspecifiedMeasureSpec = false;
// In M and newer, our widgets can pass a "hint" value in the size// for UNSPECIFIED MeasureSpecs. This lets child views of scrolling containers// know what the expected parent size is going to be, so e.g. list items can size// themselves at 1/3 the size of their container. It breaks older apps though,// specifically apps that use some popular open source libraries.sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M;
我们平时使用还是直接调用MeasureSpec.makeMeasureSpec();就好。

经过上面一番洗礼,现在再看getMode()和getSize()这双子星兄弟就简单多了。

/** * Extracts the mode from the supplied measure specification. * * @param measureSpec the measure specification to extract the mode from * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, *         {@link android.view.View.MeasureSpec#AT_MOST} or *         {@link android.view.View.MeasureSpec#EXACTLY} */public static int getMode(int measureSpec) {    return (measureSpec & MODE_MASK);}/** * Extracts the size from the supplied measure specification. * * @param measureSpec the measure specification to extract the size from * @return the size in pixels defined in the supplied measure specification */public static int getSize(int measureSpec) {    return (measureSpec & ~MODE_MASK);}
getMode():通过 位与运算,剥掉MeasureSpec的低30位,将MeasureSpec的低30位全部置0,留下高两位的值,即mode值,

getSize():通过位与运算,砍掉MeasureSpec的高两位,将其高两位全部置0,留下低30位的值,正是size的值。


该类还剩最后两个方法没有分析,一个是toString(),忽略~

另外一个就是 adjust()方法,该方法没有注释~在此鄙视一下Google大神,哈哈,不过它是一个

该方法传入MeasureSpec 和一个delta偏移量,给MeasureSpec的size追加上这个delta偏移量,并调用makeMeasureSpec()方法返回MeasureSpec。不过如果是UNSPECIFIED类型,就不调整size。如果调整size后,size小于0,会修正到0。

看到这个方法,我觉得我好像知道了,makeMeasureSpec()方法在API17以后修正了算法的意义,因为在调用adjust方法时,如果传入的delta过大,是会导致size+delta超出30位的,这个时候老的makeMeasureSpec()方法,不仅size值错了,连mode也会被殃及池鱼。

static int adjust(int measureSpec, int delta) {    final int mode = getMode(measureSpec);    int size = getSize(measureSpec);    if (mode == UNSPECIFIED) {        // No need to adjust size for UNSPECIFIED mode.        return makeMeasureSpec(size, UNSPECIFIED);    }    size += delta;    if (size < 0) {        Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +                ") spec: " + toString(measureSpec) + " delta: " + delta);        size = 0;    }    return makeMeasureSpec(size, mode);}
不过该方法是protected类型的,我们平时也用不到~。

随便搜了一下,View只在measure()方法里调用了它,

===================================================================

【7 总结】

一个bug引发的血案,求知欲驱使着我从Android源码一路看到java基础知识 计算机组成原理,顺着一路走下来,基本功更加扎实了。

同事昨天问我,你看源码补码的这个有啥用啊, 有啥意义啊,

我想说的是,它不会短时间让我飞多快多高,但它能决定我最终能飞多快多高。

大家一起努力吧!

====================================================================

【8 补充】

我个人觉得 位运算和正则表达式有些相似,所以把收集的一些位运算的用法发出来,

原谅我原文地址忘了,sorry。

1.  判断int型变量a是奇数还是偶数    
     a&1  = 0 偶数 
     a&1 =  1 奇数 
2.  求平均值,比如有两个int类型变量x、y,首先要求x+y的和,再除以2,但是有可能x+y的结果会超过int的最大表示范围,所以位运算就派上用场啦。
      (x&y)+((x^y)>>1); 
3.  对于一个大于0的整数,判断它是不是2的几次方
    ((x&(x-1))==0)&&(x!=0); 
4.  比如有两个int类型变量x、y,要求两者数字交换,位运算的实现方法:性能绝对高效
    x ^= y; 
    y ^= x; 
    x ^= y; 
5. 求绝对值
    int abs( int x ) 
   { 
     int y ; 
     y = x >> 31 ; 
    return (x^y)-y ;        //or: (x+y)^y 
   }
6.  取模运算,采用位运算实现:
     a % (2^n) 等价于 a & (2^n - 1) 
7.  乘法运算   采用位运算实现
     a * (2^n) 等价于 a << n
8.   除法运算转化成位运算
      a / (2^n) 等价于 a>> n 
9.   求相反数
      (~x+1) 
10  a % 2 等价于 a & 1 


6 0