Android中RelativeLayout和LinearLayout性能分析

【原文地址 點擊打開鏈接

先看一些現象吧:用eclipse或者Android studio,新建一個Activity自動生成的佈局文件都是RelativeLayout,或許你會認爲這是IDE的默認設置問題,其實不然,這是由 android-sdk\tools\templates\activities\BlankActivity\root\res\layout\activity_simple.xml.ftl 這個文件事先就定好了的,也就是說這是Google的選擇,而非IDE的選擇。那SDK爲什麼會默認給開發者新建一個默認的RelativeLayout佈局呢?當然是因爲RelativeLayout的性能更優,性能至上嘛。但是我們再看看默認新建的這個RelativeLayout的父容器,也就是當前窗口的頂級View——DecorView,它卻是個垂直方向的LinearLayout,上面是標題欄,下面是內容欄。那麼問題來了,Google爲什麼給開發者默認新建了個RelativeLayout,而自己卻偷偷用了個LinearLayout,到底誰的性能更高,開發者該怎麼選擇呢?

View的一些基本工作原理

先通過幾個問題,簡單的瞭解寫android中View的工作原理吧。

View是什麼?

簡單來說,View是Android系統在屏幕上的視覺呈現,也就是說你在手機屏幕上看到的東西都是View。

View是怎麼繪製出來的?

View的繪製流程是從ViewRoot的performTraversals()方法開始,依次經過measure(),layout()和draw()三個過程才最終將一個View繪製出來。

View是怎麼呈現在界面上的?

Android中的視圖都是通過Window來呈現的,不管Activity、Dialog還是Toast它們都有一個Window,然後通過WindowManager來管理View。Window和頂級View——DecorView的通信是依賴ViewRoot完成的。

View和ViewGroup什麼區別?

不管簡單的Button和TextView還是複雜的RelativeLayout和ListView,他們的共同基類都是View。所以說,View是一種界面層控件的抽象,他代表了一個控件。那ViewGroup是什麼東西,它可以被翻譯成控件組,即一組View。ViewGroup也是繼承View,這就意味着View本身可以是單個控件,也可以是多個控件組成的控件組。根據這個理論,Button顯然是個View,而RelativeLayout不但是一個View還可以是一個ViewGroup,而ViewGroup內部是可以有子View的,這個子View同樣也可能是ViewGroup,以此類推。

RelativeLayout和LinearLayout性能PK

基於以上原理和大背景,我們要探討的性能問題,說的簡單明瞭一點就是:當RelativeLayout和LinearLayout分別作爲ViewGroup,表達相同佈局時繪製在屏幕上時誰更快一點。上面已經簡單說了View的繪製,從ViewRoot的performTraversals()方法開始依次調用perfromMeasure、performLayout和performDraw這三個方法。這三個方法分別完成頂級View的measure、layout和draw三大流程,其中perfromMeasure會調用measure,measure又會調用onMeasure,在onMeasure方法中則會對所有子元素進行measure,這個時候measure流程就從父容器傳遞到子元素中了,這樣就完成了一次measure過程,接着子元素會重複父容器的measure,如此反覆就完成了整個View樹的遍歷。同理,performLayout和performDraw也分別完成perfromMeasure類似的流程。通過這三大流程,分別遍歷整棵View樹,就實現了Measure,Layout,Draw這一過程,View就繪製出來了。那麼我們就分別來追蹤下RelativeLayout和LinearLayout這三大流程的執行耗時。
如下圖,我們分別用兩用種方式簡單的實現佈局測試下


31713556B8907738423903D70A5031AA.jpg
LinearLayout

Measure:0.738ms
Layout:0.176ms
draw:7.655ms

RelativeLayout

Measure:2.280ms
Layout:0.153ms
draw:7.696ms
從這個數據來看無論使用RelativeLayout還是LinearLayout,layout和draw的過程兩者相差無幾,考慮到誤差的問題,幾乎可以認爲兩者不分伯仲,關鍵是Measure的過程RelativeLayout卻比LinearLayout慢了一大截。

Measure都幹什麼了

RelativeLayout的onMeasure()方法
View[] views = mSortedHorizontalChildren;
    int count = views.length;

    for (int i = 0; i < count; i++) {
      View child = views[i];
      if (child.getVisibility() != GONE) {
        LayoutParams params = (LayoutParams) child.getLayoutParams();
        int[] rules = params.getRules(layoutDirection);

        applyHorizontalSizeRules(params, myWidth, rules);
        measureChildHorizontal(child, params, myWidth, myHeight);

        if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
          offsetHorizontalAxis = true;
        }
      }
    }

    views = mSortedVerticalChildren;
    count = views.length;
    final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;

    for (int i = 0; i < count; i++) {
      View child = views[i];
      if (child.getVisibility() != GONE) {
        LayoutParams params = (LayoutParams) child.getLayoutParams();

        applyVerticalSizeRules(params, myHeight);
        measureChild(child, params, myWidth, myHeight);
        if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
          offsetVerticalAxis = true;
        }

        if (isWrapContentWidth) {
          if (isLayoutRtl()) {
            if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
              width = Math.max(width, myWidth - params.mLeft);
            } else {
              width = Math.max(width, myWidth - params.mLeft - params.leftMargin);
            }
          } else {
            if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
              width = Math.max(width, params.mRight);
            } else {
              width = Math.max(width, params.mRight + params.rightMargin);
            }
          }
        }

        if (isWrapContentHeight) {
          if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
            height = Math.max(height, params.mBottom);
          } else {
            height = Math.max(height, params.mBottom + params.bottomMargin);
          }
        }

        if (child != ignore || verticalGravity) {
          left = Math.min(left, params.mLeft - params.leftMargin);
          top = Math.min(top, params.mTop - params.topMargin);
        }

        if (child != ignore || horizontalGravity) {
          right = Math.max(right, params.mRight + params.rightMargin);
          bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
        }
      }
    }

根據源碼我們發現RelativeLayout會對子View做兩次measure。這是爲什麼呢?首先RelativeLayout中子View的排列方式是基於彼此的依賴關係,而這個依賴關係可能和佈局中View的順序並不相同,在確定每個子View的位置的時候,就需要先給所有的子View排序一下。又因爲RelativeLayout允許A,B 2個子View,橫向上B依賴A,縱向上A依賴B。所以需要橫向縱向分別進行一次排序測量。

LinearLayout的onMeasure()方法
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
      measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
      measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
  }

與RelativeLayout相比LinearLayout的measure就簡單明瞭的多了,先判斷線性規則,然後執行對應方向上的測量。隨便看一個吧。

for (int i = 0; i < count; ++i) {
      final View child = getVirtualChildAt(i);

      if (child == null) {
        mTotalLength += measureNullChild(i);
        continue;
      }

      if (child.getVisibility() == View.GONE) {
       i += getChildrenSkipCount(child, i);
       continue;
      }

      if (hasDividerBeforeChildAt(i)) {
        mTotalLength += mDividerHeight;
      }

      LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

      totalWeight += lp.weight;

      if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
        // Optimization: don't bother measuring children who are going to use
        // leftover space. These views will get measured again down below if
        // there is any leftover space.
        final int totalLength = mTotalLength;
        mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
      } else {
        int oldHeight = Integer.MIN_VALUE;

        if (lp.height == 0 && lp.weight > 0) {
          // heightMode is either UNSPECIFIED or AT_MOST, and this
          // child wanted to stretch to fill available space.
          // Translate that to WRAP_CONTENT so that it does not end up
          // with a height of 0
          oldHeight = 0;
          lp.height = LayoutParams.WRAP_CONTENT;
        }

        // Determine how big this child would like to be. If this or
        // previous children have given a weight, then we allow it to
        // use all available space (and we will shrink things later
        // if needed).
        measureChildBeforeLayout(
           child, i, widthMeasureSpec, 0, heightMeasureSpec,
           totalWeight == 0 ? mTotalLength : 0);

        if (oldHeight != Integer.MIN_VALUE) {
         lp.height = oldHeight;
        }

        final int childHeight = child.getMeasuredHeight();
        final int totalLength = mTotalLength;
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
           lp.bottomMargin + getNextLocationOffset(child));

        if (useLargestChild) {
          largestChildHeight = Math.max(childHeight, largestChildHeight);
        }
      }

父視圖在對子視圖進行measure操作的過程中,使用變量mTotalLength保存已經measure過的child所佔用的高度,該變量剛開始時是0。在for循環中調用measureChildBeforeLayout()對每一個child進行測量,該函數實際上僅僅是調用了measureChildWithMargins(),在調用該方法時,使用了兩個參數。其中一個是heightMeasureSpec,該參數爲LinearLayout本身的measureSpec;另一個參數就是mTotalLength,代表該LinearLayout已經被其子視圖所佔用的高度。 每次for循環對child測量完畢後,調用child.getMeasuredHeight()獲取該子視圖最終的高度,並將這個高度添加到mTotalLength中。在本步驟中,暫時避開了lp.weight>0的子視圖,即暫時先不測量這些子視圖,因爲後面將把父視圖剩餘的高度按照weight值的大小平均分配給相應的子視圖。源碼中使用了一個局部變量totalWeight累計所有子視圖的weight值。處理lp.weight>0的情況需要注意,如果變量heightMode是EXACTLY,那麼,當其他子視圖佔滿父視圖的高度後,weight>0的子視圖可能分配不到佈局空間,從而不被顯示,只有當heightMode是AT_MOST或者UNSPECIFIED時,weight>0的視圖才能優先獲得佈局高度。最後我們的結論是:如果不使用weight屬性,LinearLayout會在當前方向上進行一次measure的過程,如果使用weight屬性,LinearLayout會避開設置過weight屬性的view做第一次measure,完了再對設置過weight屬性的view做第二次measure。由此可見,weight屬性對性能是有影響的,而且本身有大坑,請注意避讓。

小結

從源碼中我們似乎能看出,我們先前的測試結果中RelativeLayout不如LinearLayout快的根本原因是RelativeLayout需要對其子View進行兩次measure過程。而LinearLayout則只需一次measure過程,所以顯然會快於RelativeLayout,但是如果LinearLayout中有weight屬性,則也需要進行兩次measure,但即便如此,應該仍然會比RelativeLayout的情況好一點。

RelativeLayout另一個性能問題

對比到這裏就結束了嘛?顯然沒有!我們再看看View的Measure()方法都幹了些什麼?

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
        widthMeasureSpec != mOldWidthMeasureSpec ||
        heightMeasureSpec != mOldHeightMeasureSpec) {
                     ......
      }
       mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
        (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
  }

View的measure方法裏對繪製過程做了一個優化,如果我們或者我們的子View沒有要求強制刷新,而父View給子View的傳入值也沒有變化(也就是說子View的位置沒變化),就不會做無謂的measure。但是上面已經說了RelativeLayout要做兩次measure,而在做橫向的測量時,縱向的測量結果尚未完成,只好暫時使用myHeight傳入子View系統,假如子View的Height不等於(設置了margin)myHeight的高度,那麼measure中上面代碼所做得優化將不起作用,這一過程將進一步影響RelativeLayout的繪製性能。而LinearLayout則無這方面的擔憂。解決這個問題也很好辦,如果可以,儘量使用padding代替margin。

結論

1.RelativeLayout會讓子View調用2次onMeasure,LinearLayout 在有weight時,也會調用子View2次onMeasure
2.RelativeLayout的子View如果高度和RelativeLayout不同,則會引發效率問題,當子View很複雜時,這個問題會更加嚴重。如果可以,儘量使用padding代替margin。
3.在不影響層級深度的情況下,使用LinearLayout和FrameLayout而不是RelativeLayout。
最後再思考一下文章開頭那個矛盾的問題,爲什麼Google給開發者默認新建了個RelativeLayout,而自己卻在DecorView中用了個LinearLayout。因爲DecorView的層級深度是已知而且固定的,上面一個標題欄,下面一個內容欄。採用RelativeLayout並不會降低層級深度,所以此時在根節點上用LinearLayout是效率最高的。而之所以給開發者默認新建了個RelativeLayout是希望開發者能採用儘量少的View層級來表達佈局以實現性能最優,因爲複雜的View嵌套對性能的影響會更大一些。



文/尹star(簡書作者)
原文鏈接:http://www.jianshu.com/p/8a7d059da746
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章