初探Android中LayoutInflater原理

接觸了Android的人也肯定不會對LayoutInflater陌生,至少在ListView等等這些常見控件中我們也經(jīng)常會使用這個類來進(jìn)行我們的item布局的解析,那么今天我們就來把LayoutInflater的工作流程仔細(xì)地分析一遍,爭取達(dá)到知其然知其所以然的境界。本文分析的源代碼均來自Android API 24。同時代碼分析在上半部分,下半部分將用demo來進(jìn)行驗證。


我們在日常開發(fā)寫代碼時一般會通過下面兩種方式來獲取LayoutInflater來進(jìn)行布局的解析:

1.LayoutInflater layoutInflater = LayoutInflater.from(context);
2.LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);  

而在第一種方法中我們跟進(jìn)from()方法去看源代碼就會發(fā)現(xiàn),其實第一種方法無非就是對第二種方法進(jìn)行封裝(代碼如下):

從上圖中標(biāo)注處可以看出其實也是通過context.getSystemService方法來獲取,只不過增加了安全判斷更加安全而已。

我們獲取LayoutInflater對象后,就可以通過以下方法來進(jìn)行布局解析:

 layoutInflater.inflate(resourceId, root,attachToRoot); 

其中第一個參數(shù)就是要加載的布局id,第二個參數(shù)是傳入一個布局做為要解析布局的父布局。如果不需要就直接傳null。第三個參數(shù)指的是加載的布局是否添加到我們傳入的父布局中。
接下來我們再來接著具體分析一下inflate()方法。

首先這里先整理出來inflate()的四個調(diào)用方法:
調(diào)用方法一
調(diào)用方法二
調(diào)用方法三
這里來一一說明一下三幅圖的大概意思。

圖一調(diào)用的inflate方法只需要傳入布局id,root父布局view對象,而第三個參數(shù)的值是通過判斷root是否為空來設(shè)置,再調(diào)用圖三方法。
圖二中第一個是XmlPullParser對象(是一種通過pull方式來解析xml的對象,有興趣的同志可以自行了解,只需要知道用來解析xml的對象即可),第二個參數(shù),第三個參數(shù)和圖一同理。
圖三我們可以看到,將傳入的布局id轉(zhuǎn)化為Resources類型,然后將它解析來獲取XmlResourceParser對象,再最終調(diào)用圖二方法。
至此其實已經(jīng)很清晰了,其實上面幾個不同的方法都只是做了一點預(yù)處理的過程,最終的實現(xiàn)的源代碼并沒有出現(xiàn)在這三個方法中,而是最終調(diào)用了:

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);

而這個方法,就是最后一個調(diào)用方法,也正是關(guān)鍵所在,接下來的內(nèi)容將對這個方法進(jìn)行講解。
先將方法源代碼貼出在下方:

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

        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;

        try {
            // Look for the root node.
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }

            final String name = parser.getName();
            
            if (DEBUG) {
                System.out.println("**************************");
                System.out.println("Creating root view: "
                        + name);
                System.out.println("**************************");
            }

            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) {
                    if (DEBUG) {
                        System.out.println("Creating params from root: " +
                                root);
                    }
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }

                if (DEBUG) {
                    System.out.println("-----> start inflating children");
                }

                // Inflate all children under temp against its context.
                rInflateChildren(parser, temp, attrs, true);

                if (DEBUG) {
                    System.out.println("-----> done inflating children");
                }

                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                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;
        } finally {
            // Don't retain static reference on context.
            mConstructorArgs[0] = lastContext;
            mConstructorArgs[1] = null;

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        return result;
  }    
}

上面是完整版本的源代碼,去除其中一些打印,異常處理和預(yù)處理無關(guān)代碼后。我們將關(guān)鍵代碼提取出來進(jìn)行分析,關(guān)鍵代碼如下:
參數(shù)定義
關(guān)鍵處理過程

第一幅圖中我們可以在標(biāo)注處看到,一開始定義了一個View類型的result變量一開始默認(rèn)就是我們傳入的root對象。

而第二幅圖就是處理的關(guān)鍵代碼。

首先在圖中標(biāo)注1處通過注釋可看到:temp是我們xml布局中定義的根布局。因此在這里將我們的布局的根布局解析出來轉(zhuǎn)化為View類型的temp對象,然后定義一個params。

接下來在標(biāo)注2處判斷root是否為空,如果不為空,將自定義布局的參數(shù)解析出來賦值給params。

然后在標(biāo)注3處判斷我們傳入的attchToRoot是否為true,如果不是,意味著我們不把我們的自定義布局加入到某個父View中,因此將params設(shè)置給temp,將temp在xml設(shè)置的屬性進(jìn)行生效。

標(biāo)注3處和標(biāo)注4處之間的rInflateChildren()方法是通過遞歸的方法用來不斷解析我們自定義布局中的子View,這不再做展開。

接下來在圖中標(biāo)注4處判斷root是否為空,attachToRoot是否為true。如果root不為空并且attachToRoot為true,那么將我們的temp添加到父布局中。

上述代碼都是在root不為空的場景下設(shè)置的,如果root為空呢?
我們看看圖中標(biāo)注5處:如果root為空,或者attachToRoot為false,那么將temp直接賦值為result并返回。(result可以在上面的完整源碼中看到創(chuàng)建時就是root).

------------------------------一條假裝很華麗的分割線------------------------------

現(xiàn)在我們來總結(jié)一下幾個結(jié)論:
  1. 如果root為null,attachToRoot將沒有作用,inflate()方法會直接返回temp(也就是自定義布局的View,直接執(zhí)行上圖4處代碼);
  2. 如果root不為null,attachToRoot設(shè)為true,則會給自定義布局文件添加一個父布局,即root。方法返回root對象。(執(zhí)行2,4處)。
  3. 如果root不為null,attachToRoot設(shè)為false,則會將布局文件最外層的所有l(wèi)ayout屬性進(jìn)行設(shè)置,當(dāng)該view被添加到父view當(dāng)中時,這些layout屬性會自動生效。返回自定義布局View。(執(zhí)行2,3處)
  4. 不傳入attachToRoot參數(shù),如果root不為null,attachToRoot參數(shù)為true(參考上文幾個方法參數(shù)說明圖)。

(ps:個人理解:總之就是返回我們布局的根布局,如果我們沒有傳入root,那么明顯我們自定義布局根布局就是最頂級,返回它。如果我們傳入了root,但是attchToRoot為false,說明我們不想把自定義布局加入到root中,所以我們想要的布局的根布局也是自定義布局的根布局。而我們傳入root,attchToRoot為true,說明我們要把自定義布局加入到root中,所以root成了最頂級布局,所以返回root。)

------------------------------又是一條假裝很華麗的分割線----------------------------

至此代碼和結(jié)論就都已經(jīng)分析完畢了,按照常理我們就該撒花結(jié)束,但是呢?說了半天,沒啥直接的效果看看??!這干說半天沒意思是吧?因此現(xiàn)在我們來寫個Demo實際看一下效果驗證一下我們上述的分析和結(jié)論是否正確:

首先我們定義一個布局(名字叫buttonLayout):

可以看到就是一個簡單的Button,Button就是根布局。
我們接下來在Activity中解析一下:

@Override
protected void onCreate(Bundle savedInstanceState) { 
  super.onCreate(savedInstanceState);   
//mainLayout就是MainActivity的布局,就是一個空的LinearLayout
  LinearLayout mainLayout = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.activity_main3, null);       
  setContentView(mainLayout);
//獲取layoutInflater
  LayoutInflater layoutInflater = LayoutInflater.from(this); 
//解析出buttonLayout 
  View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, null);       
//將buttonLayout添加到mainLayout中
  mainLayout.addView(buttonLayout);
  Log.d("result", "inflate返回的view為:" + buttonLayout.toString());
}

來看看實際效果:
方法返回結(jié)果日志

?首先當(dāng)root為空時,會把自定義布局的根布局返回給我們(如上圖),將Button返回,也驗證了上面的結(jié)論一。然后......
發(fā)現(xiàn)好像Button的大小好像遠(yuǎn)遠(yuǎn)沒有定義的400dp寬高??我們再改一下布局,把button寬高設(shè)置為match_parent看看:
再運行看看效果:

我曹???干啥呢?設(shè)置了沒用啊?還是一樣。先別慌別流汗。我們仔細(xì)來回憶一下之前分析的代碼順便來求證一下前面的分析是否正確。

首先我們在調(diào)用inflate()方法的時候傳入root為null,因此在inflate()方法中不會執(zhí)行之前分析的一系列代碼,只會執(zhí)行下圖(就是上文圖,拿過來避免翻上去看)中1,5處代碼,因此實際上我們填寫的參數(shù)都沒有被設(shè)置,因此我們的按鈕不管怎么設(shè)置都是默認(rèn)狀態(tài)。

接下來我們改一下調(diào)用代碼,將我們的mainLayout作為root傳入,attachToRoot傳為false(其他代碼不變):

//修改前代碼
 View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, null);       
//修改后前代碼
 View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, mainLayout, false); 

運行一下。

因為這次我們把mainLayout作為了button的父布局。button存在于一個父布局中,因此參數(shù)設(shè)置才生效了。先來看一下方法返回的結(jié)果是什么???,因為attachToRoot為false,所以直接將button返回給我們,和上面的結(jié)論三一致。
方法返回結(jié)果日志
然后來看看運行效果:

有效果啦!(請原諒400dp效果太明顯了。。。)。這是為什么呢?(來來來,科普一下)其實安卓中l(wèi)ayout_width和layout_height是用于設(shè)置View在布局中的大小的(這種官方話我聽著都別扭)。

簡單來說,就是首先View必須存在于一個父布局中l(wèi)ayout_width等參數(shù)才會生效(給一個小孩子一包糖說你可以吃,去找你爹給你打開,如果孩子連爹都沒有,空有一包糖也打不開呀)。

例如如果將layout_width設(shè)置成match_parent表示讓View的寬度填充滿父布局,如果設(shè)置成wrap_content表示讓View的寬度在父布局中的寬度剛好可以包含自身內(nèi)容,如果設(shè)置成具體的數(shù)值則View的寬度會變成相應(yīng)的數(shù)值。
接下來我們再改代碼,把attachToRoot改為true傳入看看(其他代碼不變):

//修改前代碼
 View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, mainLayout, false);      
 mainLayout.addView(buttonLayout);
//修改后前代碼
 View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, mainLayout, true); 
 mainLayout.addView(buttonLayout);

運行,等待。
emmm...
emmm…
emm….


????哈玩意兒??幾行代碼還能掛了??這是為啥??還是別慌,其實報錯才是正確姿勢,我們來看看報錯信息:
報錯信息
仔細(xì)看看報錯信息,意思就是說我們的布局已經(jīng)有了一個父布局了,不能再addView了。其實這也解決了我們在ListView中g(shù)etView中經(jīng)常遇到的錯誤(具體不再闡述)。當(dāng)我們傳入root,并且設(shè)置attachToRoot為true時,inflate()方法內(nèi)部(上文源代碼的標(biāo)注4處)就為我們addView了。我們?nèi)绻谶@之后再手動調(diào)用addView,那么就會報錯,因此我們無需在手動addView,去掉手動add代碼再看看:
首先效果是正確的!button大小正確顯示,。再看看方法返回的結(jié)果:
方法返回結(jié)果日志
果不其然,這次變成了返回mainLayout,這也印證了之前的結(jié)論如果有root,attachToRoot為true會返回root給我們。也就證明了結(jié)論二。至此我們就一起分析源碼, 驗證源碼的效果也就差不多啦。

ps:如果有時候我們一定要root為null,但是又要動態(tài)改變button的大小呢?(瞎想的場景,畢竟大家都喜歡沒事瞎想)。那么我們就可以在button外面套一個布局,這樣的話解析時外布局參數(shù)設(shè)置雖然失效,但是button的參數(shù)已經(jīng)是相對于外布局了,就可以有效。修改buttonLayout布局代碼如下:

執(zhí)行結(jié)果:
可以看出button的效果又回來啦。

至此本文的分析就差不多結(jié)束啦,其中還有一些可以深究的地方如果感興趣的同學(xué)可以繼續(xù)深入探索!
ps:還是老話,本人萌新,如果文中有錯誤或者模糊的地方,希望大家能多多指正,還望多多包涵。


再ps:寫本文和分析源碼的過程中也參考了郭霖大神的文章,也算是站在前輩的肩膀上繼續(xù)進(jìn)行自己的探索把。附上鏈接:
http://blog.csdn.net/guolin_blog/article/details/12921889

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,098評論 25 708
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,881評論 18 139
  • 一、適用場景 ListViewListview是一個很重要的組件,它以列表的形式根據(jù)數(shù)據(jù)的長自適應(yīng)展示具體內(nèi)容,用...
    Geeks_Liu閱讀 10,717評論 1 28
  • ¥開啟¥ 【iAPP實現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,510評論 0 17
  • 1 最近見一位老朋友,我們談起大家共同認(rèn)識的一位熟人,這位朋友喟嘆:“我不知道自己會不會也向他那樣,時常沉浸在自己...
    高宇_Betty閱讀 770評論 1 3