Android布局優(yōu)化(一)LayoutInflate — 從布局加載原理說起

如需轉(zhuǎn)載請(qǐng)?jiān)u論或簡信,并注明出處,未經(jīng)允許不得轉(zhuǎn)載

系列文章

目錄

前言

最近打算寫一些Android布局優(yōu)化相關(guān)的文章,既然要進(jìn)行布局優(yōu)化,就要先了解布局加載原理,才能知道有哪些地方可以作為優(yōu)化的切入點(diǎn)。開發(fā)同學(xué)做任何事情最好都能夠知其所以然

布局加載源碼分析

這里主要為了分析布局加載相關(guān)的原理,所以省略了一些邏輯。想了解View繪制流程,可以看最全的View繪制流程(上)— Window、DecorView、ViewRootImp的關(guān)系

我們先從Activity.setContentView開始分析布局是如何被加載的

Activity.setContentView(@LayoutRes int layoutResID)

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

PhoneWIndow.setContentView(int layoutResID)

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        //初始化DecorView和mContentParent
        installDecor();
    }
    ...
        //加載資源文件,創(chuàng)建view樹裝載到mContentParent
        mLayoutInflater.inflate(layoutResID, mContentParent);
    ...
}

LayoutInflate.inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    //1.加載解析xml文件
    final XmlResourceParser parser = res.getLayout(resource);
    try {
        //2.填充View樹
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

可以看出布局加載流程主要分為加載解析xml文件填充View樹兩部分

加載解析xml文件

Resources.getLayout(@LayoutRes int id)

 public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
        return loadXmlResourceParser(id, "layout");
 }

Resources.loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,@NonNul l String type)

XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,@NonNul l String type) throws NotFoundException {
        if (id != 0) {
            try {
                synchronized (mCachedXmlBlocks) {
                    final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
                    final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
                    final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
                    // First see if this block is in our cache.
                    final int num = cachedXmlBlockFiles.length;
                    for (int i = 0; i < num; i++) {
                        if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null
                                && cachedXmlBlockFiles[i].equals(file)) {
                            return cachedXmlBlocks[i].newParser();
                        }
                    }

                    // Not in the cache, create a new block and put it at
                    // the next slot in the cache.
                    final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
                    if (block != null) {
                        final int pos = (mLastCachedXmlBlockIndex + 1) % num;
                        mLastCachedXmlBlockIndex = pos;
                        final XmlBlock oldBlock = cachedXmlBlocks[pos];
                        if (oldBlock != null) {
                            oldBlock.close();
                        }
                        cachedXmlBlockCookies[pos] = assetCookie;
                        cachedXmlBlockFiles[pos] = file;
                        cachedXmlBlocks[pos] = block;
                        return block.newParser();
                    }
                }
            } catch (Exception e) {
                final NotFoundException rnf = new NotFoundException("File " + file
                        + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id));
                rnf.initCause(e);
                throw rnf;
            }
        }

        throw new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x"
                + Integer.toHexString(id));
    }

我們不用非常深入這個(gè)方法的具體實(shí)現(xiàn)細(xì)節(jié),我們只需要知道,這個(gè)方法的作用就是將我們寫的xml文件讀取到內(nèi)存中,并進(jìn)行一些數(shù)據(jù)解析和封裝。所以這個(gè)方法本質(zhì)上就是一個(gè)IO操作,我們知道,IO操作往往是比較耗費(fèi)性能的

填充View樹

LayoutInflate.inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;
            int type;
            final String name = parser.getName();
            if (TAG_MERGE.equals(name)) {
                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
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        temp.setLayoutParams(params);
                    }
                }

                rInflateChildren(parser, temp, attrs, true);
              
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        return result;
    }
}

上面這個(gè)方法中我們最主要關(guān)注createViewFromTag(View parent, String name, Context context, AttributeSet attrs)

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
  
    //解析view標(biāo)簽
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }

    //如果需要該標(biāo)簽與主題相關(guān),需要對(duì)context進(jìn)行包裝,將主題信息加入context包裝類ContextWrapper
    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)) {
       //BlinkLayout是一種閃爍的FrameLayout,它包裹的內(nèi)容會(huì)一直閃爍,類似QQ提示消息那種。
        return new BlinkLayout(context, attrs);
    }

            //設(shè)置Factory,來對(duì)View做額外的拓展,這塊屬于可定制的內(nèi)容
        View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

          //如果此時(shí)不存在Factory,不管Factory還是Factory2,還是mPrivateFactory都不存在,
            //那么會(huì)直接對(duì)name直接進(jìn)行解析
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                //如果name中包含"."即為自定義View,否則為原生的View控件
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
}

根據(jù)源碼可以將createViewFromTag分為三個(gè)流程:

  1. 對(duì)一些特殊標(biāo)簽,做分別處理,例如:view,TAG_1995(blink)

  2. 進(jìn)行對(duì)Factory、Factory2的設(shè)置判斷,如果設(shè)置那么就會(huì)通過設(shè)置FactoryFactory2進(jìn)行生成View

  3. 如果沒有設(shè)置FactoryFactory2,那么就會(huì)使用LayoutInflater默認(rèn)的生成方式,進(jìn)行View的生成

createViewFromTag過程分析:

  1. 處理view標(biāo)簽

如果標(biāo)簽的名稱是view,注意是小寫的view,這個(gè)標(biāo)簽一般大家不太常用

<view
    class="RelativeLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"></view>

在使用時(shí),相當(dāng)于所有控件標(biāo)簽的父類一樣,可以設(shè)置class屬性,這個(gè)屬性會(huì)決定view這個(gè)節(jié)點(diǎn)會(huì)變成什么控件

  1. 如果該節(jié)點(diǎn)與主題相關(guān),則需要特殊處理

如果該節(jié)點(diǎn)與主題(Theme)相關(guān),需要將context與theme信息包裝至ContextWrapper

  1. 處理TAG_1995標(biāo)簽

這就有意思了,TAG_1995指的是blink這個(gè)標(biāo)簽,這個(gè)標(biāo)簽感覺使用的很少,以至于大家根本不知道。

這個(gè)標(biāo)簽最后會(huì)被解析成BlinkLayout,BlinkLayout其實(shí)就是一個(gè)FrameLayout,這個(gè)控件最后會(huì)將包裹內(nèi)容一直閃爍(就和電腦版QQ消息提示一樣)

<blink
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="這個(gè)標(biāo)簽會(huì)一直閃爍"/>
    
</blink>
  1. 判斷其是否存在Factory或者Factory2

在這里先對(duì)Factory進(jìn)行判空,這里不管Factory還是Factory2mPrivateFactory 就是Factory2),本質(zhì)上都是一種擴(kuò)展操作,提前解析name,然后直接將解析后的View返回

Factory

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

Factory2

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

從這里可以看出,Factory2Factory都是一個(gè)接口,需要自己實(shí)現(xiàn),而Factory2Factory的區(qū)別是Factory2繼承Factory,從而擴(kuò)展出一個(gè)參數(shù),就是增加了該節(jié)點(diǎn)的父View。設(shè)置FactoryFactory2需要通過setFactory()或者setFactory2()來實(shí)現(xiàn)

setFactory()

public void setFactory(Factory factory) {
    //如果已經(jīng)設(shè)置Factory,不可以繼續(xù)設(shè)置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");
    }
    //設(shè)置Factory會(huì)添加一個(gè)標(biāo)記
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = factory;
    } else {
        mFactory = new FactoryMerger(factory, null, mFactory, 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");
    }
    //注意設(shè)置Factory和Factory2的標(biāo)記是共用的
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = mFactory2 = factory;
    } else {
        mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
    }
}

通過上面代碼可以看出,FactoryFactory2只能夠設(shè)置一次,并且FactoryFactory2二者互斥,只能存在一個(gè)。所以一般setFactory()或者setFactory2(),一般在cloneInContext()之后設(shè)置,這樣生成一個(gè)新的LayoutInflater,標(biāo)記默認(rèn)是false,才能夠設(shè)置

  1. createView(String name, String prefix, AttributeSet attrs)
 public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        //判斷構(gòu)造器是否存在    
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
        //如果構(gòu)造器不存在,這個(gè)就相當(dāng)于Class之前是否被加載過,sConstructorMap就是緩存這些Class的Map
            if (constructor == null) {
                //通過前綴+name的方式去加載
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                //通過過濾去設(shè)置一些不需要加載的對(duì)象
                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                //緩存Class
                sConstructorMap.put(name, constructor);
            } else {
            //如果Class存在,并且加載Class的ClassLoader合法
                //這里先判斷該Class是否應(yīng)該被過濾
                if (mFilter != null) {
                    //過濾器也有緩存之前的Class是否被允許加載,判斷這個(gè)Class的過濾狀態(tài)
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        //加載Class對(duì)象操作
                        clazz = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);
                        //判斷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[] args = mConstructorArgs;
            args[1] = attrs;
            
                    //如果過濾器不存在,直接實(shí)例化該View
            final View view = constructor.newInstance(args);
            //如果View屬于ViewStub那么需要給ViewStub設(shè)置一個(gè)克隆過的LayoutInflater
            if (view instanceof ViewStub) {
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            return view

從上面的代碼可以看出,我們是通過反射的方式去創(chuàng)建View實(shí)例的

總結(jié)

經(jīng)過對(duì)布局加載原理的分析,我們可以看出布局加載的主要性能瓶頸主要在兩個(gè)方面

  1. 加載xml文件是一個(gè)IO過程,如果xml文件過大,就會(huì)比較耗時(shí)

  2. View實(shí)例是通過反射進(jìn)行創(chuàng)建的,通過反射創(chuàng)建對(duì)象相對(duì)會(huì)更耗費(fèi)性能

android布局加載過程
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評(píng)論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,744評(píng)論 3 421
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,879評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,935評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,325評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評(píng)論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,534評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,084評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,892評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,067評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,322評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評(píng)論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評(píng)論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,800評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,084評(píng)論 2 375