在 Android 開發的過程中,對性能進行優化是必不可少的一步。而在布局上的優化并不像其他優化方式那么復雜,通過 Android SDK 提供的 Hierarchy Viewer 可以很直接地看到冗余的層級,去除這些多余的層級將使我們的UI變得更加流暢。
如何使用 Hierarchy Viewer ?
在 Android studio 菜單欄上,點擊 Tools --> Android --> Android Device Monitor
在 Android Device Monitor 菜單欄上,點擊 Window --> Open perspective --> Hierarchy Viewer 即可。
然而,一般在真機上無法使用 Hierarchy Viewer,只能在運行開發版 Android 系統的設備進行交互(一般來說,使用模擬器上即可),著實有點遺憾。但是,也可以在真機上通過 root 等一系列操作之后,便可以使用 Hierarchy Viewer 。這部分教程就需要大家在網上另行查閱了。
下面是一些常用的布局優化方式:
一、include 布局
當多個頁面公用了一些UI組件時,就可以使用 include 布局。Android 提供了 include 標簽,讓我們可以將子布局引入到一個布局文件中,這樣一來,公用布局就可以獨立成為一個布局 xml,其他頁面只要 include 引用這個布局xml即可。
下面以一個自定義標題欄為例
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageButton
android:id="@+id/title_back_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/back" />
<TextView
android:id="@+id/title_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Title" />
</RelativeLayout>
將該標題欄引入一個Activity的xml中
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.jerry.myapplication.MainActivity">
<include
android:id="@+id/top_title"
layout="@layout/common_title" />
<TextView
android:id="@+id/username_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
通過 include 標簽引入 common_title 這個布局(注意:include 布局中指定布局 xml 是使用 layout 屬性,而不是 android:layout 屬性)。這樣一來,就復用了 common_title 的標題欄效果,不必在每個頁面中重復定義標題欄布局。大大降低了我們維護 xml 的成本,也提升了代碼復用率。
include 標簽的原理:
在解析 xml 布局時,如果檢測到 include 標簽,那么就直接把該布局下的根視圖添加到 include 所在的父視圖中。對于布局 xml 的解析最終都會調用到 LayoutInflater 的 inflate 方法,該方法最終又會調用 rInflate 方法。這個方法就是遍歷 xml 中的所有元素,然后逐個進行解析。
如何獲取 include 布局里的控件?
以上面例子為例,獲取標題欄內的 Textview
private TextView mTextView;
mTextView = findViewById(R.id.top_title).findViewById(R.id.title_textview);
二、merge 標簽
merge 標簽適用于子布局的根視圖與它的父視圖是同一類型。它的作用是合并UI布局,降低UI布局的嵌套層次。使用場景是存在多層使用同一種布局類的嵌套視圖,這種情況下用merge標簽作為子視圖的頂級視圖來解決多余的層級。
如上圖,child_view 與 parent_view 都是FrameLayout類型,那么 child_view 下的兩個控件可以直接使用 parent_view 來布局,這樣就可以去除 child_view 這個層級。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
上面這個布局相當于是 child_view,而 Activity 內容視圖的頂層布局也 FrameLayout,因此產生了視圖冗余,可以用 merge 標簽去掉這層冗余。
下面是screen_title.xml文件的源代碼:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:fitsSystemWindows="true">
<!-- Popout bar for action modes -->
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="?android:attr/windowTitleSize"
style="?android:attr/windowTitleBackgroundStyle">
<TextView android:id="@android:id/title"
style="?android:attr/windowTitleStyle"
android:background="@null"
android:fadingEdge="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<!-以下就是開發人員設置的布局所填充的位置-->
<FrameLayout android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
<?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">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</merge>
與 include 一樣,merge 的解析也在 LayoutInflater 的 inflate() 函數中。在 inflate() 函數中循環解析 xml 中 的 tag,如果解析到 merge 標簽則會調用 rinflate 函數。
注意事項:
merge 必須作為布局文件的根節點標簽。
merge 并不是一個 ViewGroup,也不是一個 View,它相當于聲明了一些視圖,等待被添加。
因為 merge 標簽不是 View,所以對 merge 標簽設置的所有屬性都是無效的。
因為 merge 標簽不是 View,所以在通過 LayoutInflate.inflate 方法渲染的時候,第二個參數必須指定一個父容器,且第三個參數必須為 true,也就是必須為 merge 下的視圖指定一個父親節點。
如果 Activity 的布局文件根節點是 FrameLayout,可以替換為 merge 標簽,這樣,執行 setContentView 之后,會減少一層 FrameLayout 節點。
三、ViewStub 視圖
ViewStub 是什么?
ViewStub 是一個不可見的和能在運行期間延遲加載目標視圖的、寬高都為0的 View (ViewStub 繼承 View)。當對一個 ViewStub 調用 inflate() 方法或設置它可見時,系統會加載在 ViewStub 標簽中指定的布局,然后將這個布局的根視圖添加到 ViewStub 的父視圖中。換句話說,在對 ViewStub 調用 inflate() 方法或者設置 visible 之前,它是不占用布局空間和系統資源的,它只是為目標視圖占了一個位置而已。
何時使用 ViewStub?
當我們只需要在某些情況下才加載一些耗資源的布局時,ViewStub 就成為我們實現這個功能的重要手段。
例如:有一個顯示九宮格圖片的 GridView 視圖,我們想根據網絡返回的數據來判斷是否加載該 GridView ,因為默認加載的話會造成資源浪費,系統加載成本較高。這時使用 ViewStub 標簽就可以很方便實現延遲加載。
以下是一個小小的演示,在一個 Activity 中使用 ViewStub 來動態加載
GridView。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/top_title"
layout="@layout/common_title" />
<Button
android:id="@+id/show_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="顯示圖片" />
<ViewStub
android:id="@+id/comment_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/layout_image_gv" />
</RelativeLayout>
布局的最后使用 ViewStub 來加載 layout_image_gv.xml 布局,下面是 layout_image_gv.xml 的代碼:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#aaaaaa">
<TextView
android:id="@+id/image_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="顯示九宮格GridView" />
<GridView
android:id="@+id/image_gc"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/image_desc"
android:numColumns="3">
</GridView>
</RelativeLayout>
public class StubLayoutActivity extends AppCompatActivity {
private ViewStub gvStub;
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_activity_stub);
gvStub = findViewById(R.id.comment_stub);
mButton = findViewById(R.id.show_btn);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//加載目標視圖,不能多次調用,否則會引發異常
gvStub.inflate();
//gvStub.setVisibility(View.VISIBLE);
}
});
}
}
ViewStub 小結
當用戶手動調用 ViewStub 的 inflate 或者 setVisibility 函數(實際上也是調用 inflate 函數)時,會將 ViewStub 自身從父控件中移除,并且加載目標布局,然后將目標布局添加到 ViewStub 的父控件中,這樣就完成視圖的動態替換,也就是延遲加載功能。
四、減少視圖樹層級
為什么要減少視圖層級?
每一個視圖在顯示時會經歷測量、布局、繪制的過程,如果我們的布局中嵌套的視圖層次過多,就會造成額外測量、布局等工作,使得UI變得卡頓,影響用戶的使用體驗。
例如一個簡單的列表 Item
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
而使用 RelativeLayout 來布局這個 item view 就可以減少一層 LinearLayout 的渲染。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/profile_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/name_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/profile_image" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/profile_image" />
</RelativeLayout>
五、總結
在 Android UI 布局過程中,需要遵守的原則有以下幾點:
盡量多使用 RelativeLayout ,不要使用絕對布局 AbsoluteLayout;
在 ListView 等列表組件中盡量避免使用 LinearLayout 的 layout_weight 屬性(子視圖使用了 layout_weight 屬性的 LinearLayout 會對它的子視圖進行兩次測量。);
將可復用的組件抽取出來并通過 <include/> 標簽使用;
使用 <ViewStub/> 標簽來加載一些不常用的布局;
使用 <merge/> 標簽來減少布局的嵌套層次。