Android Q LayoutInflater佈局生成View源碼詳解

LayoutInflater是什麼?


LayoutInflater是Android系統的一個服務,我們可以通過它,把佈局文件動態生成View,實現View視圖的動態添加。

LayoutInflater在Android日常開發工作中經常使用到,並且我們經常調用的Activity的setContentView方法,它的內部實現就用到了LayoutInflater。

LayoutInflater對象的獲取方式

我們可以通過多種方式來調用LayoutInflater服務。

方法一:LayoutInflater.from(context)

我們可以通過LayoutInflater.from(context)方法來返回LayoutInflater對象,來看源碼:

    public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }
邏輯解析:
  1. 這裏可以看到,LayoutInflater確實是一個系統服務,服務名稱是Context.LAYOUT_INFLATER_SERVICE,可以通過context.getSystemService來獲得。
  2. 如果獲取失敗,則拋出錯誤。

方法二:通過context.getSystemService方法來獲取

其實方法1只是方法2的簡單封裝而已,這裏我們直接通過getSystemService來獲取:

LayoutInflater layoutInflater =
        (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

方法三:在Activity中,調用getLayoutInflater方法

這個方法最簡單,我們可以直接在Activity中調用getLayoutInflater方法來獲取。

Activity的getLayoutInflater方法源碼:

    public LayoutInflater getLayoutInflater() {
        return getWindow().getLayoutInflater();
    }
邏輯解析:
  1. 這裏直接返回的是getWindow()的getLayoutInflater()方法。
  2. getWindow()返回的是Activity所對應的PhoneWindow對象。
  3. PhoneWindow對象,在初始化時,通過LayoutInflater.from(context)獲取了LayoutInflater對象。

關於Activity顯示以及內部視圖的創建等邏輯,我們會在今後的文章中進行源碼分析。

LayoutInflater的使用

我們獲取LayoutInflater實例之後,通過它的inflate方法來使佈局生效。

layoutInflater.inflate(R.layout.activity_main, viewGroup);

示例中,使用layoutInflater.inflate將佈局R.layout.activity_main作爲子視圖添加到viewGroup視圖容器中。

接下來我們來分析inflate方法的源碼實現。

LayoutInflater佈局解析分析


LayoutInflater的inflate方法負責解析佈局並生成View對象,我們接下來分析它的源碼實現。

來看inflate方法:

frameworks/base/core/java/android/view/LayoutInflater.java

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }

    
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
    }

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                  + Integer.toHexString(resource) + ")");
        }

        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
        if (view != null) {
            return view;
        }
        XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
邏輯解析:
  1. 可以看到inflate方法有多個方法重載。
  2. 首先嚐試從預編譯緩存中獲取View對象,如果成功,則直接返回,這裏的預編譯是什麼,稍後我們介紹。
  3. 如果預編譯中獲取失敗,則調用res的getLayout方法獲取XML的資源解析器,這裏用的是PULL解析器(前一章中有介紹)。
  4. 最後調用inflate方法進行解析。

tryInflatePrecompiled方法(預編譯選項)

tryInflatePrecompiled是Android 10(Android Q)中新增的方法,用來根據佈局文件的xml預編譯生成dex,然後通過反射來生成對應的View,從而減少XmlPullParser解析Xml的時間。它是一個編譯優化選項,我們下面來分析。

    private @Nullable
    View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
        boolean attachToRoot) {
        //如果mUseCompiledView是false,則表示預編譯優化開個沒有打開,直接返回
        if (!mUseCompiledView) {
            return null;
        }

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");

        // 獲得佈局資源對應的packageName和layout名稱
        String pkg = res.getResourcePackageName(resource);
        String layout = res.getResourceEntryName(resource);

        try {
            //獲得 "包名" + ".CompiledView"的類
            Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader);
            //獲得以layout名稱命名的方法
            Method inflater = clazz.getMethod(layout, Context.class, int.class);
            //執行該方法,返回View對象。
            View view = (View) inflater.invoke(null, mContext, resource);
            //最後進行View的佈局設置並根據條件判斷是否添加到父視圖中
            if (view != null && root != null) {
                // We were able to use the precompiled inflater, but now we need to do some work to
                // attach the view to the root correctly.
                XmlResourceParser parser = res.getLayout(resource);
                try {
                    AttributeSet attrs = Xml.asAttributeSet(parser);
                    advanceToRootNode(parser);
                    ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);

                    if (attachToRoot) {
                        root.addView(view, params);
                    } else {
                        view.setLayoutParams(params);
                    }
                } finally {
                    parser.close();
                }
            }

            return view;
        } catch (Throwable e) {
            if (DEBUG) {
                Log.e(TAG, "Failed to use precompiled view", e);
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        return null;
    }
邏輯解析:
  1. 首先根據mUseCompiledView判斷預編譯是否是開啓狀態,如果mUseCompiledView是false,則表示預編譯優化開個沒有打開,直接返回。
  2. 獲得佈局資源對應的packageName和layout名稱。
  3. 預加載編譯資源,存儲在以"包名" + ".CompiledView"的類中,並且佈局資源對應了一個以layout名爲名稱的方法。
  4. 使用反射調用,執行該方法,就會返回一個資源佈局對應的View視圖對象。
  5. 最後進行View的佈局設置並根據條件判斷是否添加到父視圖中。

預編譯選項開關

我們來看預編譯選項的開關,mUseCompiledView變量是何時設置的。

    private boolean mUseCompiledView;
    
    private void initPrecompiledViews() {
        // Precompiled layouts are not supported in this release.
        boolean enabled = false;
        initPrecompiledViews(enabled);
    }

    private void initPrecompiledViews(boolean enablePrecompiledViews) {
        mUseCompiledView = enablePrecompiledViews;

        if (!mUseCompiledView) {
            mPrecompiledClassLoader = null;
            return;
        }
    」

它是在initPrecompiledViews方法中進行設置的,其中無參的默認爲false,表示不開啓。

我們來看initPrecompiledViews調用的地方:

    protected LayoutInflater(Context context) {
        mContext = context;
        initPrecompiledViews();
    }
    /**
     * @hide for use by CTS tests
     */
    @TestApi
    public void setPrecompiledLayoutsEnabledForTesting(boolean enablePrecompiledLayouts) {
        initPrecompiledViews(enablePrecompiledLayouts);
    }

可以看到,系統只調用了initPrecompiledViews()無參的方法,表示默認關閉預編譯優化。帶參數的initPrecompiledViews方法只在內部測試時使用。

inflate方法

我們繼續來看inflate方法:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                //將給定的解析器推進到第一個開始標記
                advanceToRootNode(parser);
                final String name = parser.getName();

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }
                //如果根標籤是merge標籤
                if (TAG_MERGE.equals(name)) {
                    //如果根節點是merge,並且root是null或者attachToRoot == false,則拋出異常。
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    //遍歷佈局並生成View
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // 創建根標籤的View對象
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        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);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    //遍歷佈局文件,生成所有子View
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    //如果父容器root是空,或者attachToRoot==false,則直接返回佈局文件的根View。
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(
                        getParserStateDescription(inflaterContext, attrs)
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

通過分析我們可以看到,該方法實現了佈局文件生成View的過程。createViewFromTag()方法實現了佈局文件根View的創建,rInflateChildren(parser, temp, attrs, true)會調用rInflate()方法,遞歸實例化子View,過程中也會使用createViewFromTag()方法創建具體的子View。

rInflate和rInflateChildren方法都是通過遞歸解析xml中的佈局,創建View,並添加到parent中,邏輯比較簡單我們就不做具體分析了。

參數含義及注意事項

我們從以上分析中可以總結出該方法及參數使用的幾個特點:

  • 參數root爲默認父視圖,如果爲null,表示佈局文件生成的View不會添加到默認視圖中。attachToRoot屬性將沒有意義。
  • 參數root如果不爲null,參數attachToRoot表示是否添加到父視圖中
    • 當 attachToRoot爲true時,則會把佈局View添加到root中,作爲root的子視圖。
    • 當 attachToRoot爲false時,不會把佈局View添加到默認父視圖root中,但是會把layout參數設置給佈局View,當佈局View被添加到父view當中時,這些layout屬性會自動生效。
  • 當佈局文件根節點是merge標籤時,root必須不爲null,並且attachToRoot必須爲true,否則就會拋出InflateException異常。
  • attachToRoot默認值,當root != null時爲true,root == null時爲false。

總結


  1. LayoutInflater是Android系統的一個服務,我們可以通過它,把佈局文件動態生成View,實現View視圖的動態添加。
  2. Activity的setContentView方法,它的內部實現就用到了LayoutInflater。
  3. 可以通過多種方式來調用LayoutInflater:LayoutInflater.from(context)、context.getSystemService方法、在Activity中,調用getLayoutInflater方法。其實本質上都是獲取LayoutInflater系統服務。
  4. LayoutInflater的inflate方法負責解析佈局並生成View對象。首先嚐試從預編譯緩存中獲取View對象,如果成功,則直接返回;如果預編譯中獲取失敗,則會使用PULL解析器進行解析。
  5. 預編譯優化是Android Q的新增邏輯,默認是關閉的。
  6. rInflate和rInflateChildren方法都是通過遞歸解析xml中的佈局,創建View,並添加到parent中。
LayoutInflater生成View的參數含義如下:
  • 參數root爲默認父視圖,如果爲null,表示佈局文件生成的View不會添加到默認視圖中。attachToRoot屬性將沒有意義。
  • 參數root如果不爲null,參數attachToRoot表示是否添加到父視圖中
    • 當 attachToRoot爲true時,則會把佈局View添加到root中,作爲root的子視圖。
    • 當 attachToRoot爲false時,不會把佈局View添加到默認父視圖root中,但是會把layout參數設置給佈局View,當佈局View被添加到父view當中時,這些layout屬性會自動生效。
  • 當佈局文件根節點是merge標籤時,root必須不爲null,並且attachToRoot必須爲true,否則就會拋出InflateException異常。
  • attachToRoot默認值,當root != null時爲true,root == null時爲false。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章