經(jīng)常可能會(huì)被問(wèn)到形如以下的問(wèn)題:
1.為什么在Android中使用ViewStub/merge/include可以幫我們完成布局優(yōu)化?
2.為什么ViewStub可以做到不占用布局資源/懶加載?
3.merge標(biāo)簽為什么能做到減少嵌套?
4.阿森納
5.為什么ViewStub多次調(diào)用inflate的報(bào)錯(cuò)?
....
目錄
1.ViewStub初始化解析
2.ViewStub使用
3.ViewStub相關(guān)問(wèn)題
inflate源碼解析
https://mp.weixin.qq.com/s/xHUeKc0xL2Si4-PJOIBVWQ
4.include標(biāo)簽使用
5.include標(biāo)簽解析
6.merge標(biāo)簽使用
7.merge標(biāo)簽解析
8.merge標(biāo)簽問(wèn)題
9.參考資料
ViewStub初始化解析
ViewStub是View類(lèi)的子類(lèi),其構(gòu)造函數(shù)如下
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
super(context);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ViewStub, defStyleAttr, defStyleRes);
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle();
setVisibility(GONE);
setWillNotDraw(true);
}
構(gòu)造函數(shù)先調(diào)用了一次setVisibility()和setWillNotDraw()方法;
ViewStub復(fù)寫(xiě)了父類(lèi)的setVisibility方法,在沒(méi)有inflate之前,ViewStub的mInflatedViewRef是null,visibility為gone,所以這里是調(diào)用父類(lèi)里的setVisibility(visibility)方法,完成flag的設(shè)置,可以簡(jiǎn)單的理解為:不可見(jiàn)
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
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) {
inflate();
}
}
}
然后直接調(diào)用的父類(lèi)setWillNotDraw方法,也就是告訴view:"viewStub暫時(shí)不繪制"
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
onMeasure方法直接設(shè)置寬高為0
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(0, 0);
}
以上是ViewStub的初始化過(guò)程做的事,回答了開(kāi)頭的第二個(gè)問(wèn)題,
『ViewStub是一個(gè)不可見(jiàn)的,不繪制,大小為0的視圖。』
ViewStub使用
當(dāng)業(yè)務(wù)需要顯示ViewStub里的布局時(shí),調(diào)用setVisibility方法,可見(jiàn)性設(shè)為true
ViewStub viewStub = findViewById(R.id.viewStub);
viewStub.setVisibility(View.VISIBLE);
實(shí)際上是調(diào)用了ViewStub的私有inflate方法
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {//#1
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
final View view = inflateViewNoAdd(parent);//#2
replaceSelfWithView(view, parent);//#3
mInflatedViewRef = new WeakReference<>(view);//#4
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);//#5
}
return view;
} else {
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
1.調(diào)用getParent方法拿父View,父View為空(為空怎么添加嘛)或者父view不是viewGroup(不是VG怎么添加?)拋出異常;
2.父view檢測(cè)正常后,調(diào)用inflateViewNoAdd方法,其本質(zhì)上是從layout文件里生成一個(gè)view
final View view = factory.inflate(mLayoutResource, parent, false);
這也是為什么ViewStub是懶加載的原因,只有當(dāng)ViewStub被要求setVisible(Visible)的時(shí)候才初始化該view。
看到這個(gè)inflate方法的第三個(gè)參數(shù)attachToRoot為false,這個(gè)也解釋了為什么ViewStub里使用的layout根標(biāo)簽不能為merge標(biāo)簽,報(bào)錯(cuò)堆棧更加明了:
android.view.InflateException: Binary XML file line #2: <merge /> can be used only with a valid ViewGroup root and attachToRoot=true
Caused by: android.view.InflateException: <merge /> can be used only with a valid ViewGroup root and attachToRoot=true
at android.view.LayoutInflater.inflate(LayoutInflater.java:485)
at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
at android.view.ViewStub.inflateViewNoAdd(ViewStub.java:269)
at android.view.ViewStub.inflate(ViewStub.java:302)
at android.view.ViewStub.setVisibility(ViewStub.java:247)
3.調(diào)用replaceSelfWithView,從父view中找到自己ViewStub,刪除自己這個(gè)節(jié)點(diǎn),然后把生產(chǎn)的view加到這個(gè)位置,完成replace;
private void replaceSelfWithView(View view, ViewGroup parent) {
final int index = parent.indexOfChild(this);
parent.removeViewInLayout(this);
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
這個(gè)就回答了為什么多次inflate會(huì)報(bào)空指針錯(cuò)誤,這里已經(jīng)把自己刪除了,findViewById的時(shí)候就找不到了。
4.初始化一個(gè)弱引用,把view傳進(jìn)去;
mInflatedViewRef的作用呢,在后續(xù)再次調(diào)用setVisibility的時(shí)候,從mInflatedViewRef取出view,就不用再初始化view了。
5.回調(diào)OnInflateListener的inflate方法;
該回調(diào)的作用見(jiàn)注釋
Listener used to receive a notification after a ViewStub has
successfully inflated its layout resource.
linstener是ViewStubProxy代理類(lèi)里進(jìn)行設(shè)置
這個(gè)代理的作用???????(埋坑,先下班)
這個(gè)代理在哪里初始化???
ViewStub相關(guān)問(wèn)題
1.為什么ViewStub能優(yōu)化布局性能?
因?yàn)閂iewStub是一個(gè)不可見(jiàn),不繪制,0大小的View,可以做到懶加載。
2.ViewStub懶加載的原理是?
它的inflate過(guò)程是在初次要求可見(jiàn)的時(shí)候進(jìn)行的,也就是按需加載。
3.ViewStub的layout能用merge做根標(biāo)簽么?
不能,因?yàn)閙erge的布局要求attachToRoot為true,而ViewStub內(nèi)部實(shí)現(xiàn)inflate布局的方法,attachToRoot為false。
4.ViewStub標(biāo)簽內(nèi)能加入其他view么?
不能,ViewStub是一個(gè)自閉合標(biāo)簽,引用的布局通過(guò)layout屬性進(jìn)行引用,需另外寫(xiě)xml布局文件。
<ViewStub
android:id="@+id/viewStub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/vs_content" />
5.ViewStub多次調(diào)用inflate/setVisible會(huì)發(fā)生什么情況?
- 如果ViewStub是局部變量,多次調(diào)用其首先會(huì)通過(guò)findViewById的方法去找ViewStub,后續(xù)會(huì)返回null,調(diào)用inflate/setVisibility時(shí)會(huì)報(bào)NPE
java.lang.NullPointerException: Attempt to invoke virtual method
'android.view.View android.view.ViewStub.inflate()' on a null object reference
原因是初次inflate后會(huì)內(nèi)部調(diào)用replaceSelfWithView方法,把viewStub節(jié)點(diǎn)從ViewTree里刪除。
- 如果ViewStub是全局變量,多次調(diào)用inflate,會(huì)拋出異常
java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent
因?yàn)槌醮蝘nflate之后自己已經(jīng)從ViewTree中刪除了,但是inflate會(huì)先判斷能不能拿到viewStub自己的parentView,后續(xù)是拿不到即拋出異常。
多次調(diào)用setVisible沒(méi)有問(wèn)題,因?yàn)橹粫?huì)調(diào)用一次inflate,內(nèi)部是通過(guò)mInflatedViewRef拿view。
如果還想再次顯示ViewStub 引用的布局/view(以下這種寫(xiě)法),則建議主動(dòng)try catch這些異常。
try {
View viewStub = viewStub.inflate();
//inflate 方法只能被調(diào)用一次,
hintText = (TextView) viewStub.findViewById(R.id.tv_vsContent);
hintText.setText("沒(méi)有相關(guān)數(shù)據(jù),請(qǐng)刷新");
} catch (Exception e) {
viewStub.setVisibility(View.VISIBLE);
} finally {
hintText.setText("沒(méi)有相關(guān)數(shù)據(jù),請(qǐng)刷新");
}
include標(biāo)簽的使用
就很簡(jiǎn)單的在xml里引入
<include
android:id="@+id/my_title_ly"
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/my_title_layout" />
為什么那么簡(jiǎn)單,因?yàn)閷?shí)際上就是對(duì)xml的解析,遇到了include后進(jìn)行處理,源碼在LayoutInflater的rInflate方法里。
include標(biāo)簽解析
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
....這里只關(guān)注include標(biāo)簽的解析...
else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
}
...源碼有省略...
}
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
//父view必須是ViewGroup,否則拋出異常
if (parent instanceof ViewGroup) {
//處理主題
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
final boolean hasThemeOverride = themeResId != 0;
if (hasThemeOverride) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
//include標(biāo)簽中沒(méi)有設(shè)置layout屬性,會(huì)拋出異常
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
if (layout == 0) {
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
if (value == null || value.length() <= 0) {
throw new InflateException("You must specify a layout in the"
+ " include tag: <include layout=\"@layout/layoutID\" />");
}
layout = context.getResources().getIdentifier(
value.substring(1), "attr", context.getPackageName());
}
// 這里可能會(huì)出現(xiàn)layout是從theme里引用的情況
if (mTempValue == null) {
mTempValue = new TypedValue();
}
if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
layout = mTempValue.resourceId;
}
if (layout == 0) {
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
throw new InflateException("You must specify a valid layout "
+ "reference. The layout ID " + value + " is not valid.");
} else {
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();
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);
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
a.recycle();
// We try to load the layout params set in the <include /> tag.
// If the parent can't generate layout params (ex. missing width
// or height for the framework ViewGroups, though this is not
// necessarily true of all ViewGroups) then we expect it to throw
// a runtime exception.
// We catch this exception and set localParams accordingly: true
// means we successfully loaded layout params from the <include>
// tag, false means we need to rely on the included layout params.
//大意是從include標(biāo)簽里load layoutParams時(shí),
//在父view拿不到的情況下希望能catch住異常,
//true是正常情況,false是異常情況,
//但是會(huì)生成一個(gè)localParams給設(shè)置上去。
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);
// Inflate all children.
rInflateChildren(childParser, view, childAttrs, true);
if (id != View.NO_ID) {
view.setId(id);
}
switch (visibility) {
case 0:
view.setVisibility(View.VISIBLE);
break;
case 1:
view.setVisibility(View.INVISIBLE);
break;
case 2:
view.setVisibility(View.GONE);
break;
}
group.addView(view);
}
} finally {
childParser.close();
}
}
} else {
throw new InflateException("<include /> can only be used inside of a ViewGroup");
}
LayoutInflater.consumeChildElements(parser);
}
merge使用
使用該標(biāo)簽的幾種情況:
如果要 include 的子布局的根標(biāo)簽是 Framelayout,那么最好替換為 merge,這樣可以減少嵌套;
如果父布局是LinearLayout,include的子布局也是LinearLayout且兩者方向一致,也可以用merge減少嵌套,因會(huì)忽略merge里的方向?qū)傩裕?/p>
如果子布局直接以一個(gè)控件為根節(jié)點(diǎn),也就是只有一個(gè)控件的情況,這時(shí)就沒(méi)必要再使用 merge 包裹了。
<?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="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="heng zhe 1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="heng zhe 2" />
</merge>
merge標(biāo)簽解析
同樣在LayoutInflator的rInflate方法里
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
....這里只關(guān)注include標(biāo)簽的解析...
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
...源碼有省略...
}
直接拋出異常,因?yàn)閙erge標(biāo)簽必須做根標(biāo)簽。
說(shuō)白了其實(shí)就是遇到merge標(biāo)簽,那么直接將其中的子元素添加到merge標(biāo)簽父view中,這樣就保證了不會(huì)引入額外的層級(jí),也同時(shí)忽略了merge里的attr屬性。
merge標(biāo)簽問(wèn)題
1.LayoutInflator的inflate方法對(duì)與merge標(biāo)簽的處理說(shuō)明,要被附加的父級(jí)view如果為空,但是要求attachToRoot,那么拋出異常,因?yàn)楦靖郊硬簧先グ ?/p>
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);
2.<merge /> 只能作為布局的根標(biāo)簽使用;
3.不要使用 <merge /> 作為 ListView Item 的根節(jié)點(diǎn);
4.<merge /> 標(biāo)簽不需要設(shè)置屬性,寫(xiě)了也不起作用,因?yàn)橄到y(tǒng)會(huì)忽略 <merge /> 標(biāo)簽;
5.inflate 以 <merge /> 為根標(biāo)簽的布局時(shí)要注意
~5.1必須指定一個(gè)父 ViewGroup;
~5.2必須設(shè)定 attachToRoot 為 true;
也就是說(shuō) inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) 方法的二個(gè)參數(shù) root 不能為 null,并且第三參數(shù) attachToRoot 必須傳 true
參考資料
Android布局優(yōu)化之ViewStub、include、merge使用與源碼分析
http://www.androidchina.net/2485.html
ViewStub--使用介紹
http://www.lxweimin.com/p/175096cd89ac
使用<merge/>標(biāo)簽減少布局層級(jí)
http://www.lxweimin.com/p/278350aa0048