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

如需轉載請評論或簡信,并注明出處慧脱,未經允許不得轉載

系列文章

目錄

前言

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

布局加載源碼分析

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

我們先從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));
    }

我們不用非常深入這個方法的具體實現(xiàn)細節(jié)蜡感,我們只需要知道郑兴,這個方法的作用就是將我們寫的xml文件讀取到內存中贝乎,并進行一些數據解析和封裝。所以這個方法本質上就是一個IO操作览效,我們知道,IO操作往往是比較耗費性能的

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

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

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

    //如果需要該標簽與主題相關,需要對context進行包裝衡招,將主題信息加入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,它包裹的內容會一直閃爍州刽,類似QQ提示消息那種。
        return new BlinkLayout(context, attrs);
    }

            //設置Factory辨绊,來對View做額外的拓展匹表,這塊屬于可定制的內容
        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);
        }

          //如果此時不存在Factory,不管Factory還是Factory2默蚌,還是mPrivateFactory都不存在苇羡,
            //那么會直接對name直接進行解析
        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;
}

根據源碼可以將createViewFromTag分為三個流程:

  1. 對一些特殊標簽锦茁,做分別處理叉存,例如:viewTAG_1995(blink)

  2. 進行對Factory稿存、Factory2的設置判斷瞳秽,如果設置那么就會通過設置FactoryFactory2進行生成View

  3. 如果沒有設置FactoryFactory2寂诱,那么就會使用LayoutInflater默認的生成方式痰洒,進行View的生成

createViewFromTag過程分析:

  1. 處理view標簽

如果標簽的名稱是view浴韭,注意是小寫的view,這個標簽一般大家不太常用

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

在使用時泉粉,相當于所有控件標簽的父類一樣,可以設置class屬性跺撼,這個屬性會決定view這個節(jié)點會變成什么控件

  1. 如果該節(jié)點與主題相關讨彼,則需要特殊處理

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

  1. 處理TAG_1995標簽

這就有意思了哩至,TAG_1995指的是blink這個標簽,這個標簽感覺使用的很少菩貌,以至于大家根本不知道重荠。

這個標簽最后會被解析成BlinkLayoutBlinkLayout其實就是一個FrameLayout尾膊,這個控件最后會將包裹內容一直閃爍(就和電腦版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="這個標簽會一直閃爍"/>
    
</blink>
  1. 判斷其是否存在Factory或者Factory2

在這里先對Factory進行判空荞彼,這里不管Factory還是Factory2mPrivateFactory 就是Factory2),本質上都是一種擴展操作抓谴,提前解析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都是一個接口,需要自己實現(xiàn)帜消,而Factory2Factory的區(qū)別是Factory2繼承Factory浓体,從而擴展出一個參數,就是增加了該節(jié)點的父View娄猫。設置FactoryFactory2需要通過setFactory()或者setFactory2()來實現(xiàn)

setFactory()

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

通過上面代碼可以看出月幌,FactoryFactory2只能夠設置一次褂删,并且FactoryFactory2二者互斥,只能存在一個缅帘。所以一般setFactory()或者setFactory2()难衰,一般在cloneInContext()之后設置,這樣生成一個新的LayoutInflater失暂,標記默認是false鳄虱,才能夠設置

  1. createView(String name, String prefix, AttributeSet attrs)
 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 {
        //如果構造器不存在,這個就相當于Class之前是否被加載過决记,sConstructorMap就是緩存這些Class的Map
            if (constructor == null) {
                //通過前綴+name的方式去加載
                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);
                //緩存Class
                sConstructorMap.put(name, constructor);
            } else {
            //如果Class存在,并且加載Class的ClassLoader合法
                //這里先判斷該Class是否應該被過濾
                if (mFilter != null) {
                    //過濾器也有緩存之前的Class是否被允許加載系宫,判斷這個Class的過濾狀態(tài)
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        //加載Class對象操作
                        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;
            
                    //如果過濾器不存在建车,直接實例化該View
            final View view = constructor.newInstance(args);
            //如果View屬于ViewStub那么需要給ViewStub設置一個克隆過的LayoutInflater
            if (view instanceof ViewStub) {
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            return view

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

總結

經過對布局加載原理的分析,我們可以看出布局加載的主要性能瓶頸主要在兩個方面

  1. 加載xml文件是一個IO過程领斥,如果xml文件過大,就會比較耗時

  2. View實例是通過反射進行創(chuàng)建的,通過反射創(chuàng)建對象相對會更耗費性能

android布局加載過程
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
禁止轉載膊存,如需轉載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末今艺,一起剝皮案震驚了整個濱河市爵卒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钓株,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件创坞,死亡現(xiàn)場離奇詭異题涨,居然都是意外死亡,警方通過查閱死者的電腦和手機纲堵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門闰渔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人向挖,你說我怎么就攤上這事『沃” “怎么了咽筋?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蒜危。 經常有香客問我睹耐,道長,這世上最難降的妖魔是什么响委? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮赘风,結果婚禮上,老公的妹妹穿的比我還像新娘荸哟。我一直安慰自己瞬捕,他們只是感情好,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布堰燎。 她就那樣靜靜地躺著笋轨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仅讽。 梳的紋絲不亂的頭發(fā)上钾挟,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音徽千,去河邊找鬼汤锨。 笑死,一個胖子當著我的面吹牛牍汹,可吹牛的內容都是我干的。 我是一名探鬼主播慎菲,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼露该,長吁一口氣:“原來是場噩夢啊……” “哼第煮!你這毒婦竟也來了闸拿?” 一聲冷哼從身側響起书幕,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤台汇,失蹤者是張志新(化名)和其女友劉穎苟呐,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體牵素,經...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡笆呆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年粱挡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片询筏。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡嫌套,死狀恐怖,靈堂內的尸體忽然破棺而出踱讨,到底是詐尸還是另有隱情,我是刑警寧澤沫勿,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布味混,位于F島的核電站产雹,受9級特大地震影響蔓挖,放射性物質發(fā)生泄漏。R本人自食惡果不足惜瘟判,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望篮撑。 院中可真熱鬧匆瓜,春花似錦、人聲如沸驮吱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拇砰。三九已至,卻和暖如春铸鹰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蹋笼。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工剖毯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留教馆,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓胶滋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親究恤。 傳聞我的和親對象是個殘疾皇子后德,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355