Android 之 LayoutInflater 全面解析

UI 優(yōu)化系列專題,來聊一聊 Android 渲染相關(guān)知識,主要涉及 UI 渲染背景知識如何優(yōu)化 UI 渲染兩部分內(nèi)容。


UI 優(yōu)化系列專題
  • UI 渲染背景知識

View 繪制流程之 setContentView() 到底做了什么腕唧?
View 繪制流程之 DecorView 添加至窗口的過程
深入 Activity 三部曲(3)View 繪制流程
Android 之 LayoutInflater 全面解析
關(guān)于渲染,你需要了解什么瘾英?
Android 之 Choreographer 詳細(xì)分析

  • 如何優(yōu)化 UI 渲染

Android 之如何優(yōu)化 UI 渲染(上)
Android 之如何優(yōu)化 UI 渲染(下)


對于 LayoutInflater枣接,相信每個 Android 開發(fā)人員都不會感到陌生。業(yè)界一般稱它為布局解析器(或填充器)缺谴,翻開 LayoutInflater 源碼發(fā)現(xiàn)它是一個抽象類但惶,我們先來看下它的自我介紹。

LayoutInflater 就是將 XML 布局文件實(shí)例化為對應(yīng)的 View 對象湿蛔,LayoutInflater 不能直接通過 new 的方式獲取膀曾,需要通過 Activity.getLayoutInflater() 或 Context.getSystemService() 獲取與當(dāng)前 Context 已經(jīng)關(guān)聯(lián)且正確配置的標(biāo)準(zhǔn) LayoutInflater。

也就是說 LayoutInflater 不能被外部實(shí)例化阳啥,只能通過系統(tǒng)提供的固有方式獲取添谊,但也正因如此,相信很多開發(fā)人員對它的認(rèn)識仍然停留在如下代碼:

final View content = LayoutInflater.from(this).inflate(R.layout.content, root, false);

今天我們就從源碼的角度察迟,進(jìn)一步分析 LayoutInflater 的工作原理斩狱,主要涉及到如下幾塊兒內(nèi)容:

  • LayoutInflater 創(chuàng)建過程與實(shí)際類型
  • LayoutInflater 的布局解析原理
  • 不容忽視的 View 創(chuàng)建耗時
  • LayoutInflater 的高階使用技巧

LayoutInflater 創(chuàng)建過程與實(shí)際類型

系統(tǒng)在 Context 中默認(rèn)提供的兩種獲取 LayoutInflater 的方式如下:

final LayoutInflater getLayoutInflater = getLayoutInflater();
final LayoutInflater getSystemServiceInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

不過它們與直接使用 LayoutInflater.from() 除了 API 不同并沒有本質(zhì)上的差異耳高,所以我們直接從 LayoutInflater 的 from 方法開始入手:

public static LayoutInflater from(Context context) {
    // 這里的context可以是Activity、Application所踊、Service泌枪。
    LayoutInflater LayoutInflater =
            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    if (LayoutInflater == null) {
        throw new AssertionError("LayoutInflater not found.");
    }
    return LayoutInflater;
}

注意參數(shù) Context,這里可以是 Activity秕岛、Application 或 Service碌燕。不知大家是否有跟蹤過它們之間有什么區(qū)別嗎?接下類我們就重點(diǎn)分析下這部分內(nèi)容瓣蛀。

Context 的 getSystemService 方法默認(rèn)是抽象的,看下它在 Context 中的聲明:

public abstract @Nullable String getSystemServiceName(@NonNull Class<?> serviceClass);

這里我們主要以 Activity 為例(其他類型也會在此引申出)雷厂,在 Activity 的直接父類 ContextThemeWrapper 中重寫了 getSystemService 方法惋增。

@Override
public Object getSystemService(String name) {
    if (LAYOUT_INFLATER_SERVICE.equals(name)) {
        if (mInflater == null) {
            // 每個Activity都有自己獨(dú)一無二的Layoutflater
            // 這里首先拿到在SystemServiceRegistry中注冊的Application的Layoutflater
            // 然后根據(jù)該創(chuàng)建屬于每個Activity的PhoneLayoutInflater
            mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
        }
        return mInflater;
    }
    return getBaseContext().getSystemService(name);
}

可以看到,Activity 對于 LayoutInflater 服務(wù)的 LAYOUT_INFLATER_SERVICE 做了單獨(dú)處理改鲫,使每個 Activity 都有其獨(dú)立的 LayoutInflater诈皿。否則直接通過 getBaseContext().getSystemService() 獲取相關(guān)服務(wù)。

這里像棘,我們有必要先跟蹤下 getBaseContext()稽亏,它的聲明在 ContextThemeWrapper 的直接父類 ContextWrapper 中,如下:

public Context getBaseContext() {
    // mBase的實(shí)際類型是ContextImpl
    return mBase;
}

注意:ContextWrapper 是 Application 和 Service 的直接父類

mBase 的實(shí)際類型是 ContextImpl缕题。在 Android 中截歉,Application、Service 和 Activity 在創(chuàng)建后會首先回調(diào)其 attach 方法烟零,并在該方法為其關(guān)聯(lián)一個 ContextImpl 對象(該部分源碼可以參考 Activity / Application 的創(chuàng)建過程在 ActivityThread 中)瘪松。

故,這里的 getSystemService() 實(shí)際調(diào)用到 ContextImpl 的 getSystemService 方法:

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

可以看到在 ContextImpl 內(nèi)部锨阿,又委托給了 SystemServiceRegistry宵睦。SystemServiceRegistry 是應(yīng)用進(jìn)程的系統(tǒng)服務(wù)注冊機(jī),在其內(nèi)部的靜態(tài)代碼塊中默認(rèn)注冊了大量系統(tǒng)服務(wù)墅诡,包括 WINDOW_SERVICE壳嚎、LOCATION_SERVICE 、AUDIO_SERVICE 等等末早,這里我們重點(diǎn)看下 LayoutInflater 的注冊過程:

static {
    // ... 省略

    registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                // LayoutInflater 的實(shí)際類型是PhoneLayoutInflater
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }}
    );

    // ... 省略
}

然后通過 SystemServiceRegistry 的 getSystemService 方法獲取相關(guān)服務(wù)過程如下:

public static Object getSystemService(ContextImpl ctx, String name) {
    // 根據(jù)name在SYSTEM_SERVICE_FETCHERS獲取該服務(wù)類型的ServiceFetcher
    ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    // 通過該Fetcher獲取對應(yīng)的服務(wù)類型烟馅,如果是第一次調(diào)用fetcher.getServie()
    // 會調(diào)用其內(nèi)部的createSercice()
    return fetcher != null ? fetcher.getService(ctx) : null;
}

SYSTEM_SERVICE_FETCHERS 是一個靜態(tài)的 Map 容器,上面在 static {} 代碼塊中注冊的服務(wù)都將保存在該容器然磷。這里首先根據(jù)服務(wù)的 name 獲取對應(yīng)的 Fetcher焙糟,然后通過該 Fetcher 的 getService 方法創(chuàng)建相應(yīng) LayoutInflater 對象。

當(dāng)我們首次獲取某個服務(wù)類型時样屠,fetcher.getService() 會執(zhí)行其內(nèi)部的 createService() 創(chuàng)建對應(yīng)服務(wù)穿撮,然后每個服務(wù)都會被保存在 SystemServiceRegistry 中缺脉,這里實(shí)際間接保存在 Fetcher 中。

在 createService 方法悦穿,我們發(fā)現(xiàn) LayoutInflater 的實(shí)際類型是 PhoneLayoutInflater攻礼,類定義如下:

public class PhoneLayoutInflater extends LayoutInflater {

    /**
      * 系統(tǒng)默認(rèn) View 目錄
      */
    private static final String[] sClassPrefixList = {
            "android.widget.",
            "android.webkit.",
            "android.app."
    };

    /**
     * Application 級的 LayoutInflater 使用該構(gòu)造方法,就是在 
     * SystemServiceRegistry 的靜態(tài)代碼款中注冊的栗柒。
     */
    public PhoneLayoutInflater(Context context) {
        super(context);
    }

    /**
     *  在Activity中使用LayoutInflater.form()時候調(diào)用該構(gòu)造方法
     */
    protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
        super(original, newContext);
    }

   /**
     * 默認(rèn)View 的創(chuàng)建流程這里(非自定義控件)
     */
    @Override
    protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            try {
                // 首先查找:android/widget目錄下礁扮,如 Seekbar
                // 然后查找:android/webkit目錄下,如 WebView
                // 最后查找:android/app目錄下瞬沦,如 ActionBar
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {}
        }

        // 如果以上都不能滿足太伊,就是:android/view 目錄下
        return super.onCreateView(name, attrs);
    }

    /**
     * newContext是具體的Activity
     */
    public LayoutInflater cloneInContext(Context newContext) {
        // 每個Activity都由其獨(dú)一無二的 Layoutflater
        return new PhoneLayoutInflater(this, newContext);
    }
}

注意 PhoneLayoutInflater 重寫了 LayoutInflater 的 onCreateView 方法,這在后面 View 的創(chuàng)建階段將會分析到逛钻。

注意觀察 PhoneLayoutInflater 的最后 cloneInContext()僚焦,重新回到上面 ContextThemeWrapper 的 getSystemService 方法,大家是否注意到通過 LayoutInflater.from() 獲取到 LayoutInflater 對象后曙痘,又調(diào)用其 cloneInContext 方法芳悲,該方法實(shí)際調(diào)用到 PhoneLayoutInflater 的 cloneInContext 方法。

此時會為每個 Activity 單獨(dú)創(chuàng)建一個 LayoutInflater边坤。之所以叫做 “clone”名扛,是因?yàn)椋合到y(tǒng)默認(rèn)會將進(jìn)程級的 LayoutInflater 配置給每個 Activity 的 LayoutInflater,這也符合了 LayoutInflater 的自我介紹 “且正確配置的標(biāo)準(zhǔn) LayoutInflater”茧痒“谷停看下這一過程(實(shí)際是配置內(nèi)部的 Factory):

// original是應(yīng)用進(jìn)程級的LayoutInflater,即在SystemServiceRegistry中保存
// 的LayoutInflater實(shí)例旺订。
protected LayoutInflater(LayoutInflater original, Context newContext) {
    mContext = newContext;
    mFactory = original.mFactory;
    mFactory2 = original.mFactory2;
    mPrivateFactory = original.mPrivateFactory;
    setFilter(original.mFilter);
}

跟蹤到這惹苗,LayoutInflater 的創(chuàng)建及實(shí)際類型就已經(jīng)非常清晰了,并且根據(jù)不同的 Context 參數(shù)耸峭,我們可以總結(jié)出如下幾條規(guī)律:

  • 由于 Application 和 Service 都是 ContextWrapper 的直接子類桩蓉,它們并沒有對 getSystemService 方法做單獨(dú)處理。故都是通過 ContextImpl 獲取的同一個劳闹,也就是保存在 SystemServiceRegistry 中的 LayoutInflater院究。

  • 每個 Activity 都有其獨(dú)一無二的 LayoutInflater,它的實(shí)際類型是 PhoneLayoutInflater本涕。當(dāng)首次獲取某個 Activity 的 LayoutInflater 時业汰,系統(tǒng)首先會根據(jù) Application 級的 LayoutInflater 創(chuàng)建并配置對應(yīng) Activity 的 LayoutInflater样漆。


LayoutInflater 布局解析

分析完了 LayoutInflater 的創(chuàng)建過程晦闰,接下來我們看下大家最熟悉的 xml 布局解析階段 inflate鳍怨。

final View content = LayoutInflater.from(this).inflate(R.layout.content, root, false);

有關(guān) LayoutInflater 的布局解析過程在之前已經(jīng)做過詳細(xì)的分析鞋喇,具體你可以參考《View 繪制流程之 setContentView() 到底做了什么 ?》眉撵,這里我們再來回顧與總結(jié)下涉及的核心問題:

  • <include /> 標(biāo)簽為什么不能作為布局的根節(jié)點(diǎn)侦香?
  • <merge /> 標(biāo)簽為什么要作為布局資源的根節(jié)點(diǎn)?
  • inflate ( int resource, ViewGroup root, boolean attachToRoot) 參數(shù) root 和 attachToRoot 的作用和規(guī)則纽疟?

這里直接跟蹤布局解析的核心過程 inflate 方法:

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

        final Context inflaterContext = mContext;
        // 獲取在XML設(shè)置的屬性
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        // 注意root容器在這里罐韩,在我們當(dāng)前分析中該root就是mContentParent
        View result = root;

        try {
            // 查找xml布局的根節(jié)點(diǎn)
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

            // 找到起始根節(jié)點(diǎn)
            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }
            // 獲取到節(jié)點(diǎn)名稱
            final String name = parser.getName();

            // 判斷是否是merge標(biāo)簽
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    // 此時如果ViewGroup==null,與attachToRoot==false將會拋出異常
                    // merge必須添加到ViewGroup中,這也是merge為什么要作為布局的根節(jié)點(diǎn)污朽,它要添加到上層容器中
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }

                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // 否則創(chuàng)建該節(jié)點(diǎn)View對象
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                // 如果contentParent不為null散吵,在分析setContentView中,這里不為null
                if (root != null) {
                    // 通過root(參數(shù)中的 ViewGroup)創(chuàng)建對應(yīng)LayoutParams
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // 如果不需要添加到 root膘壶,直接設(shè)置該View的LayoutParams
                        temp.setLayoutParams(params);
                    }
                }

                // 解析Child
                rInflateChildren(parser, temp, attrs, true);

                if (root != null && attachToRoot) {
                    // 添加到ViewGroup
                    root.addView(temp, params);
                }

                if (root == null || !attachToRoot) {
                    // 此時布局根節(jié)點(diǎn)為temp
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {} catch (Exception e) {} finally {
            mConstructorArgs[0] = lastContext;
            mConstructorArgs[1] = null;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        return result;
    }
}

while 循環(huán)部分错蝴,首先找到 XML 布局文件的根節(jié)點(diǎn)洲愤,如果未找到:if (type != XmlPullParser.START_TAG) 直接拋出異常颓芭。否則獲取到該節(jié)點(diǎn)名稱,判斷如果是 merge 標(biāo)簽柬赐,此時需要注意參數(shù) root 和 attachToRoot亡问,root 必須不為null,并且 attachToRoot 必須為 true肛宋,即 merge 內(nèi)容必須要添加到 root 容器中州藕。

如果不是 merge 標(biāo)簽,此時根據(jù)標(biāo)簽名 name 調(diào)用 createViewFromTag() 創(chuàng)建該 View 對象酝陈,rInflate 和 rInflateChildren 都是去解析子 View,rInflateChildren 方法實(shí)際也是調(diào)用到了 rInflate 方法:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {
    //還是調(diào)用rInflate方法
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

區(qū)別在于最后一個參數(shù) finishInflate,它的作用是標(biāo)志當(dāng)前 ViewGroup 樹創(chuàng)建完成后回調(diào)其 onFinishInflate 方法待牵。

如果根標(biāo)簽是 merge缨该,此時 finishInflate 為 false蛤袒,這也很容易理解汗盘,此時的父容器為 inflate() 傳入的 ViewGroup,它是不需要再次回調(diào) onFinishInflate() 菱阵,該過程如下:

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

        // 獲取到節(jié)點(diǎn)名稱
        final String name = parser.getName();

        if (TAG_REQUEST_FOCUS.equals(name)) {
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            // include標(biāo)簽
            if (parser.getDepth() == 0) {
                // include如果為根節(jié)點(diǎn)則拋出異常了
                // include不能作為布局文件的根節(jié)點(diǎn)
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            // 如果此時包含merge標(biāo)簽嫡锌,此時也會拋出異常
            // merge只能作為布局文件的根節(jié)點(diǎn)
            throw new InflateException("<merge /> must be the root element");
        } else {
            // 創(chuàng)建該節(jié)點(diǎn)的View對象
            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);
        }
    }

    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }

    if (finishInflate) {
        // 回調(diào)ViewGroup的onFinishInflate方法
        parent.onFinishInflate();
    }
}

while 循環(huán)部分蛛倦,parser.next() 獲取下一個節(jié)點(diǎn),如果獲取到節(jié)點(diǎn)名為 include且改,此時 parse.getDepth() == 0 表示根節(jié)點(diǎn),直接拋出異常:“<include /> cannot be the root element”慨蓝,即 <include /> 不能作為布局的根節(jié)點(diǎn)

如果此時獲取到節(jié)點(diǎn)名稱為 merge济丘,也是直接拋出異常了疟赊,即 <merge /> 只能作為布局的根節(jié)點(diǎn):“<merge /> must be the root element”

否則創(chuàng)建該節(jié)點(diǎn)對應(yīng) View 對象吉执,rInflateChildren 遞歸完成以上步驟,并將解析到的 View 添加到其直接父容器:viewGroup.addView(view, params)咕宿。

注意方法的最后通知調(diào)用每個 ViewGroup 的 onFinishInflate(),大家是否有注意到這其實(shí)是入棧的操作试浙,即最頂層的 ViewGroup 最后回調(diào) onFinishInflate()力细。


至此眠蚂,我們可以回答上面提出的相關(guān)問題了昔脯,先來通過一張流程圖加深下對 LayoutInflater 的解析過程(該圖基于 setContentView()):

  • 如果布局根節(jié)點(diǎn)為 merge ,會判斷 inflate 方法參數(shù) if ( root != null && attachToRoot == true )鲸拥,表示布局文件要直接添加到 root 中捏浊,否則拋出異常:“<merge /> can be used only with a valid ViewGroup root and attachToRoot=true”

  • 繼續(xù)解析子節(jié)點(diǎn)的過程中如果再次解析到 merge 標(biāo)簽劣领,則直接拋出異常:“<merge /> must be the root element”。既 <merge /> 標(biāo)簽必須作為布局文件的根節(jié)點(diǎn)惊暴。

  • 如果解析到節(jié)點(diǎn)名稱為 include,會判斷當(dāng)前節(jié)點(diǎn)深度是否為 0油啤,0 表示當(dāng)前處于根節(jié)點(diǎn),此時直接拋出異常:“<include /> cannot be the root element”幽告。即 <include /> 不能作為布局文件的根節(jié)點(diǎn)


不容忽視的 View 創(chuàng)建耗時

在分析 XML 布局解析階段冻河,我們忽略了一個非常重要的 View 創(chuàng)建過程 createViewFromTag 方法,接下來我們就詳細(xì)跟蹤下這部分內(nèi)容廷蓉。

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                           boolean ignoreThemeAttr) {
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }

    // Apply a theme wrapper, if allowed and one is specified.
    if (!ignoreThemeAttr) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
    }

    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }

    try {
        View view;
        // 首先通過mFactory2加載View
        if (mFactory2 != null) {
            // 交給Factory2工程創(chuàng)建
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            // 其次通過mFactory工程創(chuàng)建
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        // 私有工廠
        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }
        // 如果上邊都沒有滿足,走默認(rèn)
        if (view == null) {
            // 保存最后一次上下文
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                // 判斷name是否包含點(diǎn)马昙,表示是否是自定義控件
                // 比如如果類名是 ImageView桃犬,實(shí)際是反射創(chuàng)建,也就是要通過類的全限定名進(jìn)行加載
                // Android 系統(tǒng)默認(rèn)加載View目錄有三個在PhoneLayoutInflater中
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    // 否則是 custom view
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }
        return view;
    } catch (InflateException e) {} catch (ClassNotFoundException e) {} catch (Exception e) {}
}

注意行楞,在 createViewFromTag 方法會依次判斷 mFactory2攒暇、mFactory、mPrivateFactory 是否為 null。也就是會依次根據(jù) mFactory2解愤、mFactory、mPrivateFactory 來創(chuàng)建 View 對象宠互∷副海看下他們在 LayoutInflater 中的聲明:

public abstract class LayoutInflater {

    // ... 省略

    private Factory mFactory;
    private Factory2 mFactory2;
    private Factory2 mPrivateFactory;
    private Filter mFilter;

    // ... 省略
}

Factory 和 Factory2 都屬于 LayoutInflater 的內(nèi)部接口岔激,聲明如下:

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

Factory2 是在 Android 3.0 版本添加乐尊,兩者功能基本一致(Factory2 優(yōu)先級高于 Factory)世澜。其實(shí)前面我們講到 Activity 的 LayoutInflater 是通過 cloneInContext 方法創(chuàng)建诺舔,這一過程就是要復(fù)用它的 mFactory2、mFactory悉患、mPrivateFactory 這樣不需要再重新設(shè)置了(關(guān)聯(lián)且正確配置的標(biāo)準(zhǔn) LayoutInflater)烁涌。

  • Factory 只包括一個核心的 onCreateView 方法谋币,即創(chuàng)建 View 對象的過程帜乞。這一特性為我們提供了 View 創(chuàng)建過程的 Hack 機(jī)會武翎,例如替換某個 View 類型垫毙,動態(tài)換膚消请、View 復(fù)用等卢厂。這部分內(nèi)容在后面高階技巧中再詳細(xì)介紹。

如果以上條件都不滿足惠啄,則執(zhí)行 LayoutInflater 的默認(rèn) View 創(chuàng)建流程慎恒,注意這里首先會根據(jù)解析到的標(biāo)簽名 name 是否包含 “.” ,用于判斷當(dāng)前標(biāo)簽是否屬于自定義控件類型撵渡。

  • 類加載器只能通過類的全限定名來加載對應(yīng)的類融柬。例如 ImageView,此時系統(tǒng)要為其補(bǔ)齊前綴后變?yōu)椋骸癮ndroid.weight.ImageView”趋距。但是我們在 XML 中只是聲明了 View 的名稱如下(自定義 View 除外粒氧,因?yàn)槠渎暶饕呀?jīng)是全限定名):
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- 系統(tǒng)提供的 View -->
    <ImageView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"/>

     <!-- 自定義 View,已經(jīng)是全限定名 -->
     <com.xxx.android.custom.CircleView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"/>

</LinearLayout>

接下來棚品,我們就看下系統(tǒng)是如何加載對應(yīng) View 標(biāo)簽以及創(chuàng)建過程:

protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}

注意: 由于 LayoutInflater 的實(shí)際類型為 PhoneLayoutInflater靠欢,還記得上面貼出的 PhoneLayoutInflater 中重寫了 onCreateView 方法

protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
   for (String prefix : sClassPrefixList) {
       try {
           // 首先查找:android/widget目錄下,如 Seekbar
           // 然后查找:android/webkit目錄下铜跑,如 WebView
           // 最后查找:android/app目錄下门怪,如 SurfaceView
           View view = createView(name, prefix, attrs);
           if (view != null) {
               return view;
           }
        } catch (ClassNotFoundException e) {}
    }

    // 如果以上都不能滿足,就是:android/view 目錄下
    return super.onCreateView(name, attrs);
}

這里將依次遍歷原生 View 所在目錄锅纺,這一過程就是為解析到的 View 標(biāo)簽補(bǔ)齊前綴組成類的全限定名掷空,然后通過 ClassLoader 進(jìn)行加載,直到加載成功囤锉,否則拋出異常坦弟。Android 系統(tǒng)提供的 View 視圖目錄如下(其實(shí)這里還包括一個 android.view,它將作為最后):

private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
};

View 的創(chuàng)建過程 createView 方法如下官地,name 表示在 XML 中的標(biāo)簽名稱如 “ImageView”酿傍,prefix 表示 ImageView 標(biāo)簽的前綴為“android.widget”,組成 “android.widget.ImageView” 交給 ClassLoader 嘗試加載:

public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
    // 查找name類型的的構(gòu)造方法
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    // verifyClassLoader方法驗(yàn)證是否是同一個ClassLoader
    if (constructor != null && !verifyClassLoader(constructor)) {
        constructor = null;
        // 如果ClassLoader不匹配驱入,則刪除該類型的緩存
        sConstructorMap.remove(name);
    }
    Class<? extends View> clazz = null;

    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

        if (constructor == null) {
            // 類未在緩存中找到赤炒,通過ClassLoader根據(jù)類全限定名加載,并緩存到sConstructorMap容器
            clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);

            // mFilter 可以攔截是否被允許創(chuàng)建該視圖類的對象
            if (mFilter != null && clazz != null) {
                boolean allowed = mFilter.onLoadClass(clazz);
                if (!allowed) {
                    failNotAllowed(name, prefix, attrs);
                }
            }
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            // 進(jìn)行緩存
            sConstructorMap.put(name, constructor);
        } else {
            // 否則判斷是否設(shè)置了 Filter
            // Filter 的主要作用是攔截當(dāng)前視圖類是否可以創(chuàng)建視圖對象
            if (mFilter != null) {
                // 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) {
                        // 這里將會直接拋出異常表示不允許創(chuàng)建該視圖類對象
                        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;

        // 發(fā)射創(chuàng)建該View對象
        final View view = constructor.newInstance(args);
        if (view instanceof 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;

    } catch (NoSuchMethodException e) {
        // ... 省略所有異常報錯
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}
  • sConstructorMap 是一個 static Map 容器亏较,用于緩存某個 View 類的構(gòu)造方法莺褒,這算是一層優(yōu)化。避免每次 loadClass() 執(zhí)行類的加載過程雪情。

  • 注意查看 ClassLoader 的 loadClass 方法遵岩,將 prefix 和 name 組成類的全限定名進(jìn)行加載,如果成功加載對應(yīng)類巡通,獲取它的構(gòu)造方法并緩存在 sConstructorMap 容器尘执。

  • mFilter 是一個 Filter 類型對象舍哄,用于決定是否允許創(chuàng)建某個 View 類的對象,它的聲明如下:

public interface Filter {

   // 參數(shù)clazz正卧,表示即將要創(chuàng)建視圖對象的類對象
   // 返回值 true 表示允許創(chuàng)建該視圖類對象蠢熄,否則返回 false跪解,不允許炉旷。
   boolean onLoadClass(Class clazz);

}
  • 最后通過反射 newInstance() 創(chuàng)建對應(yīng)的 View 對象并返回。

LayoutInflater 在 View 對象的創(chuàng)建過程使用了大量反射叉讥,如果某個布局界面內(nèi)容又較復(fù)雜窘行,該過程耗時是不容忽視的。更極端的情況可能是某個 View 的創(chuàng)建過程需要執(zhí)行 4 次图仓,例如 SurfaceView罐盔,因?yàn)橄到y(tǒng)默認(rèn)遍歷規(guī)則依次為 android/weight、android/webkit 和 android/app救崔,但是由于 SurfaceView 屬于 android/view 目錄下惶看,故此時需要第 4 次 loadClass 才可以正確加載,這個效率會有多差(在 AppCompatActivity 中該過程略有改善六孵,后面的高階階段介紹)纬黎!

至此 LayoutInflater 的工作原理就已經(jīng)分析完了,個人認(rèn)為 Android 系統(tǒng)對布局 View 的創(chuàng)建過程處理的過于簡單粗暴了劫窒。但是換個角度本今,這也給我們留下更多優(yōu)化和學(xué)習(xí)的空間。


LayoutInflater 的高階使用技巧

通過上面的分析主巍,其實(shí)大家也能猜到這部分主要圍繞 LayoutInflater.Factory 展開冠息,接下來我們就來看下利用 LayoutInflater.Factory 可以幫助我們完成哪些工作?

1. Activity 默認(rèn)實(shí)現(xiàn)了 LayoutInflater.Factory2 接口

這可能也是很多開發(fā)人員所不了解的孕索,其實(shí)我們完全可以在自己的 Xxx-Activity 中重寫對應(yīng)方法逛艰,實(shí)現(xiàn)例如 View 替換、復(fù)用等機(jī)制搞旭。

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2 ... {

    /**
      * LayoutInflater.Factory散怖,LayoutInflater.Factory2 繼承自 LayoutInflater.Factory
      */
    @Nullable
    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }

    /**
      * LayoutInflater.Factory2
      */
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        if (!"fragment".equals(name)) {
            return onCreateView(name, context, attrs);
        }
        return mFragments.onCreateView(parent, name, context, attrs);
    }
}
2. AppCompatActivity 兼容設(shè)計(jì)

Android 在 5.0 之后引入了 Material Design 的設(shè)計(jì),為了更好的支撐 Material 主題选脊、調(diào)色版杭抠、Toolbar 等各種新特性,兼容版本的 AppCompatActivity 就應(yīng)運(yùn)而生了恳啥。大家是否有注意過偏灿,使用 AppCompatActivity 之后,所有(需要兼容特性的 View) View 控件都被替換成了 AppCompat-Xxx 類型:

AppCompatActivity 則是利用了 AppCompatDelegate 在不同的 Android 版本之間實(shí)現(xiàn)兼容配置钝的。其中將 View 的創(chuàng)建階段又單獨(dú)委托給 AppCompatViewInflater翁垂,它本質(zhì)還是利用了 LayoutInflater.Factory2 接口:

    public final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {

        // ... 省略

        View view = null;
        // 根據(jù)View類型替換成對應(yīng)的AppCompat類型
        switch (name) {
            case "TextView":
                view = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new AppCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new AppCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new AppCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new AppCompatRadioButton(context, attrs);
                break;
            case "CheckedTextView":
                view = new AppCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new AppCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new AppCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new AppCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new AppCompatSeekBar(context, attrs);
                break;
        }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check its android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

可以非常清晰的看到铆遭,整個兼容版 View 的替換過程。不過還是有一些 View 需要通過反射加載創(chuàng)建沿猜。其實(shí)這一部分也是我們最應(yīng)該優(yōu)化的枚荣,有關(guān)布局 View 的創(chuàng)建過程,我們完全可以將其接手以避免反射或極端的遍歷加載過程啼肩。

3. 動態(tài)換膚

關(guān)于動態(tài)換膚橄妆,業(yè)界做的比較好的要屬網(wǎng)易云音樂了。通過動態(tài)換膚滿足用戶的新鮮感祈坠,提升增值業(yè)務(wù)產(chǎn)品吸引力害碾。

動態(tài)換膚主要涉及兩個核心過程:① 采集需要換膚的控件,② 加載相應(yīng)皮膚包赦拘,并替換所有需要換膚的控件慌随。

如何確定哪些控件需要動態(tài)換膚呢?這里簡單提供一種思路躺同,首先要明確換膚到底是換的什么阁猜?只要理解了換的是什么,我們就知道要查詢哪些屬性了:

    static {
        mAttributes.add("background");
        mAttributes.add("src");

        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");
    }

那如何采集呢蹋艺?其實(shí)這一過程就可以利用到今天介紹的 LayoutInflater.Factory剃袍。而且還可以結(jié)合 ActivityLifecycleCallback 進(jìn)一步減少代碼的侵入性。

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
         
        // ... View 的創(chuàng)建過程省略

        // 篩選符合換膚條件的 View
        skinAttribute.load(view, attrs);

        return view;
    }

篩選過程主要是遍歷 View 的屬性集合 AttributeSet车海,查找 View 是否包含匹配的換膚屬性笛园。該過程如下:

    public void load(View view, AttributeSet attrs) {
        final List<SkinPair> skinPairs = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            // 獲得屬性名
            String attributeName = attrs.getAttributeName(i);
            // 是否符合需要篩選的屬性名
            if (mAttributes.contains(attributeName)) {
                String attributeValue = attrs.getAttributeValue(i);
                if (attributeValue.startsWith("#")) {
                    // 屬性資源寫死了,無法替換
                    continue;
                }
                //資源id
                int resId;
                if (attributeValue.startsWith("?")) {
                    // 主題資源侍芝,attr Id
                    int attrId = Integer.parseInt(attributeValue.substring(1));
                    // 獲得主題style中對應(yīng) attr 的資源id值
                    resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
                } else {
                    // @12343455332
                    resId = Integer.parseInt(attributeValue.substring(1));
                }
                if (resId != 0) {
                    // 可以被替換的屬性
                    SkinPair skinPair = new SkinPair(attributeName, resId);
                    skinPairs.add(skinPair);
                }
            }
        }

        // 將View與之對應(yīng)的可以動態(tài)替換的屬性集合放入集合中
        if (!skinPairs.isEmpty()) {
            // 每個SkinView表示可以被換膚的控件
            // skinPairs表示該控件哪些屬性需要被換膚
            SkinView skinView = new SkinView(view, skinPairs);
            skinView.applySkin();
            mSkinViews.add(skinView);
        }
    }

有關(guān) Android 換膚原理網(wǎng)上資料也比較多研铆,感興趣的朋友可以進(jìn)一步學(xué)習(xí)理解。

4. setFactory / setFactory2

LayoutInflater 內(nèi)部為開發(fā)者提供了直接設(shè)置 Factory 的方法州叠,不過需要注意該方法只能被設(shè)置一次棵红,否則將會拋出異常。聰明的你很快就會想到可以利用反射將其修改(LayoutInflater 并沒有被 @hide 聲明)咧栗。

    public void setFactory(Factory factory) {
        if (mFactorySet) {
            // 注意該變量逆甜,被設(shè)置過一次后會被置為true
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            // factory 不能為null
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            // 如果之前不存在,直接賦值
            mFactory = factory;
        } else {
            // 否則合并
            mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
        }
    }

看似一個簡單的 LayoutInflater.Factory 可以說在 View 的加載和創(chuàng)建過程提供了“無限種”可能致板。這也體現(xiàn)了優(yōu)秀的策略模式交煞,這種策略對于應(yīng)用的擴(kuò)展和兼容都提供了很大的幫助,AppCompat 就是比較經(jīng)典的例子斟或。

通過今天的分析素征,在你的項(xiàng)目中 View 的創(chuàng)建過程是否存在優(yōu)化的空間呢?可以將今天的內(nèi)容優(yōu)化到具體的應(yīng)用中,以幫助我們更好的優(yōu)化 UI 渲染性能御毅。


思考: Fragment 中也會使用到 LayoutInflater根欧,它是否和 Activity 使用的同一個呢?歡迎大家的分享留言或指正端蛆。

最后凤粗,文章如果對你有幫助,請留個贊吧今豆。


擴(kuò)展閱讀

其他系列專題

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末亭罪,一起剝皮案震驚了整個濱河市瘦馍,隨后出現(xiàn)的幾起案子歼秽,更是在濱河造成了極大的恐慌,老刑警劉巖情组,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件燥筷,死亡現(xiàn)場離奇詭異,居然都是意外死亡院崇,警方通過查閱死者的電腦和手機(jī)肆氓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來底瓣,“玉大人谢揪,你說我怎么就攤上這事【杵荆” “怎么了拨扶?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長茁肠。 經(jīng)常有香客問我患民,道長,這世上最難降的妖魔是什么垦梆? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任匹颤,我火速辦了婚禮,結(jié)果婚禮上托猩,老公的妹妹穿的比我還像新娘印蓖。我一直安慰自己,他們只是感情好京腥,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布赦肃。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪摆尝。 梳的紋絲不亂的頭發(fā)上温艇,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音堕汞,去河邊找鬼勺爱。 笑死,一個胖子當(dāng)著我的面吹牛讯检,可吹牛的內(nèi)容都是我干的琐鲁。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼人灼,長吁一口氣:“原來是場噩夢啊……” “哼围段!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起投放,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤奈泪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后灸芳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體涝桅,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年烙样,在試婚紗的時候發(fā)現(xiàn)自己被綠了冯遂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡谒获,死狀恐怖蛤肌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情批狱,我是刑警寧澤裸准,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站精耐,受9級特大地震影響狼速,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜卦停,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一向胡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧惊完,春花似錦僵芹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽荷辕。三九已至,卻和暖如春件豌,著一層夾襖步出監(jiān)牢的瞬間疮方,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工茧彤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留骡显,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓曾掂,卻偏偏與公主長得像惫谤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子珠洗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345