在 Android 開發(fā)的過程中,對(duì)性能進(jìn)行優(yōu)化是必不可少的一步。而在布局上的優(yōu)化并不像其他優(yōu)化方式那么復(fù)雜,通過 Android SDK 提供的 Hierarchy Viewer 可以很直接地看到冗余的層級(jí),去除這些多余的層級(jí)將使我們的UI變得更加流暢。
如何使用 Hierarchy Viewer ?
在 Android studio 菜單欄上,點(diǎn)擊 Tools --> Android --> Android Device Monitor
在 Android Device Monitor 菜單欄上,點(diǎn)擊 Window --> Open perspective --> Hierarchy Viewer 即可。
然而,一般在真機(jī)上無法使用 Hierarchy Viewer,只能在運(yùn)行開發(fā)版 Android 系統(tǒng)的設(shè)備進(jìn)行交互(一般來說,使用模擬器上即可),著實(shí)有點(diǎn)遺憾。但是,也可以在真機(jī)上通過 root 等一系列操作之后,便可以使用 Hierarchy Viewer 。這部分教程就需要大家在網(wǎng)上另行查閱了。
下面是一些常用的布局優(yōu)化方式:
一、include 布局
當(dāng)多個(gè)頁面公用了一些UI組件時(shí),就可以使用 include 布局。Android 提供了 include 標(biāo)簽,讓我們可以將子布局引入到一個(gè)布局文件中,這樣一來,公用布局就可以獨(dú)立成為一個(gè)布局 xml,其他頁面只要 include 引用這個(gè)布局xml即可。
下面以一個(gè)自定義標(biāo)題欄為例
<?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>
將該標(biāo)題欄引入一個(gè)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 標(biāo)簽引入 common_title 這個(gè)布局(注意:include 布局中指定布局 xml 是使用 layout 屬性,而不是 android:layout 屬性)。這樣一來,就復(fù)用了 common_title 的標(biāo)題欄效果,不必在每個(gè)頁面中重復(fù)定義標(biāo)題欄布局。大大降低了我們維護(hù) xml 的成本,也提升了代碼復(fù)用率。
include 標(biāo)簽的原理:
在解析 xml 布局時(shí),如果檢測(cè)到 include 標(biāo)簽,那么就直接把該布局下的根視圖添加到 include 所在的父視圖中。對(duì)于布局 xml 的解析最終都會(huì)調(diào)用到 LayoutInflater 的 inflate 方法,該方法最終又會(huì)調(diào)用 rInflate 方法。這個(gè)方法就是遍歷 xml 中的所有元素,然后逐個(gè)進(jìn)行解析。
如何獲取 include 布局里的控件?
以上面例子為例,獲取標(biāo)題欄內(nèi)的 Textview
private TextView mTextView;
mTextView = findViewById(R.id.top_title).findViewById(R.id.title_textview);
二、merge 標(biāo)簽
merge 標(biāo)簽適用于子布局的根視圖與它的父視圖是同一類型。它的作用是合并UI布局,降低UI布局的嵌套層次。使用場(chǎng)景是存在多層使用同一種布局類的嵌套視圖,這種情況下用merge標(biāo)簽作為子視圖的頂級(jí)視圖來解決多余的層級(jí)。
如上圖,child_view 與 parent_view 都是FrameLayout類型,那么 child_view 下的兩個(gè)控件可以直接使用 parent_view 來布局,這樣就可以去除 child_view 這個(gè)層級(jí)。
<?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>
上面這個(gè)布局相當(dāng)于是 child_view,而 Activity 內(nèi)容視圖的頂層布局也 FrameLayout,因此產(chǎn)生了視圖冗余,可以用 merge 標(biāo)簽去掉這層冗余。
下面是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>
<!-以下就是開發(fā)人員設(shè)置的布局所填充的位置-->
<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() 函數(shù)中。在 inflate() 函數(shù)中循環(huán)解析 xml 中 的 tag,如果解析到 merge 標(biāo)簽則會(huì)調(diào)用 rinflate 函數(shù)。
注意事項(xiàng):
merge 必須作為布局文件的根節(jié)點(diǎn)標(biāo)簽。
merge 并不是一個(gè) ViewGroup,也不是一個(gè) View,它相當(dāng)于聲明了一些視圖,等待被添加。
因?yàn)?merge 標(biāo)簽不是 View,所以對(duì) merge 標(biāo)簽設(shè)置的所有屬性都是無效的。
因?yàn)?merge 標(biāo)簽不是 View,所以在通過 LayoutInflate.inflate 方法渲染的時(shí)候,第二個(gè)參數(shù)必須指定一個(gè)父容器,且第三個(gè)參數(shù)必須為 true,也就是必須為 merge 下的視圖指定一個(gè)父親節(jié)點(diǎn)。
如果 Activity 的布局文件根節(jié)點(diǎn)是 FrameLayout,可以替換為 merge 標(biāo)簽,這樣,執(zhí)行 setContentView 之后,會(huì)減少一層 FrameLayout 節(jié)點(diǎn)。
三、ViewStub 視圖
ViewStub 是什么?
ViewStub 是一個(gè)不可見的和能在運(yùn)行期間延遲加載目標(biāo)視圖的、寬高都為0的 View (ViewStub 繼承 View)。當(dāng)對(duì)一個(gè) ViewStub 調(diào)用 inflate() 方法或設(shè)置它可見時(shí),系統(tǒng)會(huì)加載在 ViewStub 標(biāo)簽中指定的布局,然后將這個(gè)布局的根視圖添加到 ViewStub 的父視圖中。換句話說,在對(duì) ViewStub 調(diào)用 inflate() 方法或者設(shè)置 visible 之前,它是不占用布局空間和系統(tǒng)資源的,它只是為目標(biāo)視圖占了一個(gè)位置而已。
何時(shí)使用 ViewStub?
當(dāng)我們只需要在某些情況下才加載一些耗資源的布局時(shí),ViewStub 就成為我們實(shí)現(xiàn)這個(gè)功能的重要手段。
例如:有一個(gè)顯示九宮格圖片的 GridView 視圖,我們想根據(jù)網(wǎng)絡(luò)返回的數(shù)據(jù)來判斷是否加載該 GridView ,因?yàn)槟J(rèn)加載的話會(huì)造成資源浪費(fèi),系統(tǒng)加載成本較高。這時(shí)使用 ViewStub 標(biāo)簽就可以很方便實(shí)現(xiàn)延遲加載。
以下是一個(gè)小小的演示,在一個(gè) Activity 中使用 ViewStub 來動(dòng)態(tài)加載
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) {
//加載目標(biāo)視圖,不能多次調(diào)用,否則會(huì)引發(fā)異常
gvStub.inflate();
//gvStub.setVisibility(View.VISIBLE);
}
});
}
}
ViewStub 小結(jié)
當(dāng)用戶手動(dòng)調(diào)用 ViewStub 的 inflate 或者 setVisibility 函數(shù)(實(shí)際上也是調(diào)用 inflate 函數(shù))時(shí),會(huì)將 ViewStub 自身從父控件中移除,并且加載目標(biāo)布局,然后將目標(biāo)布局添加到 ViewStub 的父控件中,這樣就完成視圖的動(dòng)態(tài)替換,也就是延遲加載功能。
四、減少視圖樹層級(jí)
為什么要減少視圖層級(jí)?
每一個(gè)視圖在顯示時(shí)會(huì)經(jīng)歷測(cè)量、布局、繪制的過程,如果我們的布局中嵌套的視圖層次過多,就會(huì)造成額外測(cè)量、布局等工作,使得UI變得卡頓,影響用戶的使用體驗(yàn)。
例如一個(gè)簡(jiǎn)單的列表 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 來布局這個(gè) 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>
五、總結(jié)
在 Android UI 布局過程中,需要遵守的原則有以下幾點(diǎn):
盡量多使用 RelativeLayout ,不要使用絕對(duì)布局 AbsoluteLayout;
在 ListView 等列表組件中盡量避免使用 LinearLayout 的 layout_weight 屬性(子視圖使用了 layout_weight 屬性的 LinearLayout 會(huì)對(duì)它的子視圖進(jìn)行兩次測(cè)量。);
將可復(fù)用的組件抽取出來并通過 <include/> 標(biāo)簽使用;
使用 <ViewStub/> 標(biāo)簽來加載一些不常用的布局;
使用 <merge/> 標(biāo)簽來減少布局的嵌套層次。