ViewRootImpl在其創建過程中通過requestLayout()向主線程發送了一條觸發“遍歷”操作的消息,“遍歷”操作是指performTraversals()方法。它的性質與WMS中的performLayoutAndPlaceSurfacesLocked()類似,是一個包羅萬象的方法。ViewRootImpl中接收到的各種變化,如來自WMS的窗口屬性變化,來自控件樹的尺寸變化、重繪請求等都引發performTraversals()的調用,并在其中完成處理。View類及其子類中的onMeasure()、onLayout()以及onDraw()等回調也都是在performTraversals()的執行過程中直接或間接地引發。也正是如此,一次次的performTraversals()調用驅動著控件樹有條不紊地工作著,一旦此方法無法正常執行,整個控件樹都將處于僵死狀態。因此,performTraversals()函數可謂是ViewRootImpl的心跳。
由于布局的相關工作是此方法中最主要的內容,為了簡化分析,并突出此方法的工作流程,本節將以布局的相關工作為主線進行探討。待完成了這部分內容的分析之后,龐大的performTraversals()方法將不再那么難以馴服,讀者便可以輕易地學習其他的工作了。
#### 1.performTraversals()的工作階段
performTraversals()是Android 源碼中最龐大的方法之一,因此在正式探討它的實現之前最好先將其劃分為以下幾個工作階段作為指導。
- 預測量階段。這是進入performTraversals()方法后的第一個階段,它會對控件樹進行第一次測量。測量結果可以通過mView. getMeasuredWidth()/Height()獲得。在此階段中將會計算出控件樹為顯示其內容所需的尺寸,即期望的窗口尺寸。在這個階段中,View及其子類的onMeasure()方法將會沿著控件樹依次得到回調。
- 布局窗口階段。根據預測量的結果,通過IWindowSession.relayout()方法向WMS請求調整窗口的尺寸等屬性,這將引發WMS對窗口進行重新布局,并將布局結果返回給ViewRootImpl。
- 最終測量階段。預測量的結果是控件樹所期望的窗口尺寸。然而由于在WMS中影響窗口布局的因素很多(參考第4章),WMS不一定會將窗口準確地布局為控件樹所要求的尺寸,而迫于WMS作為系統服務的強勢地位,控件樹不得不接受WMS的布局結果。因此在這一階段,performTraversals()將以窗口的實際尺寸對控件進行最終測量。在這個階段中,View及其子類的onMeasure()方法將會沿著控件樹依次被回調。
- 布局控件樹階段。完成最終測量之后便可以對控件樹進行布局了。測量確定的是控件的尺寸,而布局則是確定控件的位置。在這個階段中,View及其子類的onLayout()方法將會被回調。
- 繪制階段。這是performTraversals()的最終階段。確定了控件的位置與尺寸后,便可以對控件樹進行繪制了。在這個階段中,View及其子類的onDraw()方法將會被回調。
說明 很多文章都傾向于將performTraversals()的工作劃分為測量、布局與繪制三個階段。然而筆者認為如此劃分隱藏了WMS在這個過程中的地位,并且沒能體現出控件樹對窗口尺寸的期望、WMS對窗口尺寸做最終的確定,最后以WMS給出的結果為準再次進行測量的協商過程。而這個協商過程充分體現了ViewRootImpl作為WMS與控件樹的中間人的角色。
接下來將結合代碼,對上述五個階段進行深入的分析。
#### 2.預測量與測量原理
本節將探討performTraversals()將以何種方式對控件樹進行預測量,同時,本節也會對控件的測量過程與原理進行介紹。
##### 預測量參數的候選
預測量也是一次完整的測量過程,它與最終測量的區別僅在于參數不同而已。實際的測量工作在View或其子類的onMeasure()方法中完成,并且其測量結果需要受限于來自其父控件的指示。這個指示由onMeasure()方法的兩個參數進行傳達:widthSpec與heightSpec。它們是被稱為MeasureSpec的復合整型變量,用于指導控件對自身進行測量。它有兩個分量,結構如圖6-5所示。
:-: 
圖 6 - 5 MeasureSpec的結構
其1到30位給出了父控件建議尺寸。建議尺寸對測量結果的影響依不同的SPEC\_MODE的不同而不同。SPEC\_MODE的取值取決于此控件的LayoutParams.width/height的設置,可以是如下三種值之一。
- MeasureSpec.UNSPECIFIED (0):表示控件在進行測量時,可以無視SPEC\_SIZE的值。控件可以是它所期望的任意尺寸。
- MeasureSpec.EXACTLY (1):表示子控件必須為SPEC\_SIZE所制定的尺寸。當控件的LayoutParams.width/height為一確定值,或者是MATCH\_PARENT時,對應的MeasureSpec參數會使用這個SPEC\_MODE。
- MeasureSpec.AT\_MOST (2):表示子控件可以是它所期望的尺寸,但是不得大于SPEC\_SIZE。當控件的LayoutParams.width/height為WRAP\_CONTENT時,對應的MeasureSpec參數會使用這個SPEC\_MODE。
Android提供了一個MeasureSpec類用于組合兩個分量成為一個MeasureSpec,或者從MeasureSpec中分離任何一個分量。
那么ViewRootImpl會如何為控件樹的根mView準備其MeasureSpec呢?
參考如下代碼,注意desiredWindowWidth/Height的取值,它們將是SPEC\_SIZE分量的候選。另外,這段代碼分析中也解釋了與測量無關,但是比較重要的代碼段。
**ViewRootImpl.java-->ViewRootImpl.performTraversals()**
```
private void performTraversals() {
// 將mView保存在局部變量host中,以此提高對mView的訪問效率
finalView host = mView;
......
// 聲明本階段的主角,這兩個變量將是mView的SPEC_SIZE分量的候選
intdesiredWindowWidth;
intdesiredWindowHeight;
.......
Rectframe = mWinFrame; // 如上一節所述,mWinFrame表示了窗口的最新尺寸
if(mFirst) {
/*mFirst表示了這是第一次遍歷,此時窗口剛剛被添加到WMS,此時窗口尚未進行relayout,因此
mWinFrame中沒有存儲有效地窗口尺寸 */
if(lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) {
......// 為狀態欄設置desiredWindowWidth/Height,其取值是屏幕尺寸
}else {
//① 第一次“遍歷”的測量,采用了應用可以使用的最大尺寸作為SPEC_SIZE的候選
DisplayMetrics packageMetrics =
mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
/* 由于這是第一次進行“遍歷”,控件樹即將第一次被顯示在窗口上,因此接下來的代碼填充了
mAttachInfo中的一些字段,然后通過mView發起了dispatchAttachedToWindow()的調用
之后每一個位于控件樹中的控件都會回調onAttachedToWindow() */
......
} else {
// ② 在非第一次遍歷的情況下,會采用窗口的最新尺寸作為SPEC_SIZE的候選
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
/* 如果窗口的最新尺寸與ViewRootImpl中的現有尺寸不同,說明WMS側單方面改變了窗口的尺寸
這將產生如下三個結果 */
if(desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
// 需要進行完整的重繪以適應新的窗口尺寸
mFullRedrawNeeded = true;
// 需要對控件樹進行重新布局
mLayoutRequested = true;
/* 控件樹有可能拒絕接受新的窗口尺寸,比如在隨后的預測量中給出了不同于窗口尺寸的測量結果
產生這種情況時,就需要在窗口布局階段嘗試設置新的窗口尺寸 */
windowSizeMayChange = true;
}
}
......
/* 執行位于RunQueue中的回調。RunQueue是ViewRootImpl的一個靜態成員,即是說它是進程唯一
的,并且可以在進程的任何位置訪問RunQueue。在進行多線程任務時,開發者可以通過調用View.post()
或View.postDelayed()方法將一個Runnable對象發送到主線程執行。這兩個方法的原理是將
Runnable對象發送到ViewRootImpl的mHandler去。當控件已經加入到控件樹時,可以通過
AttachInfo輕易獲取這個Handler。而當控件沒有位于控件樹中時,則沒有mAttachInfo可用,此時
執行View.post()/PostDelay()方法,Runnable將會被添加到這個RunQueue隊列中。
在這里,ViewRootImpl將會把RunQueue中的Runnable發送到mHandler中,進而得到執行。所以
無論控件是否顯示在控件樹中,View.post()/postDelay()方法都是可用的,除非當前進程中沒有任何
處于活動狀態的ViewRootImpl */
getRunQueue().executeActions(attachInfo.mHandler);
booleanlayoutRequested = mLayoutRequested && !mStopped;
/* 僅當layoutRequested為true時才進行預測量。
layoutRequested為true表示在進行“遍歷”之前requestLayout()方法被調用過。
requestLayout()方法用于要求ViewRootImpl進行一次“遍歷”并對控件樹重新進行測量與布局 */
if(layoutRequested) {
final Resources res = mView.getContext().getResources();
if(mFirst) {
......// 確定控件樹是否需要進入TouchMode,本章將在6.5.1節介紹 TouchMode
}else {
/*檢查WMS是否單方面改變了ContentInsets與VisibleInsets。注意對二者的處理的差異,
ContentInsets描述了控件在布局時必須預留的空間,這樣會影響控件樹的布局,因此將
insetsChanged標記為true,以此作為是否進行控件布局的條件之一。而VisibleInsets則
描述了被遮擋的空間,ViewRootImpl在進行繪制時,需要調整繪制位置以保證關鍵控件或區域,
如正在進行輸入的TextView等不被遮擋,這樣VisibleInsets的變化并不會導致重新布局,
所以這里僅僅是將VisibleInsets保存到mAttachInfo中,以便繪制時使用 */
if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) {
insetsChanged = true;
}
if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) {
mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
}
/*當窗口的width或height被指定為WRAP_CONTENT時,表示這是一個懸浮窗口。
此時會對desiredWindowWidth/Height進行調整。在前面的代碼中,這兩個值被設置
被設置為窗口的當前尺寸。而根據MeasureSpec的要求,測量結果不得大于SPEC_SIZE。
然而,如果這個懸浮窗口需要更大的尺寸以完整顯示其內容時,例如為AlertDialog設置了
一個更長的消息內容,如此取值將導致無法得到足夠大的測量結果,從而導致內容無法完整顯示。
因此,對于此等類型的窗口,ViewRootImpl會調整desiredWindowWidth/Height為此應用
可以使用的最大尺寸 */
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 懸浮窗口的尺寸取決于測量結果。因此有可能需要向WMS申請改變窗口的尺寸。
windowSizeMayChange = true;
if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL) {
//
} else {
// ③ 設置懸浮窗口SPEC_SIZE的候選為應用可以使用的最大尺寸
DisplayMetrics packageMetrics = res.getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
}
}
// **④ 進行預測量。**通過measureHierarchy()方法以desiredWindowWidth/Height進行測量
windowSizeMayChange |=measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
// 其他階段的處理
......
}
```
由此可知,預測量時的SPEC\_SIZE按照如下原則進行取值:
- 第一次“遍歷”時,使用應用可用的最大尺寸作為SPEC\_SIZE的候選。
- 此窗口是一個懸浮窗口,即LayoutParams.width/height其中之一被指定為WRAP\_CONTENT時,使用應用可用的最大尺寸作為SPEC\_SIZE的候選。
- 在其他情況下,使用窗口最新尺寸作為SPEC\_SIZE的候選。
最后,通過measureHierarchy()方法進行測量。
##### 測量協商
measureHierarchy()用于測量整個控件樹。傳入的參數desiredWindowWidth與desiredWindowHeight在前述代碼中根據不同的情況作了精心的挑選。控件樹本可以按照這兩個參數完成測量,但是measureHierarchy()有自己的考量,即如何將窗口布局地盡可能地優雅。
這是針對將LayoutParams.width設置為了WRAP\_CONTENT的懸浮窗口而言。如前文所述,在設置為WRAP\_CONTENT時,指定的desiredWindowWidth是應用可用的最大寬度,如此可能會產生如圖6-6左圖所示的丑陋布局。這種情況較容易發生在AlertDialog中,當AlertDialog需要顯示一條比較長的消息時,由于給予的寬度足夠大,因此它有可能將這條消息以一行顯示,并使得其窗口充滿了整個屏幕寬度,在橫屏模式下這種布局尤為丑陋。
倘若能夠對可用寬度進行適當的限制,迫使AlertDialog將消息換行顯示,則產生的布局結果將會優雅得多,如圖6-6右圖所示。但是,倘若不分清紅皂白地對寬度進行限制,當控件樹真正需要足夠的橫向空間時,會導致內容無法顯示完全,或者無法達到最佳的顯示效果。例如當一個懸浮窗口希望盡可能大地顯示一張照片時就會出現這樣的情況。
:-: 
圖 6 - 6 丑陋的布局與優雅的布局
那么measureHierarchy()如何解決這個問呢?它采取了與控件樹進行協商的辦法,即先使用measureHierarchy()所期望的寬度限制嘗試對控件樹進行測量,然后通過測量結果來檢查控件樹是否能夠在此限制下滿足其充分顯示內容的要求。倘若沒能滿足,則measureHierarchy()進行讓步,放寬對寬度的限制,然后再次進行測量,再做檢查。倘若仍不能滿足則再度進行讓步。
參考代碼如下:
**ViewRootImpl.java-->ViewRootImpl.measureHierarchy()**
```
private boolean measureHierarchy(final View host,final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth,
final int desiredWindowHeight) {
intchildWidthMeasureSpec; // 合成后的用于描述寬度的MeasureSpec
intchildHeightMeasureSpec; // 合成后的用于描述高度的MeasureSpec
booleanwindowSizeMayChange = false; // 表示測量結果是否可能導致窗口的尺寸發生變化
booleangoodMeasure = false; // goodMeasure表示了測量是否能滿足控件樹充分顯示內容的要求
// 測量協商僅發生在LayoutParams.width被指定為WRAP_CONTENT的情況下
if(lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
/* **① 第一次協商。**measureHierarchy()使用它最期望的寬度限制進行測量。這一寬度限制定義為
一個系統資源。可以在frameworks/base/core/res/res/values/config.xml找到它的定義 */
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth,mTmpValue, true);
intbaseSize = 0;
// 寬度限制被存放在baseSize中
if(mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
if(baseSize != 0 && desiredWindowWidth > baseSize) {
// 使用getRootMeasureSpec()函數組合SPEC_MODE與SPEC_SIZE為一個MeasureSpec
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec =
getRootMeasureSpec(desiredWindowHeight,lp.height);
//**②第一次測量。**由performMeasure()方法完成
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
/* 控件樹的測量結果可以通過mView的getmeasuredWidthAndState()方法獲取。如果
控件樹對這個測量結果不滿意,則會在返回值中添加MEASURED_STATE_TOO_SMALL位 */
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL)
==0) {
goodMeasure = true; // 控件樹對測量結果滿意,測量完成
} else {
// **③ 第二次協商。**上次測量結果表明控件樹認為measureHierarchy()給予的寬度太小,
在此適當地放寬對寬度的限制,使用最大寬度與期望寬度的中間值作為寬度限制 */
baseSize = (baseSize+desiredWindowWidth)/2;
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
// **④ 第二次測量**
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 再次檢查控件樹是否滿足此次測量
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL)
== 0) {
goodMeasure = true; // 控件樹對測量結果滿意,測量完成
}
}
}
}
if(!goodMeasure) {
/* **⑤ 最終測量。**當控件樹對上述兩次協商的結果都不滿意時,measureHierarchy()放棄所有限制
做最終測量。這一次將不再檢查控件樹是否滿意了,因為即便其不滿意,measurehierarchy()也沒
有更多的空間供其使用了 */
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
/* 最后,如果測量結果與ViewRootImpl中當前的窗口尺寸不一致,則表明隨后可能有必要進行窗口
尺寸的調整 */
if(mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
{
windowSizeMayChange = true;
}
}
// 返回窗口尺寸是否可能需要發生變化
returnwindowSizeMayChange;
}
```
顯然,對于非懸浮窗口,即當LayoutParams.width被設置為MATCH\_PARENT時,不存在協商過程,直接使用給定的desiredWindowWidth/Height進行測量即可。而對于懸浮窗口,measureHierarchy()可以連續進行兩次讓步。因而在最不利的情況下,在ViewRootImpl的一次“遍歷”中,控件樹需要進行三次測量,即控件樹中的每一個View.onMeasure()會被連續調用三次之多,如圖6-7所示。所以相對于onLayout(),onMeasure()方法的對性能的影響比較大。
:-: 
圖 6 - 7 協商測量的三次嘗試
接下來通過performMeasure()看控件樹如何進行測量。
##### 測量原理
performMeasure()方法的實現非常簡單,它直接調用mView.measure()方法,將measureHierarchy()給予的widthSpec與heightSpec交給mView。
看下View.measure()方法的實現:
**View.java-->View.measure()**
```
public final void measure(int widthMeasureSpec,int heightMeasureSpec) {
/* 僅當給予的MeasureSpec發生變化,或要求強制重新布局時,才會進行測量。
所謂強制重新布局,是指當控件樹中的一個子控件的內容發生變化時,需要進行重新的測量和布局的情況
在這種情況下,這個子控件的父控件(以及其父控件的父控件)所提供的MeasureSpec必定與上次測量
時的值相同,因而導致從ViewRootImpl到這個控件的路徑上的父控件的measure()方法無法得到執行
進而導致子控件無法重新測量其尺寸或布局。因此,當子控件因內容發生變化時,從子控件沿著控件樹回溯
到ViewRootImpl,并依次調用沿途父控件的requestLayout()方法,在這個方法中,會在
mPrivateFlags中加入標記PFLAG_FORCE_LAYOUT,從而使得這些父控件的measure()方法得以順利
執行,進而這個子控件有機會進行重新測量與布局。這便是強制重新布局的意義 */
if ((mPrivateFlags& PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
/* **① 準備工作。**從mPrivateFlags中將PFLAG_MEASURED_DIMENSION_SET標記去除。
PFLAG_MEASURED_DIMENSION_SET標記用于檢查控件在onMeasure()方法中是否通過
調用setMeasuredDimension()將測量結果存儲下來 */
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
......
/* **② 對本控件進行測量** 每個View子類都需要重載這個方法以便正確地對自身進行測量。
View類的onMeasure()方法僅僅根據背景Drawable或style中設置的最小尺寸作為
測量結果*/
onMeasure(widthMeasureSpec, heightMeasureSpec);
/* ③ 檢查onMeasure()的實現是否調用了setMeasuredDimension()
setMeasuredDimension()會將PFLAG_MEASURED_DIMENSION_SET標記重新加入
mPrivateFlags中。之所以做這樣的檢查,是由于onMeasure()的實現可能由開發者完成,
而在Android看來,開發者是不可信的 */
if((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET)
!=PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException(......);
}
// ④ 將PFLAG_LAYOUT_REQUIRED標記加入mPrivateFlags。這一操作會對隨后的布局操作放行
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
// 記錄父控件給予的MeasureSpec,用以檢查之后的測量操作是否有必要進行
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
```
從這段代碼可以看出,View.measure()方法沒有實現任何測量算法,它的作用在于引發onMeasure()的調用,并對onMeasure()行為的正確性進行檢查。另外,在控件系統看來,一旦控件執行了測量操作,那么隨后必須進行布局操作,因此在完成測量之后,將PFLAG\_LAYOUT\_REQUIRED標記加入mPrivateFlags,以便View.layout()方法可以順利進行。
onMeasure()的結果通過setMeasuredDimension()方法盡行保存。setMeasuredDimension()方法的實現如下:
**View.java-->View.setMeasuredDimension()**
```
protected final void setMeasuredDimension(intmeasuredWidth, int measuredHeight) {
/* ① 測量結果被分別保存在成員變量mMeasuredWidth與mMeasuredHeight中
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
// ② 向mPrivateFlags中添加PFALG_MEASURED_DIMENSION_SET,以此證明onMeasure()保存了測量結果
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
```
其實現再簡單不過。存儲測量結果的兩個變量可以通過getMeasuredWidthAndState()與getMeasuredHeightAndState()兩個方法獲得,就像ViewRootImpl.measureHierarchy()中所做的一樣。此方法雖然簡單,但需要注意,與MeasureSpec類似,測量結果不僅僅是一個尺寸,而是一個測量狀態與尺寸的復合整、變量。其0至30位表示了測量結果的尺寸,而31、32位則表示了控件對測量結果是否滿意,即父控件給予的MeasureSpec是否可以使得控件完整地顯示其內容。當控件對測量結果滿意時,直接將尺寸傳遞給setMeasuredDimension()即可,注意要保證31、32位為0。倘若對測量結果不滿意,則使用View.MEASURED\_STATE\_TOO\_SMALL | measuredSize 作為參數傳遞給setMeasuredDimension()以告知父控件對MeasureSpec進行可能的調整。
既然明白了onMeasure()的調用如何發起,以及它如何將測量結果告知父控件,那么onMeasure()方法應當如何實現的呢?對于非ViewGroup的控件來說其實現相對簡單,只要按照MeasureSpec的原則如實計算其所需的尺寸即可。而對于ViewGroup類型的控件來說情況則復雜得多,因為它不僅擁有自身需要顯示的內容(如背景),它的子控件也是其需要測量的內容。因此它不僅需要計算自身顯示內容所需的尺寸,還有考慮其一系列子控件的測量結果。為此它必須為每一個子控件準備MeasureSpec,并調用每一個子控件的measure()函數。
由于各種控件所實現的效果形形色色,開發者還可以根據需求自行開發新的控件,因此onMeasure()中的測量算法也會變化萬千。不從Android系統實現的角度仍能得到如下的onMeasure()算法的一些實現原則:
- 控件在進行測量時,控件需要將它的Padding尺寸計算在內,因為Padding是其尺寸的一部分。
- ViewGroup在進行測量時,需要將子控件的Margin尺寸計算在內。因為子控件的Margin尺寸是父控件尺寸的一部分。
- ViewGroup為子控件準備MeasureSpec時,SPEC\_MODE應取決于子控件的LayoutParams.width/height的取值。取值為MATCH\_PARENT或一個確定的尺寸時應為EXACTLY,WRAP\_CONTENT時應為AT\_MOST。至于SPEC\_SIZE,應理解為ViewGroup對子控件尺寸的限制,即ViewGroup按照其實現意圖所允許子控件獲得的最大尺寸。并且需要扣除子控件的Margin尺寸。
- 雖然說測量的目的在于確定尺寸,與位置無關。但是子控件的位置是ViewGroup進行測量時必須要首先考慮的。因為子控件的位置即決定了子控件可用的剩余尺寸,也決定了父控件的尺寸(當父控件的LayoutParams.width/height為WRAP\_CONTENT時)。
- 在測量結果中添加MEASURED\_STATE\_TOO\_SMALL需要做到實事求是。當一個方向上的空間不足以顯示其內容時應考慮利用另一個方向上的空間,例如對文字進行換行處理,因為添加這個標記有可能導致父控件對其進行重新測量從而降低效率。
- 當子控件的測量結果中包含MEASURED\_STATE\_TOO\_SMALL標記時,只要有可能,父控件就應當調整給予子控件的MeasureSpec,并進行重新測量。倘若沒有調整的余地,父控件也應當將MEASURED\_STATE\_TOO\_SMALL加入到自己的測量結果中,讓它的父控件嘗試進行調整。
- ViewGroup在測量子控件時必須調用子控件的measure()方法,而不能直接調用其onMeasure()方法。直接調用onMeasure()方法的最嚴重后果是子控件的PFLAG\_LAYOUT\_REQUIRED標識無法加入到mPrivateFlag中,從而導致子控件無法進行布局。
綜上所述,測量控件樹的實質是測量控件樹的根控件。完成控件樹的測量之后,ViewRootImpl便得知了控件樹對窗口尺寸的需求。
##### 確定是否需要改變窗口尺寸
接下來回到performTraversals()方法。在ViewRootImpl.measureHierarchy()執行完畢之后,ViewRootImpl了解了控件樹所需的空間。于是便可確定是否需要改變窗口窗口尺寸以便滿足控件樹的空間要求。前述的代碼中多處設置windowSizeMayChange變量為true。windowSizeMayChange僅表示有可能需要改變窗口尺寸。而接下來的這段代碼則用來確定窗口是否需要改變尺寸。
**ViewRootImpl.java-->ViewRootImp.performTraversals()**
```
private void performTraversals() {
......// 測量控件樹的代碼
/* 標記mLayoutRequested為false。因此在此之后的代碼中,倘若控件樹中任何一個控件執行了
requestLayout(),都會重新進行一次“遍歷” */
if (layoutRequested) {
mLayoutRequested = false;
}
// 確定窗口是否確實需要進行尺寸的改變
booleanwindowShouldResize = layoutRequested && windowSizeMayChange
&& ((mWidth != host.getMeasuredWidth() || mHeight !=host.getMeasuredHeight())
|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.width() < desiredWindowWidth && frame.width() !=mWidth)
|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.height() < desiredWindowHeight && frame.height() !=mHeight));
}
```
確定窗口尺寸是否確實需要改變的條件看起來比較復雜,這里進行一下總結,先介紹必要條件:
- layoutRequested為true,即ViewRootImpl.requestLayout()方法被調用過。View中也有requestLayout()方法。當控件內容發生變化從而需要調整其尺寸時,會調用其自身的requestLayout(),并且此方法會沿著控件樹向根部回溯,最終調用到ViewRootImp.requestLayout(),從而引發一次performTraversals()調用。之所以這是一個必要條件,是因為performTraversals()還有可能因為控件需要重繪時被調用。當控件僅需要重繪而不需要重新布局時(例如背景色或前景色發生變化時),會通過invalidate()方法回溯到ViewRootImpl,此時不會通過performTraversals()觸發performTraversals()調用,而是通過scheduleTraversals()進行觸發。在這種情況下layoutRequested為false,即表示窗口尺寸不需發生變化。
- windowSizeMayChange為true,如前文所討論的,這意味著WMS單方面改變了窗口尺寸而控件樹的測量結果與這一尺寸有差異,或當前窗口為懸浮窗口,其控件樹的測量結果將決定窗口的新尺寸。
在滿足上述兩個條件的情況下,以下兩個條件滿足其一:
- 測量結果與ViewRootImpl中所保存的當前尺寸有差異。
- 懸浮窗口的測量結果與窗口的最新尺寸有差異。
注意ViewRootImpl對是否需要調整窗口尺寸的判斷是非常小心的。第4章介紹WMS的布局子系統時曾經介紹過,調整窗口尺寸所必須調用的performLayoutAndPlaceSurfacesLocked()函數會導致WMS對系統中的所有窗口新型重新布局,而且會引發至少一個動畫幀渲染,其計算開銷相當之大。因此ViewRootImpl僅在必要時才會驚動WMS。
至此,預測量階段完成了。
##### 總結
這一階段的工作內容是為了給后續階段做參數的準備并且其中最重要的工作是對控件樹的預測量,至此ViewRootImpl得知了控件樹對窗口尺寸的要求。另外,這一階段還準備了后續階段所需的其他參數:
- viewVisibilityChanged。即View的可見性是否發生了變化。由于mView是窗口的內容,因此mView的可見性即是窗口的可見性。當這一屬性發生變化時,需要通過通過WMS改變窗口的可見性。
- LayoutParams。預測量階段需要收集應用到LayoutParams的改動,這些改動一方面來自于WindowManager.updateViewLayout(),而另一方面則來自于控件樹。以SystemUIVisibility為例,View.setSystemUIVisibility()所修改的設置需要反映到LayoutParams中,而這些設置確卻保存在控件自己的成員變量里。在預測量階段會通過ViewRootImpl.collectViewAttributes()方法遍歷控件樹中的所有控件以收集這些設置,然后更新LayoutParams。
- 前言
- 推薦序
- 第1章 開發環境部署
- 1.1獲取Android源代碼
- 1.2Android的編譯
- 1.3在IDE中導入Android源代碼
- 1.3.1將Android源代碼導入Eclipse
- 1.3.2將Android源代碼導入SourceInsight
- 1.4調試Android源代碼
- 1.4.1使用Eclipse調試Android Java源代碼
- 1.4.2使用gdb調試Android C/C 源代碼
- 1.5本章小結
- 第2章 深入理解Java Binder和MessageQueue
- 2.1概述
- 2.2Java層中的Binder分析
- 2.2.1Binder架構總覽
- 2.2.2初始化Java層Binder框架
- 2.2.3窺一斑,可見全豹乎
- 2.2.4理解AIDL
- 2.2.5Java層Binder架構總結
- 2.3心系兩界的MessageQueue
- 2.3.1MessageQueue的創建
- 2.3.2提取消息
- 2.3.3nativePollOnce函數分析
- 2.3.4MessageQueue總結
- 2.4本章小結
- 第3章 深入理解AudioService
- 3.1概述
- 3.2音量管理
- 3.2.1音量鍵的處理流程
- 3.2.2通用的音量設置函數setStreamVolume()
- 3.2.3靜音控制
- 3.2.4音量控制小結
- 3.3音頻外設的管理
- 3.3.1 WiredAccessoryObserver 設備狀態的監控
- 3.3.2AudioService的外設狀態管理
- 3.3.3音頻外設管理小結
- 3.4AudioFocus機制的實現
- 3.4.1AudioFocus簡單的例子
- 3.4.2AudioFocus實現原理簡介
- 3.4.3申請AudioFocus
- 3.4.4釋放AudioFocus
- 3.4.5AudioFocus小結
- 3.5AudioService的其他功能
- 3.6本章小結
- 第4章 深入理解WindowManager-Service
- 4.1初識WindowManagerService
- 4.1.1一個從命令行啟動的動畫窗口
- 4.1.2WMS的構成
- 4.1.3初識WMS的小結
- 4.2WMS的窗口管理結構
- 4.2.1理解WindowToken
- 4.2.2理解WindowState
- 4.2.3理解DisplayContent
- 4.3理解窗口的顯示次序
- 4.3.1主序、子序和窗口類型
- 4.3.2通過主序與子序確定窗口的次序
- 4.3.3更新顯示次序到Surface
- 4.3.4關于顯示次序的小結
- 4.4窗口的布局
- 4.4.1從relayoutWindow()開始
- 4.4.2布局操作的外圍代碼分析
- 4.4.3初探performLayoutAndPlaceSurfacesLockedInner()
- 4.4.4布局的前期處理
- 4.4.5布局DisplayContent
- 4.4.6布局的階段
- 4.5WMS的動畫系統
- 4.5.1Android動畫原理簡介
- 4.5.2WMS的動畫系統框架
- 4.5.3WindowAnimator分析
- 4.5.4深入理解窗口動畫
- 4.5.5交替運行的布局系統與動畫系統
- 4.5.6動畫系統總結
- 4.6本章小結
- 第5章 深入理解Android輸入系統
- 5.1初識Android輸入系統
- 5.1.1getevent與sendevent工具
- 5.1.2Android輸入系統簡介
- 5.1.3IMS的構成
- 5.2原始事件的讀取與加工
- 5.2.1基礎知識:INotify與Epoll
- 5.2.2 InputReader的總體流程
- 5.2.3 深入理解EventHub
- 5.2.4 深入理解InputReader
- 5.2.5原始事件的讀取與加工總結
- 5.3輸入事件的派發
- 5.3.1通用事件派發流程
- 5.3.2按鍵事件的派發
- 5.3.3DispatcherPolicy與InputFilter
- 5.3.4輸入事件的派發總結
- 5.4輸入事件的發送、接收與反饋
- 5.4.1深入理解InputChannel
- 5.4.2連接InputDispatcher和窗口
- 5.4.3事件的發送
- 5.4.4事件的接收
- 5.4.5事件的反饋與發送循環
- 5.4.6輸入事件的發送、接收與反饋總結
- 5.5關于輸入系統的其他重要話題
- 5.5.1輸入事件ANR的產生
- 5.5.2 焦點窗口的確定
- 5.5.3以軟件方式模擬用戶操作
- 5.6本章小結
- 第6章 深入理解控件系統
- 6.1 初識Android的控件系統
- 6.1.1 另一種創建窗口的方法
- 6.1.2 控件系統的組成
- 6.2 深入理解WindowManager
- 6.2.1 WindowManager的創建與體系結構
- 6.2.2 通過WindowManagerGlobal添加窗口
- 6.2.3 更新窗口的布局
- 6.2.4 刪除窗口
- 6.2.5 WindowManager的總結
- 6.3 深入理解ViewRootImpl
- 6.3.1 ViewRootImpl的創建及其重要的成員
- 6.3.2 控件系統的心跳:performTraversals()
- 6.3.3 ViewRootImpl總結
- 6.4 深入理解控件樹的繪制
- 6.4.1 理解Canvas
- 6.4.2 View.invalidate()與臟區域
- 6.4.3 開始繪制
- 6.4.4 軟件繪制的原理
- 6.4.5 硬件加速繪制的原理
- 6.4.6 使用繪圖緩存
- 6.4.7 控件動畫
- 6.4.8 繪制控件樹的總結
- 6.5 深入理解輸入事件的派發
- 6.5.1 觸摸模式
- 6.5.2 控件焦點
- 6.5.3 輸入事件派發的綜述
- 6.5.4 按鍵事件的派發
- 6.5.5 觸摸事件的派發
- 6.5.6 輸入事件派發的總結
- 6.6 Activity與控件系統
- 6.6.1 理解PhoneWindow
- 6.6.2 Activity窗口的創建與顯示
- 6.7 本章小結
- 第7章 深入理解SystemUI
- 7.1 初識SystemUI
- 7.1.1 SystemUIService的啟動
- 7.1.2 狀態欄與導航欄的創建
- 7.1.3 理解IStatusBarService
- 7.1.4 SystemUI的體系結構
- 7.2 深入理解狀態欄
- 7.2.1 狀態欄窗口的創建與控件樹結構
- 7.2.2 通知信息的管理與顯示
- 7.2.3 系統狀態圖標區的管理與顯示
- 7.2.4 狀態欄總結
- 7.3 深入理解導航欄
- 7.3.1 導航欄的創建
- 7.3.2 虛擬按鍵的工作原理
- 7.3.3 SearchPanel
- 7.3.4 關于導航欄的其他話題
- 7.3.5 導航欄總結
- 7.4 禁用狀態欄與導航欄的功能
- 7.4.1 如何禁用狀態欄與導航欄的功能
- 7.4.2 StatusBarManagerService對禁用標記的維護
- 7.4.3 狀態欄與導航欄對禁用標記的響應
- 7.5 理解SystemUIVisibility
- 7.5.1 SystemUIVisibility在系統中的漫游過程
- 7.5.2 SystemUIVisibility發揮作用
- 7.5.3 SystemUIVisibility總結
- 7.6 本章小結
- 第8章 深入理解Android壁紙
- 8.1 初識Android壁紙
- 8.2深入理解動態壁紙
- 8.2.1啟動動態壁紙的方法
- 8.2.2壁紙服務的啟動原理
- 8.2.3 理解UpdateSurface()方法
- 8.2.4 壁紙的銷毀
- 8.2.5 理解Engine的回調
- 8.3 深入理解靜態壁紙-ImageWallpaper
- 8.3.1 獲取用作靜態壁紙的位圖
- 8.3.2 靜態壁紙位圖的設置
- 8.3.3 連接靜態壁紙的設置與獲取-WallpaperObserver
- 8.4 WMS對壁紙窗口的特殊處理
- 8.4.1 壁紙窗口Z序的確定
- 8.4.2 壁紙窗口的可見性
- 8.4.3 壁紙窗口的動畫
- 8.4.4 壁紙窗口總結
- 8.5 本章小結