本文是從網絡上復制整理,方便個人閱讀。正確性不置可否。
為進賢等地區(qū)用戶提供了全套網頁設計制作服務,及進賢網站建設行業(yè)解決方案。主營業(yè)務為成都網站設計、成都做網站、進賢網站設計,以傳統(tǒng)方式定制建設網站,并提供域名空間備案等一條龍服務,秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務。我們深信只要達到每一位用戶的要求,就會得到認可,從而選擇與我們長期合作。這樣,我們也可以走得更遠!
Android布局原則:
盡量多使用LinearLayout和RelativeLayout;FrameLayout使用在布局疊加的時;AbsoluteLayout已經廢棄,不要使用;TableLayout已經被GridView替代,不建議使用。
在布局層次一樣的情況下,建議使用LinearLayout代替RelativeLayout,因為LinearLayout性能要稍高一點。
將可復用的標簽抽取出來并且通過include標簽使用。
使用merge標簽減少布局的嵌套層次。
使用ViewStub標簽加載一些不常用的布局。
一、RelativeLayout和LinearLayout是Android中常用的布局,兩者的使用會極大的影響程序生成每一幀的性能,因此,正確的使用它們是提升程序性能的重要工作。下面將通過分析它們的源碼來探討其View繪制性能,并得出其正確的使用方法。
通過官方文檔我們知道View的繪制進行measure, layout, draw,分別對應onMeasure(), onLayout, onDraw(),而他們的性能差異主要在onMeasure()上。
首先是RelativeLayout:
1 @Override 2 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 3 ...... 4 View[] views = mSortedHorizontalChildren; 5 int count = views.length; 6 7 for (int i = 0; i < count; i++) { 8 View child = views[i]; 9 if (child.getVisibility() != GONE) { 10 LayoutParams params = (LayoutParams) child.getLayoutParams(); 11 int[] rules = params.getRules(layoutDirection); 12 13 applyHorizontalSizeRules(params, myWidth, rules); 14 measureChildHorizontal(child, params, myWidth, myHeight); 15 16 if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) { 17 offsetHorizontalAxis = true; 18 } 19 } 20 } 21 22 views = mSortedVerticalChildren; 23 count = views.length; 24 final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; 25 26 for (int i = 0; i < count; i++) { 27 View child = views[i]; 28 if (child.getVisibility() != GONE) { 29 LayoutParams params = (LayoutParams) child.getLayoutParams(); 30 31 applyVerticalSizeRules(params, myHeight); 32 measureChild(child, params, myWidth, myHeight); 33 if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) { 34 offsetVerticalAxis = true; 35 } 36 37 if (isWrapContentWidth) { 38 if (isLayoutRtl()) { 39 if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { 40 width = Math.max(width, myWidth - params.mLeft); 41 } else { 42 width = Math.max(width, myWidth - params.mLeft - params.leftMargin); 43 } 44 } else { 45 if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { 46 width = Math.max(width, params.mRight); 47 } else { 48 width = Math.max(width, params.mRight + params.rightMargin); 49 } 50 } 51 } 52 53 if (isWrapContentHeight) { 54 if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { 55 height = Math.max(height, params.mBottom); 56 } else { 57 height = Math.max(height, params.mBottom + params.bottomMargin); 58 } 59 } 60 61 if (child != ignore || verticalGravity) { 62 left = Math.min(left, params.mLeft - params.leftMargin); 63 top = Math.min(top, params.mTop - params.topMargin); 64 } 65 66 if (child != ignore || horizontalGravity) { 67 right = Math.max(right, params.mRight + params.rightMargin); 68 bottom = Math.max(bottom, params.mBottom + params.bottomMargin); 69 } 70 } 71 } 72 ...... 73 }
根據(jù)上述關鍵代碼,RelativeLayout分別對所有子View進行兩次measure,橫向縱向分別進行一次。
LinearLayout:
1 @Override 2 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 3 if (mOrientation == VERTICAL) { 4 measureVertical(widthMeasureSpec, heightMeasureSpec); 5 } else { 6 measureHorizontal(widthMeasureSpec, heightMeasureSpec); 7 } 8 }
根據(jù)線性布局方向,執(zhí)行不同的方法,這里分析measureVertical方法。
1 void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { 2 ...... 3 for (int i = 0; i < count; ++i) { 4 ...... 5 6 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); 7 8 totalWeight += lp.weight; 9 10 if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) { 11 // Optimization: don't bother measuring children who are going to use 12 // leftover space. These views will get measured again down below if 13 // there is any leftover space. 14 final int totalLength = mTotalLength; 15 mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); 16 skippedMeasure = true; 17 } else { 18 int oldHeight = Integer.MIN_VALUE; 19 20 if (lp.height == 0 && lp.weight > 0) { 21 // heightMode is either UNSPECIFIED or AT_MOST, and this 22 // child wanted to stretch to fill available space. 23 // Translate that to WRAP_CONTENT so that it does not end up 24 // with a height of 0 25 oldHeight = 0; 26 lp.height = LayoutParams.WRAP_CONTENT; 27 } 28 29 // Determine how big this child would like to be. If this or 30 // previous children have given a weight, then we allow it to 31 // use all available space (and we will shrink things later 32 // if needed). 33 measureChildBeforeLayout( 34 child, i, widthMeasureSpec, 0, heightMeasureSpec, 35 totalWeight == 0 ? mTotalLength : 0); 36 37 if (oldHeight != Integer.MIN_VALUE) { 38 lp.height = oldHeight; 39 } 40 41 final int childHeight = child.getMeasuredHeight(); 42 final int totalLength = mTotalLength; 43 mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + 44 lp.bottomMargin + getNextLocationOffset(child)); 45 46 if (useLargestChild) { 47 largestChildHeight = Math.max(childHeight, largestChildHeight); 48 } 49 } 50 ......
LinearLayout首先會對所有的子View進行measure,并計算totalWeight(所有子View的weight屬性之和),然后判斷子View的weight屬性是否為最大,如為最大則將剩余的空間分配給它。如果不使用weight屬性進行布局,則不進行第二次measure。
1 // Either expand children with weight to take up available space or 2 // shrink them if they extend beyond our current bounds. If we skipped 3 // measurement on any children, we need to measure them now. 4 int delta = heightSize - mTotalLength; 5 if (skippedMeasure || delta != 0 && totalWeight > 0.0f) { 6 float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; 7 8 mTotalLength = 0; 9 10 for (int i = 0; i < count; ++i) { 11 final View child = getVirtualChildAt(i); 12 13 if (child.getVisibility() == View.GONE) { 14 continue; 15 } 16 17 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); 18 19 float childExtra = lp.weight; 20 if (childExtra > 0) { 21 // Child said it could absorb extra space -- give him his share 22 int share = (int) (childExtra * delta / weightSum); 23 weightSum -= childExtra; 24 delta -= share; 25 26 final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 27 mPaddingLeft + mPaddingRight + 28 lp.leftMargin + lp.rightMargin, lp.width); 29 30 // TODO: Use a field like lp.isMeasured to figure out if this 31 // child has been previously measured 32 if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) { 33 // child was measured once already above... 34 // base new measurement on stored values 35 int childHeight = child.getMeasuredHeight() + share; 36 if (childHeight < 0) { 37 childHeight = 0; 38 } 39 40 child.measure(childWidthMeasureSpec, 41 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)); 42 } else { 43 // child was skipped in the loop above. 44 // Measure for this first time here 45 child.measure(childWidthMeasureSpec, 46 MeasureSpec.makeMeasureSpec(share > 0 ? share : 0, 47 MeasureSpec.EXACTLY)); 48 } 49 50 // Child may now not fit in vertical dimension. 51 childState = combineMeasuredStates(childState, child.getMeasuredState() 52 & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT)); 53 } 54 55 ...... 56 } 57 ...... 58 } else { 59 alternativeMaxWidth = Math.max(alternativeMaxWidth, 60 weightedMaxWidth); 61 62 63 // We have no limit, so make all weighted views as tall as the largest child. 64 // Children will have already been measured once. 65 if (useLargestChild && heightMode != MeasureSpec.EXACTLY) { 66 for (int i = 0; i < count; i++) { 67 final View child = getVirtualChildAt(i); 68 69 if (child == null || child.getVisibility() == View.GONE) { 70 continue; 71 } 72 73 final LinearLayout.LayoutParams lp = 74 (LinearLayout.LayoutParams) child.getLayoutParams(); 75 76 float childExtra = lp.weight; 77 if (childExtra > 0) { 78 child.measure( 79 MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), 80 MeasureSpec.EXACTLY), 81 MeasureSpec.makeMeasureSpec(largestChildHeight, 82 MeasureSpec.EXACTLY)); 83 } 84 } 85 } 86 } 87 ...... 88 }
根據(jù)上面源碼的分析,RelativeLayout將對所有的子View進行兩次measure,而LinearLayout在使用 weight屬性進行布局時也會對子View進行兩次measure,如果他們位于整個View樹的頂端時并可能進行多層的嵌套時,位于底層的View將 會進行大量的measure操作,大大降低程序性能。因此,應盡量將RelativeLayout和LinearLayout置于View樹的底層,并減 少嵌套。
二、Measure 和 Layout
從整體上來看 Measure 和 Layout 兩個步驟的執(zhí)行:
樹的遍歷是有序的,由父視圖到子視圖,每一個 ViewGroup 負責測繪它所有的子視圖,而最底層的 View 會負責測繪自身。
具體分析
measure 過程由measure(int, int)
方法發(fā)起,從上到下有序的測量 View ,在 measure 過程的最后,每個視圖存儲了自己的尺寸大小和測量規(guī)格。 layout 過程由layout(int, int, int, int)
方法發(fā)起,也是自上而下進行遍歷。在該過程中,每個父視圖會根據(jù) measure 過程得到的尺寸來擺放自己的子視圖。
measure 過程會為一個View及所有子節(jié)點的 mMeasuredWidth 和 mMeasuredHeight 變量賦值,該值可以通過 getMeasuredWidth()
和getMeasuredHeight()
方
法獲得。而且這兩個值必須在父視圖約束范圍之內,這樣才可以保證所有的父視圖都接收所有子視圖的測量。如果子視圖對于 Measure
得到的大小不滿意的時候,父視圖會介入并設置測量規(guī)則進行第二次 measure。比如,父視圖可以先根據(jù)未給定的 dimension
去測量每一個子視圖,如果最終子視圖的未約束尺寸太大或者太小的時候,父視圖就會使用一個確切的大小再次對子視圖進行 measure 。
measure 過程傳遞尺寸的兩個類
ViewGroup.LayoutParams (View 自身的布局參數(shù))
MeasureSpecs 類(父視圖對子視圖的測量要求)
ViewGroup.LayoutParams
這個類我們很常見,就是用來指定視圖的高度和寬度等參數(shù)。對于每個視圖的 height 和 width,你有以下選擇:
MATCH_PARENT 表示子視圖希望和父視圖一樣大(不包含padding值)
WRAP_CONTENT 表示視圖為正好能包裹其內容大小(包含padding值)
ViewGroup 的子類有其對應的 ViewGroup.LayoutParams 的子類。比如 RelativeLayout 擁有的 ViewGroup.LayoutParams 的子類 RelativeLayoutParams。
有時我們需要使用 view.getLayoutParams() 方法獲取一個視圖 LayoutParams
,然后進行強轉,但由于不知道其具體類型,可能會導致強轉錯誤。其實該方法得到的就是其所在父視圖類型的 LayoutParams,比如 View
的父控件為 RelativeLayout,那么得到的 LayoutParams 類型就為 RelativeLayoutParams。
MeasureSpecs
測量規(guī)格,包含測量要求和尺寸的信息,有三種模式:
UNSPECIFIED
父視圖不對子視圖有任何約束,它可以達到所期望的任意尺寸。比如ListView、ScrollView,一般自定義View中用不到,
EXACTLY
父視圖為子視圖指定一個確切的尺寸,而且無論子視圖期望多大,它都必須在該指定大小的邊界內,對應的屬性為 match_parent 或具體指,比如 100dp,父控件可以通過MeasureSpec.getSize(measureSpec)
直接得到子控件的尺寸。
AT_MOST
父視圖為子視圖指定一個最大尺寸。子視圖必須確保它自己所有子視圖可以適應在該尺寸范圍內,對應的屬性為
wrap_content,這種模式下,父控件無法確定子 View
的尺寸,只能由子控件自己根據(jù)需求去計算自己的尺寸,這種模式就是我們自定義視圖需要實現(xiàn)測量邏輯的情況。
三、include
在實際開發(fā)中,我們經常會遇到一些共用的UI組件,比如帶返回按鈕的導航欄,如果為每一個xml文件都設置這部分布局,一是重復的工作量大,二是如果有變更,那么每一個xml文件都得修改。不過,我們可以將這些共用的組件抽取出來單獨放到一個xml文件中,然后使用< include />標簽導入到相應布局
四、merge
< merge />標簽的作用是合并UI布局,使用該標簽能降低UI布局的嵌套層次。該標簽的主要使用場景主要包括兩個,第一種情況是當xml文件的根布局是FrameLayout時,可以用merge作為根節(jié)點。理由是因為Activity的內容布局中,默認就用了一個FrameLayout作為xml布局根節(jié)點的父節(jié)點;第二種情況是當用include標簽導入一個共用布局時,如果父布局和子布局根節(jié)點為同一類型,可以使用merge將子節(jié)點布局的內容合并包含到父布局中,這樣就可以減少一級嵌套層次。這樣就降低了布局嵌套層次。
五、ViewStub
ViewStub是Android布局優(yōu)化中一個很不錯的標簽/控件,直接繼承自View。但是真正用的可能不多。當對一個ViewStub調用inflate()方法或設置它可見時,系統(tǒng)會加載在ViewStub標簽中引入的我們自己定義的View,然后填充在父布 局當中。也就是說,在對ViewStub調用inflate()方法或設置visible之前,它是不占用布局空間和系統(tǒng)資源的。它的使用場景可以是在我 們需要加載并顯示一些不常用的View時,例如一些網絡異常的提示信息等。