android開發佈局優化—include、merge、viewstub原始碼分析總結
儘管Android SDK為開發者提供了各種各樣的小部件來提供小型且可重用的互動元素,但開發者可能仍然需要重新使用特殊佈局的較大元件。這就是我們所謂的佈局複用。要有效地重新使用完整的佈局,可以使用和標籤在當前佈局中嵌入另一個佈局。
重複使用佈局非常有用,因為它允許開發者建立可重用的複雜佈局。例如,是/否按鈕面板,或帶有說明文字的自定義進度欄。這也意味著您的應用程式的任何元素都可以在多個佈局中提取,分別管理,然後包含在每個佈局中。因此,儘管可以通過編寫自定義來建立各個UI元件,但View通過重新使用佈局檔案,可以更輕鬆地完成此任務。
include標籤
include標籤適用於當某個佈局檔案載入的檢視需要在不同的頁面重複使用時,例如,專案開發過程中最常見的每個頁面標題欄的封裝
lbjfan_title.xml
<?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="48dp"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_gravity="center_vertical"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity ="center_vertical"/>
</LinearLayout>
使用include標籤複用:
<?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">
<include layout="@layout/lbjfan_title" />
</LinearLayout>
注意:如果include標籤設定id,會覆蓋掉include載入的佈局中根節點設定的id,使用findViewByid時要注意空指標異常,更具體一點如果我們給lbjfan_title.xml中的根節點LinearLayout設定了id,同時給引用該佈局的include設定了id,則LinearLayout的id會被覆蓋掉,此時如果使用findViewById就會出現空指標異常。
View的解析最終都是通過LayoutInflater類中的rInflate方法解析的,該方法原始碼:
/**
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
譯文:遞迴解析XML檔案並例項化View和子View,最後呼叫onFinishInflate方法完成載入
*/
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
/.......略........../
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);
//解析include標籤
} 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)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
/.........略........../
}
parseInclude方法關鍵原始碼
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
final XmlResourceParser childParser = context.getResources().getLayout(layout);
try {
final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
while ((type = childParser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty.
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(childParser.getPositionDescription() +
": No start tag found!");
}
final String childName = childParser.getName();
//include肯定不是merge標籤
if (TAG_MERGE.equals(childName)) {
// The <merge> tag doesn't support android:theme, so
// nothing special to do here.
rInflate(childParser, parent, context, childAttrs, false);
} else {
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
final ViewGroup group = (ViewGroup) parent;
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.Include);
//獲取include標籤的id
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
a.recycle();
ViewGroup.LayoutParams params = null;
try {
params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
if (params == null) {
params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);
rInflateChildren(childParser, view, childAttrs, true);
if (id != View.NO_ID) {
view.setId(id);
}
//將View新增到include的parent
group.addView(view);
}
} finally {
childParser.close();
}
}
} else {
throw new InflateException("<include /> can only be used inside of a ViewGroup");
}
}
Merge標籤
官方文件中這樣描述Merge標籤:The tag helps eliminate redundant view groups in your view hierarchy when including one layout within another. For example, if your main layout is a vertical LinearLayout in which two consecutive views can be re-used in multiple layouts, then the re-usable layout in which you place the two views requires its own root view. However, using another LinearLayout as the root for the re-usable layout would result in a vertical LinearLayout inside a vertical LinearLayout. The nested LinearLayout serves no real purpose other than to slow down your UI performance.
譯文:將標籤包含在另一個佈局中時,該標籤有助於消除檢視層次結構中的冗餘檢視組。例如,如果主佈局是可以在多個佈局中重用的垂直佈局,則放置可重用佈局需要具有其自己的根檢視。但是,使用另一個LinearLayout作為可重用佈局的根目錄將導致垂直LinearLayout內部巢狀垂直LinearLayout。巢狀垂直的LinearLayout除了減慢你的UI效能,沒有任何益處。
繼續來看看inflate方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
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標籤,呼叫rinflate方法
if (TAG_MERGE.equals(name)) {
//使用merger標籤時常出現的異常
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//此時解析時傳入的root已經不是merge,而是merge對應的根標籤
rInflate(parser, root, inflaterContext, attrs, false);
} else {
}
}
}
return result;
}
}
rInflate方法:
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
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)) {
throw new InflateException("<merge /> must be the root element");
} else {
//由於root不是merge,將會執行下面程式碼
final View view = createViewFromTag(parent, name, context, attrs);
//獲取merger對應的ViewGroup
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//解析子View
rInflateChildren(parser, view, attrs, true);
//將子View新增到ViewGroup中
viewGroup.addView(view, params);
}
}
}
可以看到,merge標籤解析時是將merge標籤中的元素直接新增到對應的ViewGroup中,因此使用merge標籤將極大減少ViewGroup的巢狀。
ViewStub標籤
官方文件中這樣描述標籤:ViewStub is a lightweight view with no dimension and doesn’t draw anything or participate in the layout. As such, it’s cheap to inflate and cheap to leave in a view hierarchy. Each ViewStub simply needs to include the android:layout attribute to specify the layout to inflate.
譯文:ViewStub就是一個寬高都為0的View,預設不可見,只有通過呼叫setVisibility方法或者Inflate方法才會將其要裝載的目標佈局給加載出來,從而達到延遲載入的效果,這個要被載入的佈局通過android:layout屬性來設定。
使用:
1.直接在佈局檔案中設定layout屬性,然後在程式碼中通過inflate方法來顯示
2.在佈局檔案中先通過setLayoutResource方法來載入佈局,然後通過inflate顯示
關鍵原始碼分析:
構造方法:
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
//獲取layout屬性載入的佈局Id
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
//獲取id,預設值NO_ID
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle();
//設定不可見
setVisibility(GONE);
//設定不繪製,因此ViewStub可以理解為佔位符
setWillNotDraw(true);
onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//寬高均為0
setMeasuredDimension(0, 0);
}
setLayoutResource方法
public void setLayoutResource(@LayoutRes int layoutResource) {
//等價於佈局檔案中設定的layout屬性
mLayoutResource = layoutResource;
}
inflate方法
public View inflate() {
//獲取ViewStub的parent(ViewGroup)
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
final View view =
//呼叫inflateViewNoAdd方法通過mLayoutResource獲取到顯示的View
inflateViewNoAdd(parent);
replaceSelfWithView(view, parent);
//使用弱引用儲存ViewStub需要載入顯示的View,setVisibility時會用到
mInflatedViewRef = new WeakReference<>(view);
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}
return view;
} else {
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
inflateViewNoAdd方法
private View inflateViewNoAdd(ViewGroup parent) {
final LayoutInflater factory;
if (mInflater != null) {
factory = mInflater;
} else {
factory = LayoutInflater.from(mContext);
}
final View view = factory.inflate(mLayoutResource, parent, false);
//將ViewStub的id設定給需要載入的View
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
return view;
}
replaceSelfWithView方法
private void replaceSelfWithView(View view, ViewGroup parent) {
final int index = parent.indexOfChild(this);
//移除自己
parent.removeViewInLayout(this);
獲取ViewStub對應的Param
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
//將需要顯示的View新增到ViewStub在ViewGroup中對應的位置
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
setVisibility方法
public void setVisibility(int visibility) {
//mInflatedViewRef在inflate方法中建立,說明已經呼叫過
if (mInflatedViewRef != null) {
View view = mInflatedViewRef.get();
if (view != null) {
view.setVisibility(visibility);
} else {
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {
super.setVisibility(visibility);
if (visibility == VISIBLE || visibility == INVISIBLE) {
//沒有呼叫過直接呼叫inlate方法
inflate();
}
}
}
可以看見ViewStub載入佈局時都是通過ViewStub方法,然後使用弱引用換存,需要設定可見性時,如果弱引用中存在載入的View則直接設定,否咋通過inflate方法顯示。除此之外,使用ViewStub標籤時,需要注意一下幾點:
1.對ViewStub的inflate操作只能進行一次,因為inflate的時候是將其指向的佈局檔案解析inflate並替換掉當前ViewStub本身(由此體現出了ViewStub“佔位符”性質),一旦替換後,此時原來的佈局檔案中就沒有ViewStub控制元件了,因此,如果多次對ViewStub進行infalte,getParent()方法獲得的Parent就為空,然後出現錯誤資訊:ViewStub must have a non-null ViewGroup viewParent。
2.ViewStub中是否設定了inflatedId,如果設定了則需要通過inflatedId來查詢目標佈局的根元素。
2.ViewStub佔位符:當一個佈局載入一次時可以使用,一開始不顯示,寬高都是0,通過inflate和setVisibility可以讓其顯示,只能inflate一次,原因是inflate時會解析自己,使用Layout的元素代替自己,同時也需注意id設定,會覆蓋根節點的id,必須有一個ViewGroup否則會丟擲異常,同時還需注意載入Layout時,使用的LayoutParam是ViewStub的LayoutParam,原始碼都可見。