前言
最近部門內對View的加載做了一波優化操作,主要是針對一些在特定時機才會顯示在頁面上的View進行ViewStub化改造。優化后,頁面的啟動速度確實得到了提升。這確實是一個項目優化的入手點。在做完這波優化后,我偶然間腦海中閃過一個念頭,就是我們項目中的自定義View的層級是否存在可改進的地方,于是我在閑暇之余自己寫了個小demo,并閱讀了下LayoutInflater的一些源碼,發現果然我們項目中的大部分組合自定義View都存在一層多余的層級。
“多一層外套”的組合自定義View
組合自定義View在日常開發中的引用場景應該還是相對比較多的,下面我粘上一個簡單的組合自定義View的實現:
/**
* 簡單的一個組合自定義View,在它的中央會顯示一個TextView文本
* 此案例的布局填充方式也是目前網上包括一些書籍描述的填充方式
*/
public class GroupNormalView extends RelativeLayout {
public GroupNormalView(Context context) {
this(context, null);
}
public GroupNormalView(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.group_normal_view, this, true);
}
}
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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="25sp"
android:textColor="#ff0000"
android:text="Normal" />
</RelativeLayout>
然后,我在MainActivity的布局中引入這個自定義View。
<?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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="activity.MainActivity">
<view.GroupNormalView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
運行demo,當頁面打開后,我們使用AS自帶的Layout Inspector工具來查看當前頁面的布局組成。如下圖:
當前頁面的布局內容中僅含一個GroupNormalView,在它的里面包含一層相對布局,然后還有一個TextView。看最初定義GroupNormalView的時候我直接讓它繼承了RelativeLayout,也就是說GroupNormalView本身就含有RelativeLayout的全部布局屬性了,那么現在有沒有覺著上圖中第二個箭頭所指的RelativeLayout有些多余呢?事實證明,真的很多余。下面就讓我們把這多余的一層布局給干掉。
“比基尼裝扮”的組合自定義View
其實,干掉上面所說的那一層多余的布局的方式有兩種。一種是在自定義控件的xml文件中,直接把布局根標簽改成merge。但是這種方式有一個缺點,就是在開發過程中我們無法實時的在右側preview中瀏覽到正確的布局排列樣式。因此我更傾向于接下來要說的這種方式,就是直接在布局文件中引用自定義View。
代碼實現:
/**
* 組合自定義View,View直接在xml布局中被引用。如需遍歷布局中的控件,可重寫onFinishInflate方法
*/
public class GroupBetterView extends RelativeLayout {
private TextView betterTxt;
public GroupBetterView(Context context) {
super(context);
}
public GroupBetterView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
betterTxt = findViewById(R.id.txt_better);
}
}
xml布局(group_better_view):
<?xml version="1.0" encoding="utf-8"?>
<view.GroupBetterView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/txt_better"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="25sp"
android:textColor="#ff0000"
android:text="Better" />
</view.GroupBetterView>
然后,在MainActivity的布局中使用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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="activity.MainActivity">
<include layout="@layout/group_better_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
同樣,運行demo,打開Layout Inspector工具,結果如下圖:
看到了吧,實現了一模一樣的效果,但是現在比之前少了一層相對布局。我們都知道,在View的繪制過程中,多一層布局的話它的繪制時間肯定就更長。試想下,如果一個頁面存在上千個View的話,如果我們采用第一種方式實現,無非是會增加頁面整體的繪制時間的。因此以后再使用組合自定義View的時候,要優先選擇第二種方式啊。
關于inflate()方法:
現在,我們只是從結果上證實了,確實第二種方式減少了一層層級,但是我們還不知道為什么會是這樣子呢。下面,我們就再來看下LayoutInflater的源碼吧。點進inflate方法,一路行走至483行:
我們可以看到,如果發現xml布局中根標簽的名字為merge的話,會進入if條件中,而不是merge的話會進入else中,而第一種方式我們布局中的根標簽為RelativeLayout,因此我們繼續看else中的代碼,第492行,在這里創建了一個temp對象,上面的注釋寫的很清楚,這個對象就是在xml布局中的根View。然后繼續往下走,至524行如下:
看到了吧,當root(也就是當前自定義View)不為空,并且attachRoot(我們在調用inflate方法時傳入的第三個參數)為true時,是將xml中的根View當做一個子View添加進了自定義View中,這下知道為什么第一種方式比第二種方式多了一層布局了吧!
總結
本文算是一個自定義View上使用的一個小優化,希望能夠對大家有所幫助。