View的工作原理之Measure過程原始碼學習(二)
上一篇文章從Android程式啟動過程講解了Activity、PhoneWindow以及ViewRoot與DecorView的聯絡。本篇文章詳細講述一下DecorView的measure過程。
在瞭解measure過程過程之前需要先了解MeasureSpec這個類, MeasureSpec是一個32位的int值,高2位表示SpecMode(測量模式),低30位表示SpecSize(測量大小)。MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的物件記憶體分配,為方便操作,提供了打包和解包方法。MeasureSpec很大程度上決定一個View的尺寸規格,之所以說是很大程度而不是完全決定,是因為View的measure過程還受到父容器的影響。在測量過程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec來測量View的寬高。MeasureSpec部分原始碼如下所示。
//View.java 24515行 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; public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }
如上程式碼,SpecMode有三種不同的值,分別是UNSPECIFIED,EXACTLY ,AT_MOST 。這裡特別指出EXACTLY的值是數字1左移30位結果1073741824,AT_MOST的值數字2左移30位結果是-2147483648。為啥特別指出這兩個值,在後面程式碼會講到。在UNSPECIFIED這種模式下,父容器不限制View的大小,要多大給多大,但這種情況一般使用者系統內部,對於我們的開發工作幾乎使用不到,所以不需深入瞭解。EXACTLY模式是精確模式,在這種模式下,父容器檢測出View的精確大小,View的最終大小就是SpecSize的值。AT_MOST模式,在這種模式下,父容器給定一個SpecSize的值,View的大小不能大於這個值,具體多大需要看View的具體實現。
瞭解完MeasureSpec之後,我們開始進入DecorView的measure過程。前一篇文章說到:在ViewRootImpl中有一個方法performTraversals,在這個方法中,DecorView進行了measure,layout,draw三個過程,分別對應performMeasure,performLayout和performDraw三個方法。在performTraversals方法中呼叫方法performMeasure,這個方法的程式碼如下
//ViewRootImpl.java 2404行
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);
}
}
可以看到,它實際上是呼叫了mView的measure方法,前一篇文章也說明了,這個mView就是DecorView。因為measure方法是final型別,所以最終呼叫的是View中的measure方法,而View中的measure方法會呼叫實現類的onMeasure中的方法,也就是DecorView的onMeasure。所以DecorView的onMeasure方法獲得的引數就是performMeasure方法的引數。這個引數是什麼呢?來看下面程式碼。
//Build.java 576行
public static final int JELLY_BEAN_MR1 = 17;
//View.java 4545行
sUseBrokenMakeMeasureSpec = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
//View.java 24563行
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {//判斷SDK版本是否17以下
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
先來看makeMeasureSpec方法的實現,這裡是將測量模式mode和測量大小size結合起來組成一個32位的int型數值。這裡需要注意,測量模式mode都是左移了30位的,具體參考上文的MeasureSpec部分。方法對SDK版本是否在17上下做了區分,原因我看了一下注釋,好像是因為SDK17以下使用兩個值相加的話,會使RelativeLayout在滾動佈局中(ScrollView或者HorizontalScrollView)出現錯誤。因為SDK17以上,使用了位運算來獲得MeasureSpec。
//ViewRootImpl.java 375行
final Rect mWinFrame; // frame given by window manager.
//ViewRootImpl.java 1628行
Rect frame = mWinFrame;
//ViewRootImpl.java 2070行
// !!FIXME!! This next section handles the case where we did not get the
// window size we asked for. We should avoid this by getting a maximum size from
// the window session beforehand.
if (mWidth != frame.width() || mHeight != frame.height()) {
mWidth = frame.width();
mHeight = frame.height();
}
//ViewRootImpl.java 1594行
WindowManager.LayoutParams lp = mWindowAttributes;
//ViewRootImpl.java 2145行
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//ViewRootImpl.java 2155行
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
上方的程式碼,performMeasure方法的引數來自getRootMeasureSpec,getRootMeasureSpec方法的引數,我已經在上方程式碼中,展示了他們的來源,mWidth和mHeight是mWinFrame的寬高,結合註釋可以知道,這也就是視窗的大小了。lp的話,沒有翻到最終來源,但是也很容易猜出,這應該是給予DecorView的佈局約束。由此,DecorView的測量規格值,是有視窗大小和系統給予它的佈局約束WindowManager.LayoutParams來共同決定了。繼續看getRootMeasureSpec方法的原始碼。
//ViewRootImpl.java 2633行
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;
}
在getRootMeasureSpec方法中,很直觀的可以看到,它是根據系統給到DecorView的佈局約束WindowManager.LayoutParams的值來確定DecorView的測量規格值MeasureSpec的。這裡需要額外記住一點的是ViewGroup.LayoutParams.MATCH_PARENT的值是-1,ViewGroup.LayoutParams.WRAP_CONTENT的值是-2。
- 如果佈局約束LayoutParams中,width或者height的值是LayoutParams.MATCH_PARENT,那麼測量模式就是EXACTLY,精確模式,大小是視窗的寬度或高度;
- 如果佈局約束LayoutParams中,width或者height的值是LayoutParams.WRAP_CONTENT,那麼測量模式就是AT_MOST,最大模式,大小是視窗的寬度或高度;
- 如果佈局約束LayoutParams中,width或者height的值是具體的數值,那麼測量模式就是EXACTLY,精確模式,寬度或高度大小給定的具體大小;
至此,DecorView的onMeasure方法引數的來源就清楚了,其實就是DecorView的寬高的測量規格值。檢視DecorView的onMeasure方法原始碼得知,因為DecorView和子元素之間存在一些需要處理的間距,所以真正傳遞給子元素的測量規格值需要進行處理,這就是onMeasure方法的功能。DecorView的onMeasure方法還呼叫super.onMeasure(widthMeasureSpec, heightMeasureSpec);。因為DecorView繼承自FrameLayout,所以這裡將呼叫FrameLayout的onMeasure方法,在這個方法中,會獲取子元素的個數,然後遍歷子元素的measure過程,這樣就把measure過程從DecorView傳到了子元素去。而子元素的measure過程,如果這個子元素不是ViewGroup,而是普通的View,那麼就執行自己的measure過程就行了,如果是ViewGroup的話,還需要像FrameLayout一樣,迴圈呼叫子元素的measure過程,如此一級一級往下,所有View的measure過程都會被執行。這個在下篇文章中將會詳細講解。
DecorView的Measure過程到這就完成了。