太長了,不想看?
LayoutInflater.inflate()
方法能將Xml格式的布局文件轉換成以父子關系組合的一系列View,轉換后的結構也稱為View Hierarchy。
我們通常向inflate()方法傳入三個參數:布局資源的resId,可為空的ViewGroup:root,以及布爾值attachToRoot。許多初學者對于后兩個參數的使用比較生硬,缺乏理解。
我理解的引入root的原因是為了讀取Xml文件中最外層標簽的布局屬性。布局屬性就是我們常看到的以<layout->
開頭的屬性,他表示一個View希望在父布局中獲得的尺寸以及位置,因此布局屬性是提供給父布局讀取以便計算尺寸、位置的。布局屬性體現在View中就是mLayoutParams
變量,這個變量的類型是ViewGroup.LayoutParams
。不同的ViewGroup的子類實現了不同的LayoutParams
類,用以從Xml中讀取自己關心的布局屬性,一個View的mLayoutParams
的類型必須與其父布局實現的LayoutParams相一致。
對于Xml文件的最外層標簽,他所轉換成的View并不知道自己的父布局會是什么類型的,因此他不會生成mLayoutParams
變量,此時該標簽的所有<layout-XXX>
屬性都沒有應用到View中。為了避免這種情況,我們需要告知最外層的View他的父布局是什么類型的,生成對應的LayoutParams儲存布局屬性。root就能幫助我們生成LayoutParams,而如果root正是我們希望的父布局,那么我們就將attachToRoot設為true,這樣我們通過Xml生成的View Hierarchy可以直接加入到root中,我們不需要手動做這步操作了。
如果我們將Xml文件的嵌套結構看作是樹狀結構的話,逐個標簽的解析其實就是樹的深度優先遍歷,我們在遍歷的同時生成了一棵以View為節點,使用父子關系關聯的樹。
View Hierarchy中每個View的生成有四步:
- 由標簽生成一個View
- 根據View的父布局的類型生成對應的LayoutParams,并將LayoutParams設置給View
- 生成View的所有Children
- 將View加入他的父布局中
由一個標簽轉換成一個View的過程其實就是通過ClassLoader加載出標簽對應的View.Class文件并獲得構造器,相當于調用View(Context context, @Nullable AttributeSet attrs)
構造一個實例。因此我們的自定義控件需要實現該構造方法才能在Xml中使用,隨后從attrs變量中獲得Xml中的屬性。
inflate方法介紹
Android開發中,我們使用LayoutInflater.inflate()
方法將layout目錄下Xml格式的資源文件轉換為一個View Hierarchy,并返回一個View對象。
View Hierarchy直譯大概就是視圖層次,指的是以父子關系關聯的一系列View,我們通過View Hierarchy的根節點(root view)可以獲得該結構中所有的View對象。
如果讓我們自己去設計一個方法將布局文件轉換為View Hierarchy,我們可能會想到逐行讀取Xml中的標簽,利用標簽的信息生成一個新的View/ViewGroup,當Xml中出現嵌套關系就意味我們需要使用父子關系關聯兩個View。而inflate()
方法做的就是這么一件事。
我們可以使用Activity.getLayoutInflater()
等方法獲得一個LayoutInflater的實例,這些方法本質上都是通過getSystemService(Context.LAYOUT_INFLATER_SERVICE)
獲得一個系統服務,這個方法最終會返回一個PhoneLayoutInflater
的實例。
inflate有幾種重載方法,但最終都會走到
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
第一個參數我們看著比較陌生,但是大部分情況下我們只需要傳入一個layout資源文件,Framework會幫我們根據資源獲得一個Parser。需要注意的是為了提升使用時的效率,Android會在編譯時就去解析layout資源文件并生成Parser,因此我們想通過inflate()方法在使用過程中使用一個單純的Xml文件(非布局資源)去生成View是不可行的。
后面兩個參數會在源碼解析過程中介紹。
我們看下官方對這幾個參數的定義:
參數 | 意義 |
---|---|
parser | XmlPullParser:以Pull的方式解析Xml文件,通過Parser對象方便我們操作 |
root | ViewGroup:可選項。當attachToRoot為true時,他將作為生成出來的View層級的parent。如果attachToRoot為false,那root僅僅為View層級樹的根節點提供LayoutParams的值 |
attachToRoot | 配合root使用 |
inflate()方法的返回值是一個View,如果root不會為空且attachToRoot為true,返回root。否則返回Xml生成的View Hierarchy的根View。
Inflate 方法源碼解析
看下inflate階段的核心代碼
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
// 解析根節點
final String name = parser.getName();
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 {
// 將Xml最外層的標簽解析為View對象,記為temp
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
if (root != null) {
// 當root不為空時,創建與root相匹配的LayoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 僅將params提供給temp
temp.setLayoutParams(params);
}
}
// 通過inflate生成temp的所有chiildre
rInflateChildren(parser, temp, attrs, true);
// root不為空且attachToRoot為true,將temp加入到root中,且用到params
if (root != null && attachToRoot) {
root.addView(temp, params);
}
if (root != null && attachToRoot)
return root;
}else {
return temp;
}
}
}
先不考慮<merge>
標簽,inflate()
方法執行的流程:
- 將最外層的標簽轉換為一個View,記為temp。
- 當root不為空時,利用root生成的temp的LayoutParams。
- 解析Xml并生成temp的所有子View。
- 當root不為空且attachToRoot為true時,將temp添加為root的一個child。
- 當root不為空且attachToRoot為true時,返回root,否則返回temp。
這個流程包含了幾個細節:
- 由Xml標簽生成View對象
- 根據Xml嵌套結構生成View父子結構
- 應用root及attachToRoot
下面,我們由表及里的解析這幾個細節。首先來看一下傳參中root與attachToRoot兩個參數的作用。
root與attachToRoot
root在inflate()方法中的第一個作用就是生成一個LayoutParams。
if (root != null) {
// 當root不為空時,創建與root相匹配的LayoutParams
params = root.generateLayoutParams(attrs);
}
LayoutParams保存的是一個控件的布局屬性。那我們來看下為什么需要利用root生成LayoutParams。
布局屬性與LayoutParams
在Xml文件中,有許多前綴為layout_
的屬性,比如我們最熟悉的layout_width/layout_height
,我們稱其為布局屬性。View使用布局屬性來告知父布局它所希望的尺寸與位置。不同類型的父布局讀取的布局屬性不同,比如layout_centerInParent
屬性,父布局為RelativeLayout時會起作用,而父布局為LinearLayout時則無法使用。
Xml中的布局屬性保存到View中就是mLayoutParams
變量,它的類型是ViewGroup.LayoutParams
。實際上ViewGroup的子類都會實現一個擴展自ViewGroup.LayoutParams
的嵌套類,這個LayoutParams類決定了他會讀取哪些布局屬性。我們看下RelativeLayout.LayoutParams
源碼的一部分:
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.RelativeLayout_Layout);
...
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
...
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf:
rules[LEFT_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toRightOf:
rules[RIGHT_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_above:
rules[ABOVE] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerInParent:
rules[CENTER_IN_PARENT] = a.getBoolean(attr, false) ? TRUE : 0;
break;
...
}
}
...
a.recycle();
}
這段代碼跟我們自定義控件時讀取Xml中的自定義屬性是一樣的做法,我們看到RelativeLayout.LayoutParams
在創建時讀取了一系列布局屬性并存儲,比如layout_centerInParent
,其他的LayoutParams不會讀取該屬性。
如果對AttributeSet、TypedArray不熟悉可以參考這里:https://blog.csdn.net/lmj623565791/article/details/45022631
我們能在RelativeLayout.onMeasure()
方法中找到對LayoutParams的使用。
// RelativeLayout.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
int count = views.length;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
...
LayoutParams params = (LayoutParams) child.getLayoutParams();
int[] rules = params.getRules(layoutDirection);
...
}
}
...
}
當計算每個子View的位置時,需要讀取他們的布局屬性,此時會將View的LayoutParams強制類型轉換為RelativeLayout.LayoutParams,這里可能會報出無法轉換類型的錯誤,所以我們需要保證加入到RelativeLayout的View的LayoutParams類型都是RelativeLayout.LayoutParams
。
我們總結一下LayoutParams的核心知識點:
- LayoutParams保存了Xml中的
layout_
開頭的布局屬性 - ViewGroup子類通常會實現一個LayoutParams類,用于讀取他們需要的布局屬性
- View的LayoutParams類型必須與其父布局的類型相匹配,否則會在
onMeasure
過程中報錯
回到root與attachToRoot
當我們解析Xml中最外層的標簽,也就是View Hierarchy的根View時,程序并不知道它的父布局會是什么類型的,因此不會生成LayoutParams。這時最外層標簽中的所有布局屬性,包括layout_width/layout_height
都不會被記錄到View對象中,也就是俗稱的“屬性失效了”。
但在實際使用中,我們通常能知道Xml生成的View Hierarchy所要加入的父布局或是要加入的父布局的類型。這時候我們傳入一個root
參數,根據root的類型去讀取根View的布局屬性并生成對應的LayoutParams
。這段代碼如下:
if (root != null) {
// root不為空時,生成與root對應的LayoutParams
params = root.generateLayoutParams(attrs);
}
public LayoutParams generateLayoutParams(AttributeSet attrs) {
// 不同的ViewGroup生成LayoutParams的細節不同
return new LayoutParams(getContext(), attrs);
}
而attachToRoot
則用于判斷root是直接作為parent使用還是僅需要他的類型信息。
if (root != null) {
if (attachToRoot) {
root.addView(temp, params);
}else{
// Set the layout params for temp if we are not attaching.
temp.setLayoutParams(params);
}
}
遞歸處理Xml中的所有標簽
在inflate
方法中,除了根標簽以外所有剩余標簽的解析只使用了一個方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
...
}
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
這個方法僅僅是調用rInflate
方法,沒有其他額外的行為。rInflate方法的中r
的含義是Recursive
,即遞歸,我們可以猜測這個方法是使用遞歸的方式去處理Xml中的所有嵌套關系。我們看下rInflate
核心部分:
這段方法涉及到XmlPullParser的知識,他將Xml文件轉換為一個對象。通過
next()
方法獲得下一個事件,一共有五個事件START_DOCUMENT、START_TAG、TEXT、END_TAG、END_DOCUMENT。并且通過depth取得當前元素嵌套的深度,未讀取到START_TAG時depth為0,每次讀取到START_TAG時depth加1。細節參考 http://www.xmlpull.org/ 。
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) {
final int depth = parser.getDepth();
// while的結束條件:找到該depth的END_TAG(或者文件結束)
while (((type = parser.next()) != XmlPullParser.END_TAG
|| parser.getDepth() > depth)
&& type != XmlPullParser.END_DOCUMENT) {
// 只有遇到START_TAG時進行解析
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
...
} 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 {
// 將當前Tag轉換為View
final View view = createViewFromTag(parent, name, context, attrs);
// 根據父布局類型讀取布局屬性
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 嵌套處理,解析當前View的所有children
rInflateChildren(parser, view, attrs, true);
// 將View加入到parent中
viewGroup.addView(view, params);
}
}
}
我們配合例子分析下rInflate
方法的流程:
<!-- depth -->
<LayoutA > <!-- 1 -->
<ViewB /> <!-- 2 -->
<LayoutC > <!-- 2 -->
<ViewC1 /> <!-- 3 -->
</LayoutC > <!-- 2 -->
</LayoutA > <!-- 1 -->
1.首先記下當前的depth
2.取Xml中的下一個事件,直到到達文件的結尾,或者到達了當前depth的END_TAG
參照例子,如果rInflate開始時解析到了<LayoutC>,那么當解析到</LayoutC>時就會從while跳出,本次執行結束。
3. while循環中,遇到START_TAG時解析
只解析STAST_TAG是保證每個Tag都只生成一個View,比如在解析到<LayoutC>時生成一個View,解析到</LayoutC>時則不需要。
解析普通的TAG的邏輯如下:
// 將當前Tag轉換為View
final View view = createViewFromTag(parent, name, context, attrs);
// 根據父布局類型讀取布局屬性
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 嵌套處理,解析當前View的所有children
rInflateChildren(parser, view, attrs, true);
// 將View加入到parent中
viewGroup.addView(view, params);
這里需要關注的就是那個嵌套的rInflateChildren()
方法,他也會走到rInflate,以當前解析出的View作為Parent,解析下一層的內容。
inflate過程推演
我們還是以上面的例子將inflate方法的執行流程推演一遍。
<!-- depth -->
<LayoutA > <!-- 1 -->
<ViewB /> <!-- 2 -->
<LayoutC > <!-- 2 -->
<ViewC /> <!-- 3 -->
</LayoutC > <!-- 2 -->
</LayoutA > <!-- 1 -->
- inflate方法,取到<LayoutA>,解析出LayoutA對象
- 通過rInflate方法解析LayoutA的所有children,開始時depth為1
- 取到<ViewB />,解析出ViewB對象,ViewB無children,它的rInflateChildren會讀到ViewB的END_TAG并結束,將ViewB加入到LayoutA中
- 取到<LayoutC>,解析出LayoutC對象,調用rInflate方法解析Children
- 取到<ViewC />,解析出ViewC對象,無Children,將ViewC加入到LayoutC中
- 取到<LayoutC />,LayoutC的rInflateChildren過程結束
- 將LayoutC對象加入到LayoutA中
- 取到</LayoutA>,為END_TAG且depth為1,rInflate方法結束
- View Hierarchy解析完成,配合root及attachToRoot做些處理便可返回
整個流程自上而下的解析了Xml文件中的所有Tag,并生成了對應的View Hierarchy,嵌套關系順利轉換成了父子關系。生成的View Hierarchy是一個樹狀結構,生成過程跟樹的深度優先遍歷有相似的感覺。
merge標簽解析
解析完rInflate方法后,我們再來看下<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);
}
其實就是忽略<merge>這一層,將<merge>的所有內容都直接加入到root中。
由Tag生成對應的View
前面我們介紹了layout Xml如何轉換為View Hierarchy,這個過程中的最后一個細節就是單個Tag如何轉化成View對象
無論是inflate方法對根View的解析還是rInflate中的嵌套解析,都是調用createViewFromTag()
方法,我們看下這個方法的核心部分。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
// <view>標簽中可以使用class來標注類
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// 除了<include>以外,ignoreThemeAttr總為ture,讀取Xml中的theme并通過Context作用到View上
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
// 就當是彩蛋吧,let's party
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
// 生成View
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
// 用于構建View的參數
// View(Context context, @Nullable AttributeSet attrs)
// 第一個傳參是context
attrs, int defStyleAttr)
mConstructorArgs[0] = context;
try {
// 使用系統控件時我們可以不帶著命名空間,此時name中不包含"."
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
}
...
}
直接看到生成View的部分
- 由Factory2生成
- 由Factory生成
- 由mPrivateFactory生成
- 由onCreateView/createView方法生成
這里的生成方法有先后順序,View一旦生成就不用走后面的方法。Factory2及Factory可以看做是給我們hook代碼的,允許我們按自己的期望去將Tag轉換為View,二者的區別是工廠方法的傳參不同。
如果沒有設置工廠
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
Xml中使用系統控件可以不加上命名空間,因此name中沒有“.”,在onCreateView方法中會為系統控件加上前綴“"android.view."”并調用createView方法。而我們前面提到了我們實際使用的LayoutInflater通常是PhoneLayoutInflater
,他重寫了onCreateView方法:
/// PhoneLayoutInflater.java
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
/// LayoutInflater.java
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
這里的操作都是為系統控件補全命名空間,具體的生成View的工作由createView完成,核心代碼如下:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
...
// 使用ClassLoader獲得構造器
clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
// View的構造器的傳參
Object[] args = mConstructorArgs;
args[1] = attrs;
// 獲得View實例
final View view = constructor.newInstance(args);
return view;
...
}
先通過ClassLoader加載類,獲得構造器,然后實例化View的子類。具體的構造方法是View(Context context, @Nullable AttributeSet attrs)
,因此我們的自定義控件也需要實現這個構造方法才能在Xml中正確使用。
`