Android佈局


本文是從網絡上覆制整理,方便我的閱讀。正確性不置能否。html


Android佈局原則:java

  1. 儘可能多使用LinearLayout和RelativeLayout;FrameLayout使用在佈局疊加的時;AbsoluteLayout已經廢棄,不要使用;TableLayout已經被GridView替代,不建議使用。網絡

  2. 在佈局層次同樣的狀況下,建議使用LinearLayout代替RelativeLayout,由於LinearLayout性能要稍高一點。app

  3. 將可複用的標籤抽取出來而且經過include標籤使用。ide

  4. 使用merge標籤減小布局的嵌套層次。oop

  5. 使用ViewStub標籤加載一些不經常使用的佈局。佈局


       RelativeLayout和LinearLayout是Android中經常使用的佈局,二者的使用會極大的影響程序生成每一幀的性能,所以,正確的使用它們是提高程序性能的重要工做。下面將經過分析它們的源碼來探討其View繪製性能,並得出其正確的使用方法。性能

RelativeLayout和LinearLayout是如何進行measure的?

        經過官方文檔咱們知道View的繪製進行measure, layout, draw,分別對應onMeasure(), onLayout, onDraw(),而他們的性能差別主要在onMeasure()上。優化

首先是RelativeLayout:ui

 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 }

        

         根據上述關鍵代碼,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 }

           

        根據線性佈局方向,執行不一樣的方法,這裏分析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 }

         

提升繪製性能的使用方式

        根據上面源碼的分析,RelativeLayout將對全部的子View進行兩次measure,而LinearLayout在使用 weight屬性進行佈局時也會對子View進行兩次measure,若是他們位於整個View樹的頂端時並可能進行多層的嵌套時,位於底層的View將 會進行大量的measure操做,大大下降程序性能。所以,應儘可能將RelativeLayout和LinearLayout置於View樹的底層,並減 少嵌套。



        2、Measure 和 Layout

        從總體上來看 Measure 和 Layout 兩個步驟的執行: MeasureLayout
        樹的遍歷是有序的,由父視圖到子視圖,每個 ViewGroup 負責測繪它全部的子視圖,而最底層的 View 會負責測繪自身。


        具體分析


        measure 過程由measure(int, int)方法發起,從上到下有序的測量 View ,在 measure 過程的最後,每一個視圖存儲了本身的尺寸大小和測量規格。 layout 過程由layout(int, int, int, int)方法發起,也是自上而下進行遍歷。在該過程當中,每一個父視圖會根據 measure 過程獲得的尺寸來擺放本身的子視圖。

        measure 過程會爲一個View及全部子節點的 mMeasuredWidth 和 mMeasuredHeight 變量賦值,該值能夠經過 getMeasuredWidth()getMeasuredHeight()方 法得到。並且這兩個值必須在父視圖約束範圍以內,這樣才能夠保證全部的父視圖都接收全部子視圖的測量。若是子視圖對於 Measure 獲得的大小不滿意的時候,父視圖會介入並設置測量規則進行第二次 measure。好比,父視圖能夠先根據未給定的 dimension 去測量每個子視圖,若是最終子視圖的未約束尺寸太大或者過小的時候,父視圖就會使用一個確切的大小再次對子視圖進行 measure 。

        measure 過程傳遞尺寸的兩個類

  • ViewGroup.LayoutParams (View 自身的佈局參數)

  • MeasureSpecs 類(父視圖對子視圖的測量要求)

    ViewGroup.LayoutParams
    這個類咱們很常見,就是用來指定視圖的高度和寬度等參數。對於每一個視圖的 height 和 width,你有如下選擇:

    • MATCH_PARENT 表示子視圖但願和父視圖同樣大(不包含padding值)

    • WRAP_CONTENT 表示視圖爲正好能包裹其內容大小(包含padding值)

ViewGroup 的子類有其對應的 ViewGroup.LayoutParams 的子類。好比 RelativeLayout 擁有的 ViewGroup.LayoutParams 的子類 RelativeLayoutParams。
有時咱們須要使用 view.getLayoutParams() 方法獲取一個視圖 LayoutParams ,而後進行強轉,但因爲不知道其具體類型,可能會致使強轉錯誤。其實該方法獲得的就是其所在父視圖類型的 LayoutParams,好比 View 的父控件爲 RelativeLayout,那麼獲得的 LayoutParams 類型就爲 RelativeLayoutParams。

MeasureSpecs
測量規格,包含測量要求和尺寸的信息,有三種模式:

  • UNSPECIFIED
    父視圖不對子視圖有任何約束,它能夠達到所指望的任意尺寸。好比ListView、ScrollView,通常自定義View中用不到,

  • EXACTLY
    父視圖爲子視圖指定一個確切的尺寸,並且不管子視圖指望多大,它都必須在該指定大小的邊界內,對應的屬性爲 match_parent 或具體指,好比 100dp,父控件能夠經過MeasureSpec.getSize(measureSpec)直接獲得子控件的尺寸。

  • AT_MOST
    父視圖爲子視圖指定一個最大尺寸。子視圖必須確保它本身全部子視圖能夠適應在該尺寸範圍內,對應的屬性爲 wrap_content,這種模式下,父控件沒法肯定子 View 的尺寸,只能由子控件本身根據需求去計算本身的尺寸,這種模式就是咱們自定義視圖須要實現測量邏輯的狀況。


       3、include

        在實際開發中,咱們常常會遇到一些共用的UI組件,好比帶返回按鈕的導航欄,若是爲每個xml文件都設置這部分佈局,一是重複的工做量大,二是若是有變動,那麼每個xml文件都得修改。不過,咱們能夠將這些共用的組件抽取出來單獨放到一個xml文件中,而後使用< include />標籤導入到相應佈局


        4、merge

        < merge />標籤的做用是合併UI佈局,使用該標籤能下降UI佈局的嵌套層次。該標籤的主要使用場景主要包括兩個,第一種狀況是當xml文件的根佈局是FrameLayout時,能夠用merge做爲根節點。理由是由於Activity的內容佈局中,默認就用了一個FrameLayout做爲xml佈局根節點的父節點;第二種狀況是當用include標籤導入一個共用佈局時,若是父佈局和子佈局根節點爲同一類型,可使用merge將子節點佈局的內容合併包含到父佈局中,這樣就能夠減小一級嵌套層次。這樣就下降了佈局嵌套層次。


        ViewStub

        ViewStub是Android佈局優化中一個很不錯的標籤/控件,直接繼承自View。可是真正用的可能很少。當對一個ViewStub調用inflate()方法或設置它可見時,系統會加載在ViewStub標籤中引入的咱們本身定義的View,而後填充在父布 局當中。也就是說,在對ViewStub調用inflate()方法或設置visible以前,它是不佔用佈局空間和系統資源的。它的使用場景能夠是在我 們須要加載並顯示一些不經常使用的View時,例如一些網絡異常的提示信息等。