Android LayoutInflater分析

今天分析下經(jīng)常使用加載布局的layoutInflater
我們?cè)诩虞d布局的時(shí)候都會(huì)主動(dòng)或者被動(dòng)的用到 LayoutInflater ,比如 Activity 的setContentView方法和Fragment的onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)回調(diào)等。LayoutInflater 的作用就是把布局文件xml實(shí)例化為相應(yīng)的View組件。我們可以通過(guò)三種方法獲取 LayoutInflater:

1.Activity.getLayoutInflater();
2.LayoutInflater.from(context);
3.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

每個(gè)方法都和 Context 相關(guān)聯(lián),其中方法1和方法2最終都會(huì)通過(guò)方法3來(lái)實(shí)現(xiàn)。
獲取到 LayoutInflater 后,通過(guò)調(diào)用inflate方法來(lái)實(shí)例化布局。而inflate方法由很多重載,我們常用的是inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot),所有 inflate 方法最終會(huì)調(diào)用到 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)。下面就從這個(gè)方法入手,開(kāi)始分析 LayoutInflater 的源碼。

inflate方法
先看一下inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)的三個(gè)參數(shù):

  1. XmlPullParser parser:很顯然是一個(gè) XML 解析器,這個(gè)解析器就是 LayoutInflater 所要加載的 XML 布局轉(zhuǎn)化來(lái)的,通過(guò) PULL 方式解析。
  2. ViewGroup root:裝載要加載的 XML 布局的根容器,比如,在 Activity 的setContentView方法中就是 id 為android.R.id.content的 FrameLayout 根布局了。
  3. boolean attachToRoot:是否將所解析的布局添加到根容器中,同時(shí)也影響了所解析布局的寬高。

被廣泛討論的是root和attachToRoot的不同傳參對(duì)被加載的布局文件的影響,下面看代碼。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            final Context inflaterContext = mContext;
            // 將parser轉(zhuǎn)成AttributeSet接口,用來(lái)讀取xml中設(shè)置的View屬性
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root; // 此方法返回的View,默認(rèn)是root
            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
                ...
                final String name = parser.getName(); // 獲取當(dāng)前的標(biāo)簽名
                ...
                if (TAG_MERGE.equals(name)) { // 處理<merge>標(biāo)簽
                    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 {
                    // Temp is the root view that was found in the xml
                    // 創(chuàng)建View對(duì)象
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;
                    if (root != null) {
                        ...
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs); // 獲取根View的寬高
                        if (!attachToRoot) { // 如果attachToRoot為false,則給根View設(shè)置寬高
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
                    ...
                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true); // 遞歸處理
                    ...
                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        // 如果root不空,且attachToRoot為true,則將根View添加到容器中
                        root.addView(temp, params);
                    }
                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        // 如果root空或者attachToRoot為false,則將返回結(jié)果設(shè)置為根View
                        result = temp;
                    }
                }
            } catch (XmlPullParserException e) {
                ...
            } catch (Exception e) {
                ...
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            // 要么是root,要么是創(chuàng)建的根View
            return result;
        }
    }

從代碼中可以看出root和attachToRoot不同傳參的影響:

  1. 如果root不為null,attachToRoot設(shè)為true,則會(huì)將加載的布局添加到一個(gè)父布局中,即root,并且返回root;
  2. 如果root不為null,attachToRoot設(shè)為false,則會(huì)對(duì)布局文件最外層的所有l(wèi)ayout屬性進(jìn)行設(shè)置,并且返回該布局的根View,當(dāng)該view被添加到父view當(dāng)中時(shí),這些layout屬性會(huì)自動(dòng)生效;
  3. 如果root為null,attachToRoot將失去作用,設(shè)置任何值都沒(méi)有意義,返回的也是要加載的布局的根View;

rInflate方法

從上面的方法中可以看到處理<merge>標(biāo)簽時(shí)會(huì)調(diào)用rInflate,處理子View時(shí)會(huì)調(diào)用rInflateChildren方法。其實(shí)rInflateChildren中調(diào)用的是rInflate,而rInflate也調(diào)用了rInflateChildren,從而形成了遞歸調(diào)用,也就是遞歸處理子View。

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;
        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>標(biāo)簽
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                // 處理<tag>標(biāo)簽
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                // 處理<include>標(biāo)簽
                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>標(biāo)簽異常
                throw new InflateException("<merge /> must be the root element");
            } else { // 創(chuàng)建View對(duì)象
                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); // 遞歸處理孩子節(jié)點(diǎn)
                viewGroup.addView(view, params); // 將View添加到父布局中
            }
        }
        if (pendingRequestFocus) { // 父布局處理焦點(diǎn)
            parent.restoreDefaultFocus();
        }
        if (finishInflate) { // 結(jié)束加載
            parent.onFinishInflate();
        }
    }

該方法中會(huì)處理<requestFocus>、<tag>、<include>、<merge>和普通View標(biāo)簽。其中:

  1. <requestFocus>是重新定位焦點(diǎn)的,調(diào)用的consumeChildElements方法其實(shí)沒(méi)干什么事,只是簡(jiǎn)單的把該標(biāo)簽消費(fèi)結(jié)束掉。
  2. <tag>標(biāo)簽一般很少用,它主要用來(lái)標(biāo)記View,給View設(shè)置一個(gè)標(biāo)簽值,例如:
    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" >

        <tag android:id="@+id/tag"
            android:value="hello" />

    </TextView>
    
    findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 試一下tag標(biāo)簽
                Toast.makeText(MainActivity.this, (String) v.getTag(R.id.tag), Toast.LENGTH_SHORT).show();
            }
        });

在ListView的自定義Adapter中,應(yīng)該都有用到過(guò)View的setTag方法,即:使用ViewHolder來(lái)重復(fù)利用View。
parseViewTag方法:

private void parseViewTag(XmlPullParser parser, View view, AttributeSet attrs)
            throws XmlPullParserException, IOException {
        final Context context = view.getContext();
        final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewTag);
        // 讀取tag的id
        final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
        // 讀取tag的值
        final CharSequence value = ta.getText(R.styleable.ViewTag_value);
        // 給View設(shè)置該tag
        view.setTag(key, value);
        ta.recycle();
        // 結(jié)束該標(biāo)簽(子View無(wú)效)
        consumeChildElements(parser);
    }
  1. <include>標(biāo)簽不能是根標(biāo)簽,parseInclude方法單獨(dú)分析。
  2. <merge>標(biāo)簽只能是根標(biāo)簽,這里會(huì)拋異常。

parseInclude方法

private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;
        if (parent instanceof ViewGroup) { // 必須在ViewGroup里才有效
            // 處理theme屬性
            ...
            // If the layout is pointing to a theme attribute, we have to
            // massage the value to get a resource identifier out of it.
            // 拿到layout指定的布局
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
            ...
            if (layout == 0) { // 必須是合法的id
                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 { // 類(lèi)似于inflate的處理
                // 拿到layout的解析器
                final XmlResourceParser childParser = context.getResources().getLayout(layout);
                try {
                    final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
                    while ((type = childParser.next()) != XmlPullParser.START_TAG &&
                            type != XmlPullParser.END_DOCUMENT) {
                        // Empty.
                    }
                    if (type != XmlPullParser.START_TAG) {
                        throw new InflateException(childParser.getPositionDescription() +
                                ": No start tag found!");
                    }
                    // layout的根標(biāo)簽
                    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;
                        final TypedArray a = context.obtainStyledAttributes(
                                attrs, R.styleable.Include);
                        // 獲取<include>里設(shè)置的id
                        final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                        // 獲取<include>里設(shè)置的visibility
                        final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                        a.recycle();
                        ViewGroup.LayoutParams params = null;
                        try { // 獲取<include>里設(shè)置的寬高
                            params = group.generateLayoutParams(attrs);
                        } catch (RuntimeException e) {
                            // Ignore, just fail over to child attrs.
                        }
                        if (params == null) {
                            // 獲取layout里設(shè)置的寬高
                            params = group.generateLayoutParams(childAttrs);
                        }
                        // <include>里設(shè)置的寬高優(yōu)先于layout里設(shè)置的
                        view.setLayoutParams(params);
                        // Inflate all children.
                        rInflateChildren(childParser, view, childAttrs, true);
                        if (id != View.NO_ID) {
                            // include里設(shè)置的id優(yōu)先級(jí)高
                            view.setId(id);
                        }
                        // include里設(shè)置的visibility優(yōu)先級(jí)高
                        switch (visibility) {
                            case 0:
                                view.setVisibility(View.VISIBLE);
                                break;
                            case 1:
                                view.setVisibility(View.INVISIBLE);
                                break;
                            case 2:
                                view.setVisibility(View.GONE);
                                break;
                        }
                        group.addView(view);
                    }
                } finally {
                    childParser.close();
                }
            }
        } else {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }
        LayoutInflater.consumeChildElements(parser);
    }
  1. include里必須設(shè)置layout屬性,且layout的id必須合法;
  2. include里設(shè)置的id優(yōu)先級(jí)高于layout里設(shè)置的id,即:兩者同時(shí)設(shè)置時(shí),后者會(huì)失效;
  3. include里設(shè)置的width和height屬性?xún)?yōu)先級(jí)高于layout里設(shè)置的寬高;
  4. include里設(shè)置的visibility屬性?xún)?yōu)先級(jí)高于layout設(shè)置的visibility。

createViewFromTag方法

正常View標(biāo)簽都是通過(guò)createViewFromTag來(lái)創(chuàng)建對(duì)應(yīng)的View對(duì)象的。

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) { 
            // 真正的View標(biāo)簽名存在class屬性中
            name = attrs.getAttributeValue(null, "class");
        }
        ...
        try {
            View view;
            if (mFactory2 != null) { // 先使用Factory2
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) { // 再使用Factory
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            if (view == null && mPrivateFactory != null) { 
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    // 通過(guò)標(biāo)簽名中是否包含'.'來(lái)區(qū)分是否為自定義View
                    if (-1 == name.indexOf('.')) {
                        // 處理系統(tǒng)View
                        view = onCreateView(parent, name, attrs);
                    } else { // 自定義View用的是全限定類(lèi)名
                        // 處理自定義View
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }
            return view;
        } catch (InflateException e) {
            ...
        } catch (ClassNotFoundException e) {
            ...
        } catch (Exception e) {
            ...
        }
    }
  1. 優(yōu)先通過(guò)Factory2和Factory來(lái)創(chuàng)建View,這兩個(gè)Factory等會(huì)再說(shuō);
  2. 通過(guò)標(biāo)簽名中是否包含'.'來(lái)區(qū)分待創(chuàng)建的View是自定義View還是系統(tǒng)View;
  3. 系統(tǒng)View會(huì)在onCreateView方法中添加android.view.前綴,然后交由createView處理。

createView方法

public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        // 有緩存
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;
        try {
            if (constructor == null) { // 第一次則通過(guò)反射創(chuàng)建constructor
                // 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);
                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                // 使用的是包含Context, AttributeSet這兩個(gè)參數(shù)的構(gòu)造函數(shù)
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor); // 添加到緩存中
            } else { // 命中緩存
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) { // 先過(guò)濾
                    // 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);
                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
            }
            Object lastContext = mConstructorArgs[0];
            if (mConstructorArgs[0] == null) {
                // Fill in the context if not already within inflation.
                mConstructorArgs[0] = mContext;
            }
            Object[] args = mConstructorArgs;
            args[1] = attrs;
            // 反射創(chuàng)建View實(shí)例對(duì)象
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // 如果是ViewStub則懶加載
                // Use the same context when inflating ViewStub later.
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            mConstructorArgs[0] = lastContext;
            return view;
        } 
        ...
    }

通過(guò)反射待創(chuàng)建View的構(gòu)造函數(shù)(兩個(gè)參數(shù):Context和AttributeSet的構(gòu)造函數(shù))來(lái)實(shí)例化View對(duì)象,如果是ViewStub對(duì)象還會(huì)進(jìn)行懶加載。

LayoutInflater.Factory/Factory2

通過(guò)以上流程,使用LayoutInflater的infalte方法加載布局文件的整體流程就分析完了。但出現(xiàn)了Factory2和Factory類(lèi),它們會(huì)優(yōu)先創(chuàng)建View,我們來(lái)看看著兩個(gè)類(lèi)到底是什么!
它們都是LayoutInflater的內(nèi)部類(lèi)——兩個(gè)接口:

    public interface Factory {
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

    public interface Factory2 extends Factory {
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    }

Factory2繼承了Factory,增加了一個(gè)帶View parent參數(shù)的onCreateView重載方法。它們是在createViewFromTag中被調(diào)用的,默認(rèn)為null,說(shuō)明開(kāi)發(fā)人員可以自定義這兩個(gè)Factory,則通過(guò)它們可以改造待加載XML布局中的View標(biāo)簽,來(lái)使用自定義規(guī)則創(chuàng)建View。
來(lái)看一下它們的設(shè)置方法:

    public void setFactory(Factory factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        // 和setFactory2類(lèi)似
        ...
        }
    }

    public void setFactory2(Factory2 factory) {
        if (mFactorySet) { // 只能設(shè)置一次
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else { // 合并原有的Factory
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }

可以看到Factory和Factory2只能設(shè)置一次,否則會(huì)拋異常。
這兩個(gè)Factory的區(qū)別是什么?

Factory2 是API 11 被加進(jìn)來(lái)的;
Factory2 繼承自 Factory,也就說(shuō)現(xiàn)在直接使用Factory2即可;
Factory2 可以對(duì)創(chuàng)建 View 的 Parent 進(jìn)行操作;

總結(jié)

LayoutInflater的相關(guān)分析就這么多,文章有點(diǎn)長(zhǎng),慢慢看吧!

  1. LayoutInflater的inflate的過(guò)程的核心方法是:createViewFromTag 和 createView 方法;
  2. LayoutInflater通過(guò)PULL解析器來(lái)解析XML布局文件,通過(guò)反射來(lái)創(chuàng)建View對(duì)象;
  3. LayoutInflater.Factory只能設(shè)置一次,可以用來(lái)替換View;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容