kyle shanks
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    --- 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`

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully