---
title: 'LayoutInflate 加載 Xml'
disqus: kyleAlien
---
LayoutInflate 加載 Xml
===
## OverView of Content
LayoutInflate 是抽象類,而實做累事 PhoneLayoutInflater,分析可以參考 [**SystemServiceRegistry 單例創建 PhoneLayoutInflater**](https://hackmd.io/qhIoWcSZQ-GzL3G_Sgqb0A?view#ContextImpl-%E5%88%86%E6%9E%90---SystemServiceRegistry-%E5%96%AE%E4%BE%8B%E5%89%B5%E5%BB%BA-PhoneLayoutInflater)
[TOC]
## LayoutInflate 概述
其功能特色都被隱藏在 Activity、Fragment 等元件之中,而 [**LayoutInflate 服務**](https://hackmd.io/qhIoWcSZQ-GzL3G_Sgqb0A?view#Android-Source-%E5%96%AE%E4%BE%8B%E7%A0%94%E7%A9%B6) 則會透過 CachedServiceFetcher 靜態內部類加載 ++各種服務++ (SystemServiceRegistry 的內部類)
```java=
// 抽象類
public abstract class LayoutInflater {
}
```
LayoutInflater 註冊服務,LAYOUT_INFLATER_SERVICE,可以知道 LayoutInflater 的實做類是 PhoneLayoutInflater 類
```java=
// SystemServiceRegistry.java
static {
...
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx)
// 實做類
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
...
}
```
### [PhoneLayoutInflater](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/policy/PhoneLayoutInflater.java) - 複寫 onCreateView
* 在服務註冊時,具體實現的類是 PhoneLayoutInflater,它會複寫 LayoutInflater 的 `onCreateView` 方法
```java=
// PhoneWindow.java
public class PhoneLayoutInflater extends LayoutInflater {
// 內建 View 類型的前綴描述,Ex: android.widget.TextView
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
public PhoneLayoutInflater(Context context) {
super(context);
}
...
/**
* 建置 View(創建內建 View),如果無法建置,則使用 super#onCreateView 方法建置
*/
@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 */
}
return super.onCreateView(name, attrs);
}
public LayoutInflater cloneInContext(Context newContext) {
return new PhoneLayoutInflater(this, newContext);
}
}
```
### Activity - inflate 加載 xml 布局
* 一般我們在 Activity#onCreate 方法中就會**使用 setContentView 加載 xml 布局**
> 
>
> > AppCompatActivity 也是繼承於 Activity,以下直接追蹤 Activity
* **==Activity#setContentView 實際呼叫的是 Window 的方法==,而由於 Window 是抽象類,其實做類是 [PhoneWindow](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/policy/PhoneLayoutInflater.java)**
```java=
// Activity.java
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, /* 省略部份參數 */) {
attachBaseContext(context);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
... 省略部份
}
public void setContentView(@LayoutRes int layoutResID) {
// getWindow 返回是 PhoneWindow
getWindow().setContentView(layoutResID);
...
}
// -----------------------------------------------------------------
// Window.java
public abstract class Window {
...
// 抽象方法
public abstract void setContentView(@LayoutRes int layoutResID);
}
// -----------------------------------------------------------------
// PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
ViewGroup mContentParent;
...
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
// 此函數會建構 DecorView,並賦予 mContentParent 值
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
...
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
... 省略部份
} else {
// 1. 解析此 ID 的 Xml 檔案
// 2. 添加 View 到 mContentParent 中
// @ 分析 inflate
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}
}
```
* 看看以下 View 創建的概念圖,做上下對照
> 
* 分析 [**LayoutInflater**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/LayoutInflater.java)#inflate 方法
```java=
// LayoutInflater.java
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();
...
// Xml 解析器
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
// 關閉解析器
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
// 解析過程同步...mConstructorArgs 是 Object[]
synchronized (mConstructorArgs) {
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
// 找到 root 元素節點,找到開頭 or 結尾
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();
...
// "1. " merga 節點
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");
}
// "2. " 遞迴解析
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// "3. "
// 透過 tag 解析來創建 view,分析並創建
// @ 追蹤 createViewFromTag 方法
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
... Debug 訊息
// "4. " 加載 Child View (傳入的 temp 是 Parent View)
rInflateChildren(parser, temp, attrs, true);
// "5. "
// ViewGroup 不為空時將其他 View 加入到 ViewGroup 之中
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// root 為空 or 不連接 parent view 的話則修正 result
// 返回 temp
if (root == null || !attachToRoot) {
result = temp;
}
}
} // catch、finally 省略
return result;
}
}
```
1. 解析 xml 根標籤
2. 如果是 merge 根標籤,則會進行 rInflate 的遞迴呼叫,把 merge 下的標籤加入根標籤
3. **普通元素則進行 createViewFromTag() 來解析元素**
4. rInflateChildren 把分析出的所有元素添加到 temp 下
5. ViewGroup 不為空時將其他 View 加入到 ViewGroup 之中
6. 返回解析結果
### Activity 設定 Root View
* 先講結論,Activity 透過 `setContentView` 方法來設定 Root (根) 布局,而根部局 ViewGroup 儲存在 PhoneWindow#mContentParent 中
```java=
// PhoneWindow.java
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
// @ 追蹤 installDecor 方法
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
... 省略部分
}
private void installDecor() {
... 省略 DecorView 的設定
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
... 省略部分
}
}
```
## View 樹
我們的視窗是一顆 View 的 Tree,LayoutInflater 需要解析完整棵 View 樹,所以會**依照上面分析到的 createViewFromTag 繼續分析**
### 布局 xml 分析 - createViewFromTag
* 分析 [**LayoutInflater**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/LayoutInflater.java)#**createViewFromTag** 函數,透過 xml 標籤名稱 tag 來創建 view
| View 類型 | 特點 |
| -------- | -------- |
| 內置 View | xml tag 沒有 `.` 符號 |
| 自訂 or 第三方 View | 全類名,也就是靠判斷 `.` 符號 來知道是否是自訂 View |
```java=
// LayoutInflater.java
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
...
try {
// tryCreateView 根據使用者的手動加載 View
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
// 內置 view
view = onCreateView(context, parent, name, attrs);
} else {
// 自定義 or 第三方 View
// @ 追蹤 createView
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} /* 省略 catch 處理 */
public final View tryCreateView(@Nullable View parent,
@NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
View view;
// 可以透過 mFactory、mFactory2 來自己處理 xml 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);
}
return view;
}
}
```
* 如果未定義自己的解析器 (Factory、Factory2),那就會自動幫你解析,而解析又分為內建 View & 自定義 View,兩者的差別就在**內建 view 不需要全類名,它會透過 PhoneWindow 來包裝全名**
> 
* 而其中的內建 view 則是 先呼叫 onCreateView,也就是會 **先呼叫到 ++PhoneWindow.onCreateView 函數++**,最終 onCreateView 還是會呼叫 createView 函數
:::success
View 在 Xml 中的差異
```xml=
<!-- 內建 view -->
<TextView
android:layout_weight="0.7"
android:layout_width="0dp"
android:layout_height="match_parent"/>
<!-- 自定義 view -->
<com.oo.MyViewText
android:layout_weight="0.7"
android:layout_width="0dp"
android:layout_height="match_parent"/>
```
:::
### 創建內置 View 對象 - createView
* 前面已經透過 Tag 標籤分析完 View(得知是內建、自定義),**LayoutInflater#createView 則是透過反射創建 View 對象**
:::success
* xml 分析布局後,使用 View 的 { Context.class, AttributeSet.class } 建構函數
:::
```java=
// LayoutInflater.java
static final Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// 獲取暫存 construct
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
// 不合法的 construct
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// 嘗試使用 ClassLoader 讀取 Class 類 (檢查類加載)
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
...
// 取得建構函數
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
// 放入緩存
sConstructorMap.put(name, constructor);
} else {
... Filter 省略
}
Object[] args = mConstructorArgs;
args[1] = attrs;
// 反射創建 view
final View view = constructor.newInstance(args);
// 用在延遲加載 (ViewStub 只會站個空位,實際要使用者手動加載)
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
// 返回 View 物件
return view;
} // 省略 catch、finally
}
```
> 
### 遞迴加載 Child View - rInflateChildren
* 在 LayoutInflater#`createViewFromTag` 後接著會調用到 `rInflateChildren` 函數,其實內部也就是調用了 **rInflate 方法**,並且不斷的 **遞迴調用 createViewFromTag (==深度優先==)**
```java=
// LayoutInflater.java
// 這裡傳入的 View 是 parentView
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
// @ 追蹤 rInflate 方法
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
// 取得當前 view 的深度
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;
}
// 取得 xml tag name
final String name = parser.getName();
// 節點 - 判斷
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) { // <include 標籤>
// 深度必須 > 0
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) { // <merge 標籤>
throw new InflateException("<merge /> must be the root element");
} else {
// 創建 View 物件
// @ createViewFromTag 上面已經分析過
final View view = createViewFromTag(parent, name, context, attrs);
// 強制轉型 viewGroup
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 遞迴調用
// @ rInflateChildren
rInflateChildren(parser, view, attrs, true);
// 加入 View
viewGroup.addView(view, params);
}
}
if (finishInflate) {
// 接口回調
parent.onFinishInflate();
}
}
```
* LayoutInflater#rInflate 透過深度優先遍歷來建立檢視樹,**每解析到一個 View 就會遞迴呼叫 `rInflateChildren` -> `rInflate` -> `createViewFromTag`**,最後再回朔到 View 並添加到其 parent 之中
> 
## 整理
* **Activity 中的 setContentView 就是解析 xml 布局文件、並建立子 view**,其中若有需要則調用遞迴分析 rInflate
* **其中調用的服務是 PhoneLayoutInflater 其目的是為了區分,自定義 View or 內建 View**
### 加載 xml - 整體概念圖
> 
## Appendix & FAQ
:::info
:::
###### tags: `Android Framework`