Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 【Android源碼閱讀系列一】一個bug引發的血案:閱讀Android源碼 MeasureSpec類(API版本:23)

【Android源碼閱讀系列一】一個bug引發的血案:閱讀Android源碼 MeasureSpec類(API版本:23)

編輯:關於Android編程

【1 引言】

本文來源於一個bug,後來越走越遠跑偏了,從LinearLayouy-》View 》-MeasureSpec-》位運算-》計算機的編碼(原碼反碼補碼)這已經到計算機組成原理了~~於是權當做一次筆記記錄:

起源的bug:

使用流式布局時,布局內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()】

 

@Override
protected 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:
 * 
  • *
  • {@link android.view.View.MeasureSpec#UNSPECIFIED}
  • *
  • {@link android.view.View.MeasureSpec#EXACTLY}
  • *
  • {@link android.view.View.MeasureSpec#AT_MOST}
  • *
* *

Note: 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.

* * @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
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved