Android 源碼分析 - LayoutInflater創建View的流程分析

??在日常開發中,我經常使用LayoutInflater將一個xml布局初始化為一個View對象,但是對它內部原理的了解卻是少之又少。今天,我們就來看看LayoutInflater

??本文主要內容:

  1. LayoutInflater創建流程。我們通過Activity或者LayoutInflater的from方法來創建一個對象,我們去看看這倆方法有啥區別。
  2. View 創建流程。主要介紹LayoutInflater將一個xml解析成為一個View經歷的過程。
  3. setContentView方法解析。分析了View的創建流程,我們再來看看是怎么初始化ActivityContentView

??本文參考文章:

  1. 反思|Android LayoutInflater機制的設計與實現

1. LayoutInflater的創建流程

??熟悉LayoutInflater的同學應該都知道,創建LayoutInflater對象有兩種方式:

  1. 通過ActivitygetLayoutInflater方法。
  2. 通過LayoutInflater的from方法。

??可是這倆方法有啥區別呢?這是本節需要解答的地方。

(1).Context結構圖

??不過在此之前,我們先來了解Context的繼承類圖。


??可能有人會問,我們分析LayoutInflater,為什么還要去了解Context的結構呢?這是因為LayoutInflater本身就是系統的一個服務,是通過ContextgetSystemService方法來獲取的。

??根據源碼我們知道,所有的系統服務都是在SystemServiceRegistry類里面進行注冊,然后統一在ContextImpl進行獲取,當然也包括LayoutInflater

(2). 兩種方法的區別

??我們通過ActivitygetLayoutInflater方法獲取的實際上是Window里面的LayoutInflater對象,而Window的LayoutInflater對象是在構造方法里面初始初始化的:

    public PhoneWindow(Context context) {
        super(context);
        mLayoutInflater = LayoutInflater.from(context);
    }

??此時這個Context就是Activity的對象。所以從本質上來看,ActivitygetLayoutInflater方法和LayoutInflater的from方法沒有很大的區別,唯一區別的在于這個Context對象的不同。我們來看一看from方法:

    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;
    }

??在from方法里面調用的是ContextgetSystemService方法,現在我們必須得了解整個Context的繼承體系。
??假設這里的ContextActivity,那么這里調用的就是ContextgetSystemService方法

    @Override
    public Object getSystemService(String name) {
        return mBase.getSystemService(name);
    }

??那這里的mBase又是什么呢?從上面的類圖我們知道是ContextImpl的對象,怎么來證明呢?

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // ······
        // 1. 創建ContextImpl的對象
        ContextImpl appContext = createBaseContextForActivity(r);
        // ······
        // 2. 調用Activity的attach方法
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);
        // ······
    }
    //---------------Activity--------------------------
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
         // 將ContextImpl傳遞給父類
        attachBaseContext(context);
        // ·······
    }
    //---------------ContextWapper---------------------
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }

??整個調用鏈非常的清晰,分別是:ActivityThread#performLaunchActivity -> Activity#attach -> ContextWapper#attachBaseContext

??然后,我們再去看看ContextImplgetSystemService方法:

    @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

??最終的對象是從SystemServiceRegistry里面獲取的。

2. View的創建流程

??LayoutInflater是通過inflate方法將一個xml布局解析成為一個View。我們都知道inflate方法通常有三個參數,分別是:resourcerootattachToRoot,表示的含義如下:

  1. resource:xml布局的id。
  2. root:解析成之后的View的父View,此參數只在attachToRoot為true才生效。
  3. attachToRoot:決定解析出來的View是否添加到root上。

??有人可能會好奇,為什么需要第三個參數,這是因為我們將xml解析成View不一定立即需要添加到一個ViewGroup中去,這是為什么呢?想一想RecyclerViewRecyclerView在初始化ItemView時,不是立即將ItemView添加進去,而是當ItemView 進入屏幕可見區域時才會添加,因為RecyclerView有預加載機制,會加載一部分屏幕外的ItemView

??我們先看一下inflate方法:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            try {
                // ······
                // 1. 如果是merge標簽,直接繞過merge標簽,
                // 解析merge標簽下面的View。
                if (TAG_MERGE.equals(name)) {
                    // ······
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // 2.創建View
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    // ······
                    // 3.遞歸解析children
                    rInflateChildren(parser, temp, attrs, true);
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                    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(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            }
            return result;
        }
    }

??inflate方法里面一種做了2件事:

  1. 如果根View是merge,直接遞歸解析它的子View。
  2. 如果根View不是merge,先解析根View,然后在遞歸解析它所有的child。

??我們分為兩步來看,先看一下解析根標簽。

(1). 根View的解析

??根View的解析與child的解析不一樣,是通過createViewFromTag方法來完成的:

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
            // ······
            View view;
            // 1. 如果mFactory2不為空,優先讓mFactory2處理
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            // 2. 如果上面解析為空,再使用mPrivateFactory常識這解析
            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    // 如果是系統widget包下的控件
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else { // 如果是第三方包或者自定義的控件
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }
            return view;
             // ······
    }

??inflate方法里面主要做了三件事:

  1. 首先使用mFactory2mFactory來嘗試著創建View對象。mFactory2mFactory二者有且只能有一個有值,所以只需要調用其中一個就行了。
  2. 如果第一步中的兩個工廠都無法創建View對象,再嘗試著使用mPrivateFactory對象創建。不過通常來說,這個對象都是為空的。
  3. 最后一步就是走兜底邏輯。這里的兜底有一點的特殊:如果View是widget的控件,會先在前面加一個android.wiget.的前綴,再行創建View;其次,如果是其他包下的控件,比如說,androidX和自定義的控件,就直接創建View對象。

??關于第一點,我還想介紹一下,Google爸爸之所以要設計兩個工廠類,主要有3個方面的考慮:

  1. 兼容性,后面發布的版本可以兼容之前的版本,比如說,AppCompatActivity是新推出來的組件,所以在新版本上使用的mFactory2,舊版本就走原來的原來邏輯,也就是默認的onCreateView方法。
  2. 擴展性,如果開發者需要自定義一種全局的樣式或者手動創建解析View,可以直接給LayoutInflayer設置Factory,用來達到自己的目的。
  3. 提升性能,這一點可以從可以從AppCompatActivity里面看出。AppCompatActivity的內部給LayoutInflayer設置了一個Factory2,也就是AppCompatDelegateImpl對象。AppCompatDelegateImpl在解析xml時,會先判斷當前View是否基礎控件,比如說,ButtonTextView或者ImageView等,如果是的話,可以通過new 的方式創建對應的AppCompatXXX對象。之所以說它提升性能,是因為它在解析基礎控件時,不再通過反射,而是通過new的方式創建的。

??上面的第三點可能有點復雜,我們可以直接看一下AppCompatDelegateImplcreateView方法。由于createView內部調用了AppCompatViewInflatercreateView方法,所以這里我們直接看AppCompatViewInflatercreateView方法:

    final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        final Context originalContext = context;
        // ······
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Button":
                view = createButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "EditText":
                view = createEditText(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Spinner":
                view = createSpinner(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageButton":
                view = createImageButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckBox":
                view = createCheckBox(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RadioButton":
                view = createRadioButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckedTextView":
                view = createCheckedTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "AutoCompleteTextView":
                view = createAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "MultiAutoCompleteTextView":
                view = createMultiAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RatingBar":
                view = createRatingBar(context, attrs);
                verifyNotNull(view, name);
                break;
            case "SeekBar":
                view = createSeekBar(context, attrs);
                verifyNotNull(view, name);
                break;
            default:
                view = createView(context, name, attrs);
        }
        // ······
        return view;
    }

??在默認的情況下,創建View對象的真正操作在createView方法里面,我們可以來看看:

    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
            // ······
            // 如果緩存中沒有View的構造方法對象,
            // 那么就創建一個,并且放入緩存中去。
            if (constructor == null) {
                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);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
                if (mFilter != null) {
                    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) {
                mConstructorArgs[0] = mContext;
            }
            Object[] args = mConstructorArgs;
            args[1] = attrs;

            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            mConstructorArgs[0] = lastContext;
            return view;
            // ······
    }

??從這里,我們就可以知道,LayoutInflater創建View的本質就是Java反射,所以在我們日常開發過程中,盡量不要套太深的布局,畢竟反射的性能是有目共睹的。

(2). children的解析

??children的解析實際上是在rInflate方法里面進行的,我們直接來看源碼:

    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        // ······
        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();
            // requestFocus標簽
            if (TAG_REQUEST_FOCUS.equals(name)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) { // tag標簽
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) { // include標簽
                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或者ViewGroup
                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);
                viewGroup.addView(view, params);
            }
        }
        // ······
    }

??children的遍歷就像是一個樹的遍歷,就是一種廣搜的思想,這里就不過多的討論了。

3. AppCompatActivity的setContentView方法解析

??說完了上面的原理,最后我們在來看看AppCompatActivitysetContentView方法。在LayoutInflater方面,AppCompatActivity相比于Activity,給LayoutInflater設置了一個Factory2,也就是上面討論的東西。

??這里我們不再討論之前談論過的東西,而是看一個有趣的東西,我也不知道Google爸爸是怎么想的。
??AppCompatActivityonCreate方法內部會給LayoutInflater設置一個Factory2對象,整個調用鏈是:AppCompatActivity#onCreate -> AppCompatDelegateImpl#installViewFactory -> LayoutInflaterCompat#setFactory2 -> LayoutInflaterCompat#forceSetFactory2。我們直接來看setFactory2方法:

    private static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
        if (!sCheckedField) {
            try {
                sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2");
                sLayoutInflaterFactory2Field.setAccessible(true);
            } catch (NoSuchFieldException e) {
                Log.e(TAG, "forceSetFactory2 Could not find field 'mFactory2' on class "
                        + LayoutInflater.class.getName()
                        + "; inflation may have unexpected results.", e);
            }
            sCheckedField = true;
        }
        if (sLayoutInflaterFactory2Field != null) {
            try {
                sLayoutInflaterFactory2Field.set(inflater, factory);
            } catch (IllegalAccessException e) {
                Log.e(TAG, "forceSetFactory2 could not set the Factory2 on LayoutInflater "
                        + inflater + "; inflation may have unexpected results.", e);
            }
        }
    }

??看到這個神奇操作沒?萬萬沒想到Google是通過反射的方式來給mFactory2方法。爸爸為啥要這樣做呢?我猜測是setFactory2方法的坑,我們來看看:

    public void setFactory2(Factory2 factory) {
        if (mFactorySet) {
            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 {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }

??只要Factory被設置過,不論是Factory還是Factory2,都不允許被再次設置。所以,我猜測是,爸爸為了成功給mFactory2設置上值,通過反射來繞開這種限制,這也是在是無奈。

??設置了Factory2工廠類之后,就是調用setContentView方法來給Activity設置ContentView。我們這里直接來看一下AppCompatDelegateImplsetContentView方法:

    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

??從這里我們可以看出來,contentView是通過LayoutInflater加載出來的。具體的細節就不再討論了,上面已經詳細的分析過了。

4. 總結

??到此為止,本文算是為止。總的來說,本文還是簡單的(隱約的感覺到,本文有點水),在這里,我們對本文的內容做一個簡單的總結。

  1. ActivitygetLayoutInflater方法和LayoutInflater在本質沒有任何的區別,最終都會調用到ContextImplgetSystemService方法里面去。
  2. LayoutInflater初始化View分為三步:1.調用mFactory2或者mFactory方法來解析xml;2. 通過mPrivateFactory來解析xml;3. 通過onCreateView或者createView方法來解析xml。除了在AppCompatDelegateImpl在解析基礎控件時使用的是new方式,其余幾乎都是反射方式來創建View,所以在布局中盡可能的少寫View或者盡量不要書寫層次很深的布局。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。