上一章我們分析了Activity啟動的時候調用setContentView加載布局的過程,但是分析過程中我們留了兩個懸念,一個是將資源文件中的layout中xml布局文件通過inflate加載到Activity中的過程,另一個是開始測量、布局和繪制的過程,第二個我們放到measure過程中分析,這一篇先分析第一個inflate過程。
- Android系統源碼分析--View繪制流程之-setContentView
- Android系統源碼分析--View繪制流程之-inflate
- Android系統源碼分析--View繪制流程之-onMeasure
- Android系統源碼分析--View繪制流程之-onLayout
- Android系統源碼分析--View繪制流程之-onDraw
- Android系統源碼分析--View繪制流程之-硬件加速
- Android系統源碼分析--View繪制流程之-addView
- Android系統源碼分析--View繪制流程之-彈性效果
LayoutInflater.inflate方法基本上每個開發者都用過,也有很多開發者了解過它的兩個方法的區別,也有一些開發者去研究過源碼,我這里再重復分析這個方法的源碼其實一是做個記錄,二是指出我認為的幾個重點,幫助我們沒有看過源碼的人去了解將xml布局加載到代碼中的過程。這里我們需要重點關注三個問題,然后根據對源碼的分析來解決這三個問題,幫助我們詳細了解inflate的過程及影響,那么這篇文章的目的就達到了。
問題:
- LayoutInflater.inflate兩個個方法是什么?
- 這兩個方法會給我們的視圖顯示帶來什么影響?
- View視圖的寬、高是什么時候解析的?
第一個問題:LayoutInflater.inflate兩個個方法是什么?
這個問題是最簡單的,基本上這兩個方法都使用過,但是使用的結果卻是不一樣的。下面我貼出來這兩個方法的代碼:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
雖然是兩個方法,但是第一個方法最終會調用第二個方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
調用第二個方法的時候第三個參數是與第二個參數ViewGroup是否為空有關的,這個參數具體作用我們后面代碼流程分析再說。我們先看使用的幾種情況:
// 第一種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, mParentView);
// 第二種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, null);
// 第三種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, mParentView, false);
// 第四種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, mParentView, true);
// 第五種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, null, false);
// 第六種情況
LayoutInflater.from(mContext).inflate(R.layout.screen_simple, null, true);
這里羅列了所有用法,但是不同的用法可能對我們的顯示效果是有影響的,那么就到了第二個問題,下面通過分析代碼過程來看看到底有什么影響。還有第三個問題,是我之前面試的時候被問到的,之前看inflate源碼沒有很詳細,所以沒有回答上來,這次也一起分析一下,這個寬、高可能很多人覺得是和其他屬性一起解析的,其實不是,這個是單獨解析的,就是因為View的寬、高是單獨解析的,所以會有一些問題出現,可能有些開發者也遇到這個坑,通過這篇文章分析你會的到答案,并且可以準確填上你的坑。
在上面六種情況中是有一樣的:
- 如果mParentView不是null,那么:1、4是一樣的,2、5是一樣的,3是一樣,6是一樣,
- 如果mParentView是null,那么:1、2、3、5是一樣,4、6是一樣的。
代碼流程
先看一張流程圖:
1.LayoutInflater.inflate
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
前面提到了inflate方法調用最終調用到第二個是三個參數的方法,只不過第三個參數是與第二個參數有關系的,這個關系就是root是不是null,如果不是null,傳遞true,反之傳遞false。
2.LayoutInflater.inflate
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
View result = root;
try {
int type;
...
final String name = parser.getName();
...
// 要加載的布局根標簽是merge,那么必須傳遞ViewGroup進來,并且要添加到該ViewGroup上
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 {// 根標簽不是merge
// temp是要解析的xml布局中的根布局視圖
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
// 1.root不為空會解析寬、高屬性(如果不添加的話,那么會將屬性設置給xml的根布局)
if (root != null) {
// root存在才會解析xml根布局的寬高(如果xml文件中設置的話)
params = root.generateLayoutParams(attrs);
// 不將該xml布局添加到root上的話
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
// 遞歸解析temp(xml文件中的根布局)下所有視圖,并按樹形結構添加到temp中
rInflateChildren(parser, temp, attrs, true);
// 2.root視圖不為空,并且需要添加到root上面,那么調用addView方法并且設置LayoutParams屬性
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 3.root為空或者attachToRoot為false,那么就會將該xml的根布局賦值給result返回,
// 但是root為空時是沒有設置寬高的
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
...
}
return result;
}
}
這里開始layout布局的最開始解析,首先if語句是判斷根視圖,也就是最外層視圖是merge標簽的時候,必須傳入的root不是null,并且第三個參數attachToRoot必須是true,否則拋出異常。如果root不為null,并且attachToRoot==true,那么調用rInflate方法繼續解析。如果不是merge標簽,那么解析過程由外向內開始解析,所以首先解析最外層的根視圖并保存為temp,這里如果root不是null,那么就要獲取LayoutParam屬性,這個方法下面再看,然后判斷如果attachToRoot是false的話那么就給temp設置屬性,如果為true就沒有設置。然后調用rInflateChildren方法遞歸解析temp下面的所有視圖,并按樹形結果添加到temp中。接著判斷root不為null,并且attachToRoot為true,那么將temp添加到root中并且設置屬性值,所以這里可以看出,attachToRoot參數是是否將解析出來的layout布局添加到root上面,如果添加則會有屬性值。
所以這里的重點就是root決定layout布局是否被設置ViewGroup.LayoutParams屬性,而attachToRoot決定解析出來的視圖是否添加到root上面。這里我們先看獲取的ViewGroup.LayoutParams屬性包含了那幾個屬性值。
3.ViewGroup.generateLayoutParams
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
這里只是new了一個新對象LayoutParams,我們看看這個LayoutParams對象的構造函數做了什么
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
這里調用setBaseAttributes函數:
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
到這里基本明確了,這里就是獲取視圖的寬、高屬性值的,也就是我們layout布局中視圖的寬、高值。寬、高包括以下幾種:
public static final int FILL_PARENT = -1;
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
只有具體值,也就是我們設置的layout_width和layout_height值,其實上面第一種已經被第二個取代了。
所以我們這里看到了視圖的寬、高就是通過ViewGroup.generateLayoutParams來獲取的,如果沒有調用那么解析的視圖就沒有有效的寬、高,如果需要具體值就要自己手動設置了。也就是在調用LayoutInflater.inflate方法的時候想讓自己設置的寬、高有效,傳入root就不能是null,否則不會獲取有效的寬、高參數,在后面顯示視圖的時候系統會配置默認的寬、高,而不是我們設置的寬、搞。這個后面會再分析。
還有一種情況就是我想獲取寬、高,但是不想添加到root上,而是我手動添加到別的ViewGroup上面需要怎么辦,那就是調用三個參數的inflate方法,root參數不是null,attachToRoot設置為false就可以了
4.LayoutInflater.rInflate
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
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)) { // requestFocus
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) { // tag
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) { // include
if (parser.getDepth() == 0) {// include不能是根標簽
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) { // merge
// merge必須是根標簽
throw new InflateException("<merge /> must be the root element");
} else {// 正常View
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);
// parent下的所有view解析完成就會添加到parent上
viewGroup.addView(view, params);
}
}
// parent下所有視圖解析并add完成就會調用onFinishInflate方法,所以我們可以根據這個方法判斷是否解析完成
if (finishInflate) {
parent.onFinishInflate();
}
}
上面第2步中,如果根標簽是merge那么直接調用這個方法繼續解析下一層,這里有五種情況,前兩種我們不分析,基本不用,我們分析下面我們常用的:如果是include標簽,那么就要判斷include的層級,如果include下沒有其他層級,那么會拋出異常,也就是include下必須有layout布局,然后會調用parseInclude來解析include標簽的布局文件;另外就是merge嵌套merge也是不行的,會拋出異常;最后就是正常視圖,通過createViewFromTag來創建該視圖,然后解析寬、高,這里是直接解析了,只有最外層是要判斷root的,然后調用rInflateChildren,這里rInflateChildren還是會調用這里的方法,也就是形成遞歸解析下一層視圖并添加到外面一層視圖上面,這里都是有寬、高屬性的。最后有一個if語句,這里的意思是每個ViewGroup下面的所有層級的視圖解析完成后,會調用這個ViewGroup的onFinishInflate方法,通知視圖解析并添加完成,所以我們在自定義ViewGroup的時候可以通過這個方法來判斷你自定義的ViewGroup是否加載完成。
下面我們再看parseInclude方法是如何解析include標簽視圖的
5.LayoutInflater.parseInclude
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
// include標簽必須在ViewGroup使用,所以這里parent必須是ViewGroup
if (parent instanceof ViewGroup) {
...
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 {// include中layout的指向id必須有效
...
try {
...
final String childName = childParser.getName();
if (TAG_MERGE.equals(childName)) {// merge
// The <merge> tag doesn't support android:theme, so
// nothing special to do here.
rInflate(childParser, parent, context, childAttrs, false);
} else {// 正常View
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
final ViewGroup group = (ViewGroup) parent;
...
ViewGroup.LayoutParams params = null;
try {
// include是否設置了寬高
params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
// 如果include沒有設置寬高,則獲取layout指向的布局中的寬高
if (params == null) {
params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);
// Inflate all children.
rInflateChildren(childParser, view, childAttrs, true);
...
group.addView(view);
}
} finally {
childParser.close();
}
}
} else {// include必須在ViewGroup中使用
throw new InflateException("<include /> can only be used inside of a ViewGroup");
}
...
}
這里首先判斷include標簽的上一個層級是不是ViewGroup,如果不是那么拋出異常,也就是include必須在ViewGroup內使用。如果是在ViewGroup中使用,那么接著判斷layout的id是否有效的,如果不是,那么就要拋出異常,也就是include必須包含有效的視圖布局,然后開始解析layout部分視圖,如果跟布局是merge,那么調用解析對應merge的方法rInflate,也就是步驟4,如果是正常的View視圖,那么通過createViewFromTag方法獲取視圖,然后獲取include標簽的寬、高,如果include中沒有設置才獲取include包含的layout中的寬、高,也就是include設置的寬、高優先于layout指向的布局中的寬、高,所以這里要注意了。獲取完成會設置對應的寬高屬性,然后調用rInflateChildren遞歸完成layout下所有層級視圖的加載。基本的邏輯就差不多了,其實并不復雜,還有個方法需要簡單介紹下-createViewFromTag,根據xml中的標簽也就是視圖的名字加載View實體。
6.LayoutInflater.createViewFromTag
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
View view;
...
if (view == null) {
...
try {
// 系統自帶的View(直接使用名字,不用帶包名,所以沒有".")
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {// 帶有包名的View(例如自定義的View,或者引用的support包中的View)
view = createView(name, null, attrs);
}
} finally {
...
}
}
return view;
} catch (InflateException e) {
...
}
}
這個方法里有兩行注釋,我解釋一下,我們在xml布局中有兩種寫法,一種是系統自帶的視圖,例如:FrameLayout,LinearLayout等,一種是自定義的或者是Support包中的也就是帶有包名的視圖:
<?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">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/header_rl"
android:scrollbars="vertical"/>
<ProgressBar
android:id="@+id/progress"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
上面這個布局就是包含兩種,系統自帶的就是ProgressBar,還有就是帶有包名的,這兩種解析方法是有區別的。系統自帶的用onCreateView方法創建View,帶有包名的通過createView方法創建。我們先看第一個:
7.LayoutInflater.onCreateView
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
// 系統正常View要添加前綴,比如:LinearLayout,添加完前綴就是android.view.LinearLayout
return createView(name, "android.view.", attrs);
}
系統的視圖都在android.view包下,所以要添加前綴“android.view.”,添加完也是完整的視圖名稱,就和自定義的是一樣的,最終還是調用createView方法:
8.LayoutInflater.createView
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
...
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
...
constructor = clazz.getConstructor(mConstructorSignature);
...
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
...
constructor = clazz.getConstructor(mConstructorSignature);
...
} else if (allowedState.equals(Boolean.FALSE)) {
...
}
}
}
...
final View view = constructor.newInstance(args);
...
return view;
} catch (NoSuchMethodException e) {
...
}
}
這里就很簡單了就是根據完整的路徑名稱加載出對應的Class文件,然后創建對應的Constructor文件,通過調用Constructor.newInstance創建對應的View對象,這就是將xml文件解析成java對象的過程。
總結
LayoutInflate.inflate方法很重要,這是我們將xml布局解析成java對象的必須過程,所以掌握這個方法的原理非常重要,上面分析的時候也提出一些重點的內容,所以我們再總結一下,方便記憶:
- inflate方法的第二個參數root不為null,加載xml文件時根視圖才有具體寬、高屬性;
- inflate方法的第三個參數attachToRoot是true時,解析的xml布局會被添加到root上,反之不添加;
- 調用兩個參數的inflate方法時,參數attachToRoot = (root != null);
- include設置的寬、高優先于layout指向的布局中設置的寬、高;
- include不能是根標簽;
- merge必須是根標簽
- include必須有有效的layout id
代碼地址:
直接拉取導入開發工具(Intellij idea或者Android studio)
注:本文原創,轉載請注明出處,多謝。