第七章 佈局

第七章 Android佈局

(一)六大布局

(1)LinearLayout線性佈局

LinearLayout容器中的組件一個挨一個排列,通過控制android:orientation屬性,可控制各組件是橫向排列還是縱向排列。

(2)TableLayout表格佈局

TableLayout繼承自Linearout,本質上仍然是線性佈局管理器。表格佈局採用行、列的形式來管理UI組件,並不需要明確地聲明包含多少行、多少列,而是通過添加TableRow、其他組件來控制表格的行數和列數。

(3)FrameLayout幀佈局

幀佈局或叫層佈局,從屏幕左上角按照層次堆疊方式佈局,後面的控件覆蓋前面的控件。幀佈局爲每個加入其中的組件創建一個空白的區域(稱爲一幀),每個子組件佔據一幀,這些幀會根據gravity屬性執行自動對齊。
該佈局在開發中設計地圖經常用到,因爲是按層次方式佈局,我們需要實現層面顯示的樣式時就可以
採用這種佈局方式,比如我們要實現一個類似百度地圖的佈局,我們移動的標誌是在一個圖層的上面。

(4)RelativeLayout相對佈局

相對佈局可以讓子控件相對於兄弟控件或父控件進行佈局,可以設置子控件相對於兄弟控件或父控件進行上下左右對齊。
RelativeLayout能替換一些嵌套視圖,當我們用LinearLayout來實現一個簡單的佈局但又使用了過多的嵌套時,就可以考慮使用RelativeLayout重新佈局。

(5)GridLayout表格佈局

GridLayout把整個容器劃分爲rows × columns個網格,每個網格可以放置一個組件。提供了setRowCount(int)和setColumnCount(int)方法來控制該網格的行和列的數量。

(6)AbsoluteLayout絕對佈局(過時)

(二)約束佈局ConstraintLayout

(1)特點

ConstraintLayout則是使用約束的方式來指定各個控件的位置和關係的,它有點類似於RelativeLayout,但遠比RelativeLayout要更強大。

(2)優點

a.非常適合使用可視化的方式來編寫界面(不適合用XML書寫)
b.有效地解決佈局嵌套過多的問題(複雜的佈局總會伴隨着多層的嵌套,而嵌套越多,程序的性能也就越差)

(3)基本操作

a.引入
b.添加約束/刪除約束
c.Inspector設置當前控件的所有屬性,如文本內容、顏色、點擊事件等等。
d.Guidelines
e.自動添加約束(Autoconnect、Inference)
詳見:Android新特性介紹,ConstraintLayout完全解析

(三)RelativeLayout和LinearLayout性能對比

(1)性能對比

問題核心在於:當RelativeLayout和LinearLayout分別作爲ViewGroup表達相同佈局時誰的繪製過程更快一點。
通過網上的很多實驗結果我們得之,兩者繪製同樣的界面時layout和draw的過程時間消耗相差無幾,關鍵在於measure過程RelativeLayout比LinearLayout慢了一些。故從RelativeLayout和LinearLayout的onMeasure過程來探索耗時問題的根源。

(2)源碼分析

1、RelativeLayout的onMeasure分析

@Override  
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
//...
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();
         applyHorizontalSizeRules(params, myWidth);
         measureChildHorizontal(child, params, myWidth, myHeight);
         if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
               offsetHorizontalAxis = true;
         }
    }
}
 
views = mSortedVerticalChildren;
count = views.length;
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) {
                 width = Math.max(width, params.mRight);
           }
           if (isWrapContentHeight) {
                 height = Math.max(height, params.mBottom);
           }
           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會根據2次排列的結果對子View各做一次measure。首先RelativeLayout中子View的排列方式是基於彼此的依賴關係,而這個依賴關係可能和Xml佈局中View的順序不同,在確定每個子View的位置的時候,需要先給所有的子View排序一下。又因爲RelativeLayout允許ViewB在橫向上依賴ViewA,ViewA在縱向上依賴B。所以需要橫向縱向分別進行一次排序測量。
同時需要注意的是View.measure()方法存在以下優化:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {
        ...
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
}

即如果我們或者我們的子View沒有要求強制刷新,而父View給子View傳入的值也沒有變化(也就是說子View的位置沒變化),就不會做無謂的測量。RelativeLayout在onMeasure中做橫向測量時,縱向的測量結果尚未完成,只好暫時使用myHeight傳入子View系統。這樣會導致在子View的高度和RelativeLayout的高度不相同時(設置了Margin),上述優化會失效,在View系統足夠複雜時,效率問題就會很明顯。

2、LinearLayout的onMeasure分析


@Override  
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  if (mOrientation == VERTICAL) {  
    measureVertical(widthMeasureSpec, heightMeasureSpec);  
  } else {  
    measureHorizontal(widthMeasureSpec, heightMeasureSpec);  
  }  
}  
//LinearLayout會先做一個簡單橫縱方向判斷,我們選擇縱向這種情況繼續分析
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
//...
for (int i = 0; i < count; ++i) {  
      final View child = getVirtualChildAt(i);  
      //... child爲空、Gone以及分界線的情況略去
     //累計權重
      LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();  
      totalWeight += lp.weight;  
      //計算
      if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {  
            //精確模式的情況下,子控件layout_height=0dp且weight大於0無法計算子控件的高度
            //但是可以先把margin值合入到總值中,後面根據剩餘空間及權值再重新計算對應的高度
            final int totalLength = mTotalLength;  
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);  
      } else {  
           if (lp.height == 0 && lp.weight > 0) {  
            //如果這個條件成立,就代表 heightMode不是精確測量以及wrap_conent模式
            //也就是說佈局是越小越好,你還想利用權值多分剩餘空間是不可能的,只設爲wrap_content模式
                 lp.height = LayoutParams.WRAP_CONTENT;  
           }  
  
          // 子控件測量
          measureChildBeforeLayout(child, i, widthMeasureSpec,0, heightMeasureSpec,totalWeight== 0 ? mTotalLength :0);         
          //獲取該子視圖最終的高度,並將這個高度添加到mTotalLength中
          final int childHeight = child.getMeasuredHeight();  
          final int totalLength = mTotalLength;  
          mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); 
          } 
        //...
}

源碼中已經標註了一些註釋,需要注意的是在每次對child測量完畢後,都會調用child.getMeasuredHeight()獲取該子視圖最終的高度,並將這個高度添加到mTotalLength中。但是getMeasuredHeight暫時避開了lp.weight>0且高度爲0子View,因爲後面會將把剩餘高度按weight分配給相應的子View。因此可以得出以下結論:
(1)如果我們在LinearLayout中不使用weight屬性,將只進行一次measure的過程。
(2)如果使用了weight屬性,LinearLayout在第一次測量時獲取所有子View的高度,之後再將剩餘高度根據weight加到weight>0的子View上。
由此可見,weight屬性對性能是有影響的。

(3)結論

1、RelativeLayout慢於LinearLayout是因爲它會讓子View調用2次measure過程,而LinearLayout只需一次,但是有weight屬性存在時,LinearLayout也需要兩次measure。
2、RelativeLayout的子View如果高度和RelativeLayout不同,會導致RelativeLayout在onMeasure()方法中做橫向測量時,縱向的測量結果尚未完成,只好暫時使用自己的高度傳入子View系統。而父View給子View傳入的值也沒有變化就不會做無謂的測量的優化會失效,解決辦法就是可以使用padding代替margin以優化此問題。
3、在不響應層級深度的情況下,使用Linearlayout而不是RelativeLayout。
4、新建一個Android項目SDK會爲我們自動生成的avtivity_main.xml佈局文件,然後它的根節點默認是RelativeLayout?
DecorView的層級深度已知且固定的,上面一個標題欄,下面一個內容欄,採用RelativeLayout並不會降低層級深度,因此這種情況下使用LinearLayout效率更高。
5、作爲頂級View的DecorView就是個垂直方向的LinearLayout,上面是標題欄,下面是內容欄,我們常用的setContentView()方法就是給內容欄設置佈局?
爲開發者默認新建RelativeLayout是希望開發者能採用儘量少的View層級,很多效果是需要多層LinearLayout的嵌套,這必然不如一層的RelativeLayout性能更好。因此我們應該儘量減少佈局嵌套,減少層級結構,使用比如viewStub,include等技巧。可以進行較大的佈局優化。

(四)佈局優化的方案

(1)Android系統是如何處理UI組件的更新操作的

1、Android需要把XML佈局文件轉換成GPU能夠識別並繪製的對象。這個操作是在DisplayList的幫助下完成的。DisplayList持有所有將要交給GPU繪製到屏幕上的數據信息。
2、CPU負責把UI組件計算成Polygons,Texture紋理,然後交給GPU進行柵格化渲染。
3、GPU進行柵格化渲染。(柵格化:把組件拆分到不同的像素上進行顯示)
4、硬件展示在屏幕上。
需要注意的是:任何時候View中的繪製內容發生變化時,都會重新執行創建DisplayList,渲染DisplayList,更新到屏幕上等一系列操作。這個流程的表現性能取決於View的複雜程度,View的狀態變化以及渲染管道的執行性能
Overdraw(過度繪製):描述的是屏幕上的某個像素在同一幀的時間內被繪製了多次。在多層次的UI結構裏面,如果不可見的UI也在做繪製的操作,就會導致某些像素區域被繪製了多次,浪費大量的CPU以及GPU資源。(可以通過開發者選項,打開Show GPU Overdraw的選項,觀察UI上的Overdraw情況)
所以我們需要儘量減少Overdraw。

(2)Android佈局優化思想

減少層級,越簡單越好,減少overdraw,就能更好的突出性能

(3)Android佈局優化常用方法

1、善用相對佈局RelativeLayout

在RelativeLayout和LinearLayout同時能夠滿足需求時,儘量使用RelativeLayout,這一點可以從我們MainActivity默認佈局就可以看出,默認是RelativeLayout,因爲可以通過扁平的RelativeLayout降低LinearLayout嵌套所產生布局樹的層級。
Android提供了幾種方便的佈局管理器,大多數時候,你只需要這些佈局的一部分基本特性去實現UI。 一般情況下用LinearLayout的時候總會比RelativeLayout多一個View的層級。而每次往應用裏面增加一個View,或者增加一個佈局管理器的時候,都會增加運行時對系統的消耗,因此這樣就會導致界面初始化、佈局、繪製的過程變慢。
同一個佈局LinearLayout與RelativeLayout用Hierarchy View查看層級樹:
LinearLayout
在這裏插入圖片描述
RelativeLayout
在這裏插入圖片描述
很明顯的可以看出來RelativeLayout比LinearLayout少了一個層級,當然渲染的時間也是大大減少了

2、使用抽象佈局標籤include、merge、ViewStub

3.1)include標籤

include標籤常用於將佈局中的公共部分提取出來,比如我們要在activity_main.xml中需要上述LinearLayout的數據,那麼就可以直接include進去了。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.jared.layoutoptimise.MainActivity">
 
    <include layout="@layout/item_test_linear_layout" />
     
</RelativeLayout>

在這裏插入圖片描述

3.2)merge標籤

merge標籤是作爲include標籤的一種輔助擴展來使用,它的主要作用是爲了防止在引用佈局文件時產生多餘的佈局嵌套,Android渲染需要消耗時間,佈局越複雜,性能就越差。如上述include標籤引入了之前的LinearLayout之後導致了界面多了一個層級。這個時候用merge的話,就可以減少一個層級。

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <ImageView
        android:id="@+id/iv_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@mipmap/ic_launcher" />
 
    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="16dp"
        android:layout_toRightOf="@+id/iv_image"
        android:text="這個是MergeLayout"
        android:textSize="16sp" />
 
    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tv_title"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="10dp"
        android:layout_toRightOf="@+id/iv_image"
        android:text="這個是MergeLayout,這個是MergeLayout"
        android:textSize="12sp" />
 
</merge>

activity_main就可以直接include了

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.jared.layoutoptimise.MainActivity">
 
    <include layout="@layout/item_merge_layout" />
 
</RelativeLayout>

在這裏插入圖片描述

3.3)ViewStub標籤

viewstub是view的子類。他是一個輕量級View, 隱藏的,沒有尺寸的View。他可以用來在程序運行時簡單的填充佈局文件。

(4)Android最新的佈局方式ConstaintLayout

ConstraintLayout允許你在不適用任何嵌套的情況下創建大型而又複雜的佈局。它與RelativeLayout非常相似,所有的view都依賴於兄弟控件和父控件的相對關係。但是,ConstraintLayout比RelativeLayout更加靈活。目前在AndroidStudio中使用也十分方便。

(五)檢測佈局深度的方法

(1)Dump UI Hierarchy for UI Atomator,分析UI層級

從Android Studio中啓動Android Device Monitor: Tools -> Android -> Android Device Monitor. 使用方法很簡單,如下圖
在這裏插入圖片描述

(2)HierachyViewer

依次點擊菜單Tools>Android>Android Device Monitor,如下圖:
在這裏插入圖片描述
或者直接點擊菜單下面,問號旁邊的圖標,如下圖:
在這裏插入圖片描述
啓動Android Device Monitor成功之後,在新的的窗口中點擊切換視圖圖標,選擇Hierarchy Viewe,如下圖:
在這裏插入圖片描述

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