ndroid系統原始碼分析--View繪製流程之-inflate
上一章我們分析了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為空,或者不新增到root上,那麼就會將該xml的根佈局賦值給result返回,
// 但是這裡是沒有解析也沒有設定寬高的
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)
注:
首發地址:www.codemx.cn
Android開發群:192508518
微信公眾賬號:Code-MX
注:本文原創,轉載請註明出處,多謝。