Android佈局優化之merge標籤詳解
我們都知道View的繪製流程需要經歷measure、layout、draw這個三個過程,如果佈局巢狀層次比較深的話,每一步都需要進行遍歷所有子View進行對應的measure、layout、draw過程,由此就會降低繪製效率,巢狀越多,耗時就越多;其實不光光只會影響view的繪製效率,同樣的也會影響xml佈局的解析效率。
針對上面的問題,Android為我們引入了merger標籤來降低佈局巢狀的問題。如果在佈局檔案中使用merge標籤,則需要在include標籤中引入,可以這麼說吧,merge標籤是include標籤一種輔助擴充套件。
merge_title_bar佈局
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="all">
<Button
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:textAllCaps="false"
android:text="返回"/>
<Button
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal ="true"
android:background="@android:color/white"
android:textAllCaps="false"
android:text="標題"/>
<Button
android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:text="分享"
android:textAllCaps="false"/>
</merge>
普通佈局title_bar
<?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="wrap_content"
tools:ignore="all">
<Button
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:layout_alignParentStart="true"
android:text="返回"/>
<Button
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:layout_centerInParent="true"
android:textAllCaps="false"
android:onClick="onClick"
android:text="標題"/>
<Button
android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:layout_alignParentEnd="true"
android:text="分享"/>
</RelativeLayout>
接下來我們在activity_ui_optimize佈局中分別引用merge_title_bar和title_bar這兩個佈局,看看之間的巢狀層級
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_root_view"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--引入普通佈局-->
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/title_bar"/>
<!--引入merge標籤佈局1-->
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/merge_title_bar"/>
<!--引入merge標籤佈局2-->
<RelativeLayout
android:id="@+id/rl_merge_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
layout="@layout/merge_title_bar"/>
</RelativeLayout>
</LinearLayout>
下面看看這個三個區域的view巢狀層級是如何的
紫色區域:就是使用普通佈局的效果
藍色區域:使用了merge標籤
紅色區域:同樣的使用了merge標籤,不同的是用RelativeLayout包裹了include標籤。
綠色區域:就是activity_ui_optimize佈局
從圖中可以看出紫色區域的button與藍色區域的button不在同一樣層級節點上,藍色區域的button少了一層巢狀,可見merge標籤可以減少view的巢狀;同樣的你會發現紫色區域與藍色區域的佈局位置有所不同,一個是橫排,另外一個是豎排,關於merge標籤元素佈局定位的問題,相信很多人在剛使用的時候可能會和我一樣產生疑問,merge標籤內元素的定位是受什麼影響,其實是由父佈局決定的,我們可以從原始碼的角度進行解釋:
首先我們要從Activity的setContentView方法講起
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
可見真正負責Activity的檢視展示並不是Activity本身,而是交給Window物件處理,Window是一個抽象類,PhoneWindow是它的唯一實現類,接下來進入PhoneWindow的setContentView方法看看
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
從setContentView方法可知,首先會通過installDecor方法建立DecorView,同時會把DecorView放入Window中,可見DecorView物件是Window頂級的檢視,緊接著會呼叫mLayoutInflater.inflate方法,下面進入LayoutInfater中的inflate方法探知
private static final String TAG_MERGE = "merge";
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
//得到節點名稱
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
//判斷是否是merge標籤
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
……省略
}
return result;
}
}
在inflate方法中,通過Pull解析,parser.getName()方法得到xml佈局節點的屬性名稱,緊接著會判斷節點的是不是merge標籤,如果是merge標籤,首先會判斷是否有父檢視root,如果沒有會丟擲InflateException異常,可見merge標籤的引入使用必須依賴於父檢視(根檢視),緊接著才會進入rInflate方法
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
//獲取節點的屬性名稱
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
//在xml檔案中,merge必須是根節點,
throw new InflateException("<merge /> must be the root element");
} else {
//建立merge標籤下的view
final View view = createViewFromTag(parent, name, context, attrs);
//得到父檢視佈局引數
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//構建view下的子view
rInflateChildren(parser, view, attrs, true);
//將view新增到父檢視在中
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
在rInflate方法中,定義一個while迴圈來遍歷解析佈局View每個子節點,得到每個節點屬性名稱,經過一系列的判斷,從中可知,在一個xml佈局檔案中,merge必須是根節點,在merge根標籤內不能再有其他的merge標籤。最後會根據merge根標籤內的每個標籤建立對應的view檢視,緊接著會得到父檢視的佈局引數,接著繪製父檢視下每個子view,最後將子View新增到父檢視中,到此可以發現merge標籤內容元素的定位最終還是由父檢視來決定的。
回過頭來,再去看看藍色區域與紅色區域的merge標籤佈局顯示,就會決定很清晰了,藍色區域下的button顯示為垂直豎排,是因為它的父檢視是垂直線性佈局LinearLayout(id:ll_root_view),而紅色區域下的button顯示水平橫排,是因為它的父檢視是相對佈局relativeLayout(id:rl_merge_view),在相對佈局中,為每個button設定了相應的定位屬性值。
這裡使用Android studio3.0的Layout Inspector工具,取代了之前的HierarchyViewer工具,下面就簡單介紹下使用方法:
1、首先找到Layout Inspector,可以通過搜尋方式找到
2、點選進入Layout Inspector ,Choose Process 對話方塊中,選擇您想要檢查的應用程序,然後點選 OK
3、在Select Window對話方塊中,選擇對應的Activity
4、到此基本就可以展示出來了
詳細的介紹可以參考官方介紹Layout Inspector,如果要使用之前的HierarchyViewer工具,需要到sdk目錄下tools下找到monitor.bat這個檔案點選就可以開啟,包括DDMS也在其中。
參考: