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

  在平常開發中,我常用LayoutInflater將一個xml佈局初始化爲一個View對象,可是對它內部原理的瞭解倒是少之又少。今天,咱們就來看看LayoutInflaterandroid

  本文主要內容:緩存

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

  本文參考文章:bash

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

1. LayoutInflater的建立流程

  熟悉LayoutInflater的同窗應該都知道,建立LayoutInflater對象有兩種方式:app

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

  但是這倆方法有啥區別呢?這是本節須要解答的地方。ide

(1).Context結構圖

  不過在此以前,咱們先來了解Context的繼承類圖。 佈局

  可能有人會問,咱們分析LayoutInflater,爲何還要去了解Context的結構呢?這是由於LayoutInflater自己就是系統的一個服務,是經過ContextgetSystemService方法來獲取的。post

  根據源碼咱們知道,全部的系統服務都是在SystemServiceRegistry類裏面進行註冊,而後統一在ContextImpl進行獲取,固然也包括LayoutInflater性能

(2). 兩種方法的區別

  咱們經過ActivitygetLayoutInflater方法獲取的其實是Window裏面的LayoutInflater對象,而Window的LayoutInflater對象是在構造方法裏面初始初始化的:ui

public PhoneWindow(Context context) {
        super(context);
        mLayoutInflater = LayoutInflater.from(context);
    }
複製代碼

  此時這個Context就是Activity的對象。因此從本質上來看,ActivitygetLayoutInflater方法和LayoutInflater的from方法沒有很大的區別,惟一區別的在於這個Context對象的不一樣。咱們來看一看from方法:this

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或者儘可能不要書寫層次很深的佈局。
相關文章
相關標籤/搜索