--- 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 布局** > ![](https://i.imgur.com/H5R2lXE.png) > > > 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 創建的概念圖,做上下對照 > ![](https://i.imgur.com/kwCTxJH.png) * 分析 [**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 來包裝全名** > ![](https://i.imgur.com/JlKzyWX.png) * 而其中的內建 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 } ``` > ![](https://i.imgur.com/kp3Oh1o.png) ### 遞迴加載 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 之中 > ![](https://i.imgur.com/JWdSj5F.png) ## 整理 * **Activity 中的 setContentView 就是解析 xml 布局文件、並建立子 view**,其中若有需要則調用遞迴分析 rInflate * **其中調用的服務是 PhoneLayoutInflater 其目的是為了區分,自定義 View or 內建 View** ### 加載 xml - 整體概念圖 > ![](https://i.imgur.com/Vdh3YgZ.png) ## Appendix & FAQ :::info ::: ###### tags: `Android Framework`