性能優化(1.4)-各種布局的性能對比(LinearLayout,RelativeLayout)

主目錄見:Android高級進階知識(這是總目錄索引)
?前面一周由于休假所以沒有寫文章,今天剛回來呢,打算給大家一起說說這兩個布局的性能對比,通過這個性能對比我們也能更好地使用這兩個布局,希望大家一起享受這一段旅程。
同時推薦一篇ConstraintLayout 完全解析 快來優化你的布局吧,這個是新的layout,大家可以看看。

一.目標

今天主要是通過源碼來分析下這兩個布局的性能,但是不會非常詳細地一句一句代碼進行解析,不然代碼涉及的東西還蠻多,如果需要每個都懂得話可以留言我會說明。
1.弄懂LinearLayout和RelativeLayout的性能;
2.明白在什么場景使用什么布局。

二.性能對比

?我們這里先說下我個人看法,在簡單布局可以用單層LinearLayout完成的布局我們可以選擇LinearLayout進行布局,如果用單層LinearLayout完成不了而要嵌套的話,那么我們可以考慮用RelativeLayout來完成布局。

1.繪制流程

?通過前面View和ViewGroup的繪制原理源碼分析這篇文章我們知道了我們的繪制過程是從performTraversals()分別調用perfromMeasure、performLayout和performDraw這三個方法。這三個方法分別完成頂級View的measure、layout和draw三大流程。然后遍歷子節點分別重復這幾個步驟,直到整個view樹完成,view也就顯示出來。所以我們看下這幾個流程的耗時我們就知道他們的性能情況了。
?首先我們選擇了一個一樣的布局,然后分別用RelativeLayout布局和LinearLayout布局,我們看下布局的樣子:

布局

然后我們看下頂層分別用LinearLayout和用RelativeLayout的耗時情況:


RelativeLayout
LinearLayout

上面兩張圖是用Hierarchy Viewer里面看的,如果有興趣也可以自己去看看。我們看到這里的layout和draw這兩個流程時間差不多,當然由于這個工具有可能多次刷新會出現結果不同,但是不同的是measure這個流程RelativeLayout用時都會相對長些,因為這里布局簡單不涉及多層嵌套,所以RelativeLayout不能發揮出優勢,我們來看看源碼是為什么?

2.LinearLayout

我們今天就來看看measure這個流程這兩個布局分別干了什么:

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

我們看到代碼很簡單,就是根據orientation是垂直的還是水平的進行布局。我們就來看看 measureHorizontal的源碼:

        // See how wide everyone is. Also remember max height.
//獲得子view的寬度,并記下最大的高度
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);//默認返回0
                continue;
            }
           
            if (child.getVisibility() == GONE) {
                i += getChildrenSkipCount(child, i);//默認返回0
                continue;
            }

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

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//獲取所有子視圖的lp,然后獲取總共的權重weight
            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.width == 0 && lp.weight > 0;
            if (widthMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
 //如果LinearLayout寬度是已經確定的。并且這個子view的width=0,weight>0,  
 //則mTotalLength只需要加上margin即可,  
//由于是weight>0;該view的具體高度等會還要計算  
                if (isExactly) {
                    mTotalLength += lp.leftMargin + lp.rightMargin;
                } else {
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength +
                            lp.leftMargin + lp.rightMargin);
                }

                // Baseline alignment requires to measure widgets to obtain the
                // baseline offset (in particular for TextViews). The following
                // defeats the optimization mentioned above. Allow the child to
                // use as much space as it wants because we can shrink things
                // later (and re-measure).
                if (baselineAligned) {
                    final int freeWidthSpec = MeasureSpec.makeSafeMeasureSpec(
                            MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.UNSPECIFIED);
                    final int freeHeightSpec = MeasureSpec.makeSafeMeasureSpec(
                            MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED);
                    child.measure(freeWidthSpec, freeHeightSpec);
                } else {
                    skippedMeasure = true;
                }
            } else {
//否則如果模式是wrap_cotent的話,那么就要先測量子view,然后將子view的寬高和間隔統計相加用
//mTotalLength 存儲起來
                if (useExcessSpace) {
                    // The widthMode is either UNSPECIFIED or AT_MOST, and
                    // this child is only laid out using excess space. Measure
                    // using WRAP_CONTENT so that we can find out the view's
                    // optimal width. We'll restore the original width of 0
                    // after measurement.
                    lp.width = 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).
                final int usedWidth = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, usedWidth,
                        heightMeasureSpec, 0);

                final int childWidth = child.getMeasuredWidth();
                if (useExcessSpace) {
                    // Restore the original width and record how much space
                    // we've allocated to excess-only children so that we can
                    // match the behavior of EXACTLY measurement.
                    lp.width = 0;
                    usedExcessSpace += childWidth;
                }

                if (isExactly) {
                    mTotalLength += childWidth + lp.leftMargin + lp.rightMargin
                            + getNextLocationOffset(child);
                } else {
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + childWidth + lp.leftMargin
                            + lp.rightMargin + getNextLocationOffset(child));
                }

                if (useLargestChild) {
                    largestChildWidth = Math.max(childWidth, largestChildWidth);
                }
            }
.........   
        }

我們看到LiearLayout的onMeasure中,使用了mTotalLength來保存測量過的子視圖的總寬度。在for循環中,如果是wrap_content的話,那么我們會調用measureChildBeforeLayout()方法,其中一個參數是widthMeasureSpec,另外一個是usedWidth(已經被子視圖使用的寬度)。每次for循環對child測量完畢后,程序就會調用getMeasuredWidth()方法來得到child的寬度,然后添加進mTotalLength 中來。這里面暫時沒有考慮weight>0的情況,因為如果考慮這個的話,后面會進行第二次的測量,父視圖會把剩余的寬度按照weight值的大小平均分配給相應的子視圖。那么我們來看weight>0的情況,這里的代碼也比較長,我們這里說明一下代碼邏輯:

1.weight>0,且width=0,mode=EXACTLY,那么寬度就是share = (int) (childWeight * remainingExcess / remainingWeightSum),是根據剩余空間跟view的weight計算得到,也就是說如果剩余空間為零,那么視圖的大小也會為零。
2.weight>0,mode != EXACTLY,那么得到的寬度就是本身的控件寬度加上share 的寬度。也就是說是wrap_content的話那么寬度是自身的寬度加上剩余的空間占比(也就是說能優先獲得自身的布局寬度,然后再去加上剩余的空間占比)。

所以我們有結論得出,如果我們布局中設置了weight的話,那么LinearLayout的話會測量兩次,這樣明顯影響了性能,所以我們應該能不適用weight的時候就少用。

3.RelativeLayout

RelativeLayout的源代碼還是比較復雜的,而且里面的依賴關系是用圖來做的,而且里面會進行圖的拓撲排序。我們這里同樣就不進行一句一句地講解,我們先來手下RelativeLayout的測量做了哪些工作:

  • 1.子視圖根據橫向關系和縱向關系排序 sortChildren();
  • 2.初始化一些變量值;
  • 3.遍歷水平關系的View,將相對布局的關系轉化為左右坐標,然后確立水平方向的子View位置;
  • 4.遍歷垂直關系的View,將相對布局關系轉化為垂直坐標,然后確立垂直方向的子View的位置;
  • 5.baseline計算;
  • 6.寬度和高度修正。
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mDirtyHierarchy) {
            mDirtyHierarchy = false;
            sortChildren();
        }
//省略初始化變量代碼
.........
        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++) {
            final View child = views[i];
            if (child.getVisibility() != GONE) {
                final LayoutParams params = (LayoutParams) child.getLayoutParams();

                applyVerticalSizeRules(params, myHeight, child.getBaseline());
                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);
                }
            }
        }
//省略寬度和高度修正代碼和baseline計算代碼
........
        setMeasuredDimension(width, height);
    }

從源碼里面我們可以看出來,這邊兩次for循環分別會根據我們設置的依賴關系,比如A垂直依賴B,B水平依賴C,那么程序會進行水平方向的依賴關系解析,然后確定坐標和位置。同樣垂直方向也是如此。這里的依賴關系節點是用圖的形式存儲,這里的代碼跟Behavior里面的源碼有點像,那個依賴關系也是用圖來存儲的,然后搜索的時候可以深度和廣度搜索排序。有興趣大家可以了解一下數據結構中圖的相關知識。所以我們看到我們的RelativeLayout會進行兩次的測量,這樣有可能會成為性能消耗的原因,但是同時RelativeLayout在復雜布局時候有可能減少嵌套層數。所以在復雜嵌套時候我們可以考慮使用他或者LinearLayout使用到weight的情況我們也可以考慮使用它。

總結:我們上面的解釋也說的很清楚了,如果有什么疑問或者錯誤大家可以留言哈,同時附上大家一直說的一個問題:為什么Google給開發者默認新建了個RelativeLayout,而自己卻在DecorView中用了個LinearLayout?因為DecorView的層級深度已知且固定的,上面一個標題欄,下面一個內容欄,采用RelativeLayout并不會降低層級深度,因此這種情況下使用LinearLayout效率更高。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容