1. 程式人生 > 實用技巧 >Android應用setContentView與LayoutInflater載入解析機制原始碼分析

Android應用setContentView與LayoutInflater載入解析機制原始碼分析

1 背景

其實之所以要說這個話題有幾個原因:

  1. 理解xml等控制元件是咋被顯示的原理,通常大家寫程式碼都是直接在onCreate裡setContentView就完事,沒怎麼關注其實現原理。
  2. 前面分析《Android觸控式螢幕事件派發機制詳解與原始碼分析三(Activity篇)》時提到了一些關於佈局巢狀的問題,當時沒有深入解釋。

所以接下來主要分析的就是View或者ViewGroup物件是如何新增至應用程式介面(視窗)顯示的。我們準備從Activity的setContentView方法開始來說(因為預設Activity中放入我們的xml或者Java控制元件是通過setContentView方法來操作的,當調運了setContentView所有的控制元件就得到了顯示)。

【工匠若水http://blog.csdn.net/yanbober轉載煩請註明出處,尊重分享成果】

2 Android5.1.1(API 22)從Activity的setContentView方法說起

2-1 Activity的setContentView方法解析

Activity的原始碼中提供了三個過載的setContentView方法,如下:

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

    public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar();
    }

    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getWindow().setContentView(view, params);
        initWindowDecorActionBar();
    }

可以看見他們都先調運了getWindow()的setContentView方法,然後調運Activity的initWindowDecorActionBar方法,關於initWindowDecorActionBar方法後面準備寫一篇關於Android ActionBar原理解析的文章,所以暫時跳過不解釋。

2-2 關於視窗Window類的一些關係

在開始分析Activity組合物件Window的setContentView方法之前請先明確如下關係(前面分析《Android觸控式螢幕事件派發機制詳解與原始碼分析三(Activity篇)》時也有說過)。

看見上面圖沒?Activity中有一個成員為Window,其例項化物件為PhoneWindow,PhoneWindow為抽象Window類的實現類。

這裡先簡要說明下這些類的職責:

  1. Window是一個抽象類,提供了繪製視窗的一組通用API。

  2. PhoneWindow是Window的具體繼承實現類。而且該類內部包含了一個DecorView物件,該DectorView物件是所有應用視窗(Activity介面)的根View。

  3. DecorView是PhoneWindow的內部類,是FrameLayout的子類,是對FrameLayout進行功能的修飾(所以叫DecorXXX),是所有應用視窗的根View 。

依據面向物件從抽象到具體我們可以類比上面關係就像如下:

Window是一塊電子屏,PhoneWindow是一塊手機電子屏,DecorView就是電子屏要顯示的內容,Activity就是手機電子屏安裝位置。

2-2 視窗PhoneWindow類的setContentView方法

我們可以看見Window類的setContentView方法都是抽象的。所以我們直接先看PhoneWindow類的setContentView(int layoutResID)方法原始碼,如下:

    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

可以看見,第五行首先判斷mContentParent是否為null,也就是第一次調運);如果是第一次呼叫,則呼叫installDecor()方法,否則判斷是否設定FEATURE_CONTENT_TRANSITIONS Window屬性(預設false),如果沒有就移除該mContentParent內所有的所有子View;接著16行mLayoutInflater.inflate(layoutResID, mContentParent);將我們的資原始檔通過LayoutInflater物件轉換為View樹,並且新增至mContentParent檢視中(其中mLayoutInflater是在PhoneWindow的建構函式中得到例項物件的LayoutInflater.from(context);)。

再來看下PhoneWindow類的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法原始碼,如下:

    @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            mContentParent.addView(view, params);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

看見沒有,我們其實只用分析setContentView(View view, ViewGroup.LayoutParams params)方法即可,如果你在Activity中調運setContentView(View view)方法,實質也是調運setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams設定為了MATCH_PARENT而已。

所以直接分析setContentView(View view, ViewGroup.LayoutParams params)方法就行,可以看見該方法與setContentView(int layoutResID)類似,只是少了LayoutInflater將xml檔案解析裝換為View而已,這裡直接使用View的addView方法追加道了當前mContentParent而已。

所以說在我們的應用程式裡可以多次呼叫setContentView()來顯示介面,因為會removeAllViews。

2-3 視窗PhoneWindow類的installDecor方法

回過頭,我們繼續看上面PhoneWindow類setContentView方法的第6行installDecor();程式碼,在PhoneWindow中檢視installDecor原始碼如下:

    private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        if (mContentParent == null) {
            //根據視窗的風格修飾,選擇對應的修飾佈局檔案,並且將id為content的FrameLayout賦值給mContentParent
            mContentParent = generateLayout(mDecor);
            //......
            //初始化一堆屬性值
        }
    }

我勒個去!又是一個死長的方法,抓重點分析吧。第2到9行可以看出,首先判斷mDecor物件是否為空,如果為空則呼叫generateDecor()建立一個DecorView(該類是
FrameLayout子類,即一個ViewGroup檢視),然後設定一些屬性,我們看下PhoneWindow的generateDecor方法,如下:

    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }

可以看見generateDecor方法僅僅是new一個DecorView的例項。

回到installDecor方法繼續往下看,第10行開始到方法結束都需要一個if (mContentParent == null)判斷為真才會執行,當mContentParent物件不為空則呼叫generateLayout()方法去建立mContentParent物件。所以我們看下generateLayout方法原始碼,如下:

    protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.

        TypedArray a = getWindowStyle();

        //......
        //依據主題style設定一堆值進行設定

        // Inflate the window decor.

        int layoutResource;
        int features = getLocalFeatures();
        //......
        //根據設定好的features值選擇不同的視窗修飾佈局檔案,得到layoutResource值

        //把選中的視窗修飾佈局檔案新增到DecorView物件裡,並且指定contentParent值
        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        //......
        //繼續一堆屬性設定,完事返回contentParent
        return contentParent;
    }

可以看見上面方法主要作用就是根據視窗的風格修飾型別為該視窗選擇不同的視窗根佈局檔案。mDecor做為根檢視將該視窗根佈局新增進去,然後獲取id為content的FrameLayout返回給mContentParent物件。所以installDecor方法實質就是產生mDecor和mContentParent物件。

在這裡順帶提一下:還記得我們平時寫應用Activity時設定的theme或者feature嗎(全屏啥的,NoTitle等)?我們一般是不是通過XML的android:theme屬性或者java的requestFeature()方法來設定的呢?譬如:

通過java檔案設定:

requestWindowFeature(Window.FEATURE_NO_TITLE);

通過xml檔案設定:

android:theme="@android:style/Theme.NoTitleBar"

對的,其實我們平時requestWindowFeature()設定的值就是在這裡通過getLocalFeature()獲取的;而android:theme屬性也是通過這裡的getWindowStyle()獲取的。

所以這下你應該就明白在java檔案設定Activity的屬性時必須在setContentView方法之前呼叫requestFeature()方法的原因了吧。

我們繼續關注一下generateLayout方法的layoutResource變數賦值情況。因為它最終通過View in = mLayoutInflater.inflate(layoutResource, null);decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));將in新增到PhoneWindow的mDecor物件。為例驗證這一段程式碼分析我們用一個例項來進行說明,如下是一個簡單的App主要程式碼:

AndroidManifest.xml檔案

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yanbober.myapplication" >

    <application
        ......
        //看重點,我們將主題設定為NoTitleBar
        android:theme="@android:style/Theme.Black.NoTitleBar" >
        ......
    </application>

</manifest>

主介面佈局檔案:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView android:text="@string/hello_world"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

APP執行介面:

看見沒有,上面我們將主題設定為NoTitleBar,所以在generateLayout方法中的layoutResource變數值為R.layout.screen_simple,所以我們看下系統這個screen_simple.xml佈局檔案,如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

佈局中,一般會包含ActionBar,Title,和一個id為content的FrameLayout,這個佈局是NoTitle的。

再來看下上面這個App的hierarchyviewer圖譜,如下:

看見了吧,通過這個App的hierarchyviewer和系統screen_simple.xml檔案比較就驗證了上面我們分析的結論,不再做過多解釋。

然後回過頭可以看見上面PhoneWindow類的setContentView方法最後通過調運mLayoutInflater.inflate(layoutResID, mContentParent);或者mContentParent.addView(view, params);語句將我們的xml或者java View插入到了mContentParent(id為content的FrameLayout物件)ViewGroup中。最後setContentView還會呼叫一個Callback介面的成員函式onContentChanged來通知對應的Activity元件檢視內容發生了變化。

2-4 Window類內部介面Callback的onContentChanged方法

上面剛剛說了PhoneWindow類的setContentView方法中最後調運了onContentChanged方法。我們這裡看下setContentView這段程式碼,如下:

    public void setContentView(int layoutResID) {
        ......
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

看著沒有,首先通過getCallback獲取物件cb(回撥介面),PhoneWindow沒有重寫Window的這個方法,所以到抽象類Window中可以看到:

    /**
     * Return the current Callback interface for this window.
     */
    public final Callback getCallback() {
        return mCallback;
    }

這個mCallback在哪賦值的呢,繼續看Window類發現有一個方法,如下:

    public void setCallback(Callback callback) {
        mCallback = callback;
    }

Window中的mCallback是通過這個方法賦值的,那就回想一下,Window又是Activity的組合成員,那就是Activity一定調運這個方法了,回到Activity發現在Activity的attach方法中進行了設定,如下:

    final void attach(Context context, ActivityThread aThread,
        ......
        mWindow.setCallback(this);
        ......
    }

也就是說Activity類實現了Window的Callback介面。那就是看下Activity實現的onContentChanged方法。如下:

    public void onContentChanged() {
    }

咦?onContentChanged是個空方法。那就說明當Activity的佈局改動時,即setContentView()或者addContentView()方法執行完畢時就會呼叫該方法。

所以當我們寫App時,Activity的各種View的findViewById()方法等都可以放到該方法中,系統會幫忙回撥。

2-5 setContentView原始碼分析總結

可以看出來setContentView整個過程主要是如何把Activity的佈局檔案或者java的View新增至窗口裡,上面的過程可以重點概括為:

  1. 建立一個DecorView的物件mDecor,該mDecor物件將作為整個應用視窗的根檢視。

  2. 依據Feature等style theme建立不同的視窗修飾佈局檔案,並且通過findViewById獲取Activity佈局檔案該存放的地方(視窗修飾佈局檔案中id為content的FrameLayout)。

  3. 將Activity的佈局檔案新增至id為content的FrameLayout內。

至此整個setContentView的主要流程就分析完畢。你可能這時會疑惑,這麼設定完一堆View關係後系統是怎麼知道該顯示了呢?下面我們就初探一下關於Activity的setContentView在onCreate中如何顯示的(宣告一下,這裡有些會暫時直接給出結論,該系列文章後面會詳細分析的)。

2-6 setContentView完以後Activity顯示介面初探

這一小部分已經不屬於sentContentView的分析範疇了,只是簡單說明setContentView之後怎麼被顯示出來的(注意:Activity調運setContentView方法自身不會顯示佈局的)。

記得前面有一篇文章《Android非同步訊息處理機制詳解及原始碼分析》的3-1-2小節說過,一個Activity的開始實際是ActivityThread的main方法(至於為什麼後面會寫文章分析,這裡站在應用層角度先有這個概念就行)。

那在這一篇我們再直接說一個知識點(至於為什麼後面會寫文章分析,這裡站在應用層角度先有這個概念就行)。

當啟動Activity調運完ActivityThread的main方法之後,接著呼叫ActivityThread類performLaunchActivity來建立要啟動的Activity元件,在建立Activity元件的過程中,還會為該Activity元件建立視窗物件和檢視物件;接著Activity元件建立完成之後,通過呼叫ActivityThread類的handleResumeActivity將它啟用。

所以我們先看下handleResumeActivity方法一個重點,如下:

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
        // If we are getting ready to gc after going to the background, well
        // we are back active so skip it.
        ......
        // TODO Push resumeArgs into the activity for consideration
        ActivityClientRecord r = performResumeActivity(token, clearHide);

        if (r != null) {
            ......
            // If the window hasn't yet been added to the window manager,
            // and this guy didn't finish itself or start another activity,
            // then go ahead and add the window.
            ......
            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
            ......
            // The window is now visible if it has been added, we are not
            // simply finishing, and we are not starting another activity.
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                ......
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }
            ......
        } else {
            // If an exception was thrown when trying to resume, then
            // just end this activity.
            ......
        }
    }

看見r.activity.makeVisible();語句沒?呼叫Activity的makeVisible方法顯示我們上面通過setContentView建立的mDecor檢視族。所以我們看下Activity的makeVisible方法,如下:

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

看見沒有,通過DecorView(FrameLayout,也即View)的setVisibility方法將View設定為VISIBLE,至此顯示出來。

到此setContentView的完整流程分析完畢。

【工匠若水http://blog.csdn.net/yanbober轉載煩請註明出處,尊重分享成果】

3 Android5.1.1(API 22)看看LayoutInflater機制原理

上面在分析setContentView過程中可以看見,在PhoneWindow的setContentView中調運了mLayoutInflater.inflate(layoutResID, mContentParent);,在PhoneWindow的generateLayout中調運了View in = mLayoutInflater.inflate(layoutResource, null);,當時我們沒有詳細分析,只是告訴通過xml得到View物件。現在我們就來分析分析這一問題。

3-1 通過例項引出問題

在開始之前我們先來做一個測試,我們平時最常見的就是ListView的Adapter中使用LayoutInflater載入xml的item佈局檔案,所以咱們就以ListView為例,如下:

省略掉Activity程式碼等,首先給出Activity的佈局檔案,如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/listview"
        android:dividerHeight="5dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></ListView>

</LinearLayout>

給出兩種不同的ListView的item佈局檔案。

textview_layout.xml檔案:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="40dp"
    android:text="Text Test"
    android:background="#ffa0a00c"/>

textview_layout_parent.xml檔案:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="100dp"
        android:layout_height="40dp"
        android:text="Text Test"
        android:background="#ffa0a00c"/>

</LinearLayout>

ListView的自定義Adapter檔案:

public class InflateAdapter extends BaseAdapter {
    private LayoutInflater mInflater = null;

    public InflateAdapter(Context context) {
        mInflater = LayoutInflater.from(context);
    }

    @Override
    public int getCount() {
        return 8;
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        //說明:這裡是測試inflate方法引數程式碼,不再考慮效能優化等TAG處理
        return getXmlToView(convertView, position, parent);
    }

    private View getXmlToView(View convertView, int position, ViewGroup parent) {
        View[] viewList = {
                mInflater.inflate(R.layout.textview_layout, null),
//                mInflater.inflate(R.layout.textview_layout, parent),
                mInflater.inflate(R.layout.textview_layout, parent, false),
//                mInflater.inflate(R.layout.textview_layout, parent, true),
                mInflater.inflate(R.layout.textview_layout, null, true),
                mInflater.inflate(R.layout.textview_layout, null, false),

                mInflater.inflate(R.layout.textview_layout_parent, null),
//                mInflater.inflate(R.layout.textview_layout_parent, parent),
                mInflater.inflate(R.layout.textview_layout_parent, parent, false),
//                mInflater.inflate(R.layout.textview_layout_parent, parent, true),
                mInflater.inflate(R.layout.textview_layout_parent, null, true),
                mInflater.inflate(R.layout.textview_layout_parent, null, false),
        };

        convertView = viewList[position];

        return convertView;
    }
}

當前程式碼執行結果:

PS:當開啟上面viewList陣列中任意一行註釋都會丟擲異常(java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView)。

你指定有些蒙圈了,而且比較鬱悶,同時想弄明白inflate的這些引數都是啥意思。執行結果為何有這麼大差異呢?

那我告訴你,你現在先別多想,記住這回事,咱們先看原始碼,下面會告訴你為啥。

3-2 從LayoutInflater原始碼例項化說起

我們先看一下原始碼中LayoutInflater例項化獲取的方法:

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

看見沒有?是否很熟悉?我們平時寫應用獲取LayoutInflater例項時不也就兩種寫法嗎,如下:

    LayoutInflater lif = LayoutInflater.from(Context context);

    LayoutInflater lif = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

可以看見from方法僅僅是對getSystemService的一個安全封裝而已。

3-3 LayoutInflater原始碼的View inflate(…)方法族剖析

得到LayoutInflater物件之後我們就是傳遞xml然後解析得到View,如下方法:

    public View inflate(int resource, ViewGroup root) {
        return inflate(resource, root, root != null);
    }

繼續看inflate(int resource, ViewGroup root, boolean attachToRoot)方法,如下:

    public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

這個方法的第8行獲取到XmlResourceParser介面的例項(Android預設實現類為Pull解析XmlPullParser)。接著看第10行inflate(parser, root, attachToRoot);,你會發現無論哪個inflate過載方法最後都調運了inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)方法,如下:

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

            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context)mConstructorArgs[0];
            mConstructorArgs[0] = mContext;
            //定義返回值,初始化為傳入的形參root
            View result = root;

            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
                //如果一開始就是END_DOCUMENT,那說明xml檔案有問題
                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }
                //有了上面判斷說明這裡type一定是START_TAG,也就是xml檔案裡的root node
                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)) {
                //處理merge tag的情況(merge,你懂的,APP的xml效能優化)
                    //root必須非空且attachToRoot為true,否則拋異常結束(APP使用merge時要注意的地方,
                    //因為merge的xml並不代表某個具體的view,只是將它包起來的其他xml的內容加到某個上層
                    //ViewGroup中。)
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    //遞迴inflate方法調運
                    rInflate(parser, root, attrs, false, false);
                } else {
                    // Temp is the root view that was found in the xml
                    //xml檔案中的root view,根據tag節點建立view物件
                    final View temp = createViewFromTag(root, name, attrs, false);

                    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
                        //根據root生成合適的LayoutParams例項
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            //如果attachToRoot=false就呼叫view的setLayoutParams方法
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }
                    // Inflate all children under temp
                    //遞迴inflate剩下的children
                    rInflate(parser, temp, attrs, true, 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非空且attachToRoot=true則將xml檔案的root view加到形參提供的root裡
                        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) {
                        //返回xml裡解析的root view
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                InflateException ex = new InflateException(e.getMessage());
                ex.initCause(e);
                throw ex;
            } catch (IOException e) {
                InflateException ex = new InflateException(
                        parser.getPositionDescription()
                        + ": " + e.getMessage());
                ex.initCause(e);
                throw ex;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            //返回引數root或xml檔案裡的root view
            return result;
        }
    }

從上面的原始碼分析我們可以看出inflate方法的引數含義:

  • inflate(xmlId, null); 只建立temp的View,然後直接返回temp。

  • inflate(xmlId, parent); 建立temp的View,然後執行root.addView(temp, params);最後返回root。

  • inflate(xmlId, parent, false); 建立temp的View,然後執行temp.setLayoutParams(params);然後再返回temp。

  • inflate(xmlId, parent, true); 建立temp的View,然後執行root.addView(temp, params);最後返回root。

  • inflate(xmlId, null, false); 只建立temp的View,然後直接返回temp。

  • inflate(xmlId, null, true); 只建立temp的View,然後直接返回temp。

到此其實已經可以說明我們上面示例部分執行效果差異的原因了(在此先強調一個Android的概念,下一篇文章我們會對這段話作一解釋:我們經常使用View的layout_width和layout_height來設定View的大小,而且一般都可以正常工作,所以有人時常認為這兩個屬性就是設定View的真實大小一樣;然而實際上這些屬性是用於設定View在ViewGroup佈局中的大小的;這就是為什麼Google的工程師在變數命名上將這種屬性叫作layout_width和layout_height,而不是width和height的原因了。),如下:

  • mInflater.inflate(R.layout.textview_layout, null)不能正確處理我們設定的寬和高是因為layout_width,layout_height是相對了父級設定的,而此temp的getLayoutParams為null。
  • mInflater.inflate(R.layout.textview_layout, parent)能正確顯示我們設定的寬高是因為我們的View在設定setLayoutParams時params = root.generateLayoutParams(attrs)不為空。
    Inflate(resId , parent,false ) 可以正確處理,因為temp.setLayoutParams(params);這個params正是root.generateLayoutParams(attrs);得到的。
  • mInflater.inflate(R.layout.textview_layout, null, true)與mInflater.inflate(R.layout.textview_layout, null, false)不能正確處理我們設定的寬和高是因為layout_width,layout_height是相對了父級設定的,而此temp的getLayoutParams為null。
  • textview_layout_parent.xml作為item可以正確顯示的原因是因為TextView具備上級ViewGroup,上級ViewGroup的layout_width,layout_height會失效,當前的TextView會有效而已。
  • 上面例子中說放開那些註釋執行會報錯java.lang.UnsupportedOperationException:
    addView(View, LayoutParams) is not supported是因為AdapterView原始碼中呼叫了root.addView(temp, params);而此時的root是我們的ListView,ListView為AdapterView的子類,所以我們看下AdapterView抽象類中addView原始碼即可明白為啥了,如下:
    /**
     * This method is not supported and throws an UnsupportedOperationException when called.
     *
     * @param child Ignored.
     *
     * @throws UnsupportedOperationException Every time this method is invoked.
     */
    @Override
    public void addView(View child) {
        throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
    }

這裡不再做過多解釋。

咦?別急,到這裡指定機智的人會問,我們在寫App時Activity中指定佈局檔案的時候,xml佈局檔案或者我們用java編寫的View最外層的那個佈局是可以指定大小的啊?他們最外層的layout_width和layout_height都是有作用的啊?

是這樣的,還記得我們上面的分析嗎?我們自己的xml佈局通過setContentView()方法放置到哪去了呢?記不記得id為content的FrameLayout呢?所以我們xml或者java的View的最外層佈局的layout_width和layout_height屬性才會有效果,就是這麼回事而已。

3-4 LayoutInflater原始碼inflate(…)方法中調運的一些非public方法剖析

看下inflate方法中被調運的rInflate方法,原始碼如下:

    void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
            boolean finishInflate, boolean inheritContext) throws XmlPullParserException,
            IOException {

        final int depth = parser.getDepth();
        int type;
        //XmlPullParser解析器的標準解析模式
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            //找到START_TAG節點程式才繼續執行這個判斷語句之後的邏輯
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            //獲取Name標記
            final String name = parser.getName();
            //處理REQUEST_FOCUS的標記
            if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_TAG.equals(name)) {
                //處理tag標記
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                //處理include標記
                if (parser.getDepth() == 0) {
                    //include節點如果是根節點就拋異常
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, parent, attrs, inheritContext);
            } else if (TAG_MERGE.equals(name)) {
                //merge節點必須是xml檔案裡的根節點(這裡不該再出現merge節點)
                throw new InflateException("<merge /> must be the root element");
            } else {
                //其他自定義節點
                final View view = createViewFromTag(parent, name, attrs, inheritContext);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs, true, true);
                viewGroup.addView(view, params);
            }
        }
        //parent的所有子節點都inflate完畢的時候回onFinishInflate方法
        if (finishInflate) parent.onFinishInflate();
    }

可以看見,上面方法主要就是迴圈遞迴解析xml檔案,解析結束回撥View類的onFinishInflate方法,所以View類的onFinishInflate方法是一個空方法,如下:

    /**
     * Finalize inflating a view from XML.  This is called as the last phase
     * of inflation, after all child views have been added.
     *
     * <p>Even if the subclass overrides onFinishInflate, they should always be
     * sure to call the super method, so that we get called.
     */
    protected void onFinishInflate() {
    }

可以看見,當我們自定義View時在建構函式inflate一個xml後可以實現onFinishInflate這個方法一些自定義的邏輯。

至此LayoutInflater的原始碼核心部分已經分析完畢。

4 從LayoutInflater與setContentView來說說應用佈局檔案的優化技巧

通過上面的原始碼分析可以發現,xml檔案解析實質是遞迴控制元件,解析屬性的過程。所以說巢狀過深不僅效率低下還可能引起調運棧溢位。同時在解析那些tag時也有一些特殊處理,從原始碼看編寫xml還是有很多要注意的地方的。所以說對於Android的xml來說是有一些優化技巧的(PS:佈局優化可以通過hierarchyviewer來檢視,通過lint也可以自動檢查出來一些),如下:

儘量使用相對佈局,減少不必要層級結構。不用解釋吧?遞迴解析的原因。

使用merge屬性。使用它可以有效的將某些符合條件的多餘的層級優化掉。使用merge的場合主要有兩處:自定義View中使用,父元素儘量是FrameLayout,當然如果父元素是其他佈局,而且不是太複雜的情況下也是可以使用的;Activity中的整體佈局,根元素需要是FrameLayout。但是使用merge標籤還是有一些限制的,具體是:merge只能用在佈局XML檔案的根元素;使用merge來inflate一個佈局時,必須指定一個ViewGroup作為其父元素,並且要設定inflate的attachToRoot引數為true。(參照inflate(int, ViewGroup, boolean)方法);不能在ViewStub中使用merge標籤;最直觀的一個原因就是ViewStub的inflate方法中根本沒有attachToRoot的設定。

使用ViewStub。一個輕量級的頁面,我們通常使用它來做預載入處理,來改善頁面載入速度和提高流暢性,ViewStub本身不會佔用層級,它最終會被它指定的層級取代。ViewStub也是有一些缺點,譬如:ViewStub只能Inflate一次,之後ViewStub物件會被置為空。按句話說,某個被ViewStub指定的佈局被Inflate後,就不能夠再通過ViewStub來控制它了。所以它不適用 於需要按需顯示隱藏的情況;ViewStub只能用來Inflate一個佈局檔案,而不是某個具體的View,當然也可以把View寫在某個佈局檔案中。如果想操作一個具體的view,還是使用visibility屬性吧;VIewStub中不能巢狀merge標籤。

使用include。這個標籤是為了佈局重用。

控制元件設定widget以後對於layout_hORw-xxx設定0dp。減少系統運算次數。

如上就是一些APP佈局檔案基礎的優化技巧。

5 總結

至此整個Activity的setContentView與Android的LayoutInflater相關原理都已經分析完畢。關於本篇中有些地方直接給出結論的知識點後面的文章中會做一說明。

setContentView整個過程主要是如何把Activity的佈局檔案或者java的View新增至窗口裡,重點概括為:

  1. 建立一個DecorView的物件mDecor,該mDecor物件將作為整個應用視窗的根檢視。

  2. 依據Feature等style theme建立不同的視窗修飾佈局檔案,並且通過findViewById獲取Activity佈局檔案該存放的地方(視窗修飾佈局檔案中id為content的FrameLayout)。

  3. 將Activity的佈局檔案新增至id為content的FrameLayout內。

  4. 當setContentView設定顯示OK以後會回撥Activity的onContentChanged方法。Activity的各種View的findViewById()方法等都可以放到該方法中,系統會幫忙回撥。

如下就是整個Activity的分析簡單關係圖:

LayoutInflater的使用中重點關注inflate方法的引數含義:

  • inflate(xmlId, null); 只建立temp的View,然後直接返回temp。

  • inflate(xmlId, parent); 建立temp的View,然後執行root.addView(temp, params);最後返回root。

  • inflate(xmlId, parent, false); 建立temp的View,然後執行temp.setLayoutParams(params);然後再返回temp。

  • inflate(xmlId, parent, true); 建立temp的View,然後執行root.addView(temp, params);最後返回root。

  • inflate(xmlId, null, false); 只建立temp的View,然後直接返回temp。

  • inflate(xmlId, null, true); 只建立temp的View,然後直接返回temp。

當我們自定義View時在建構函式inflate一個xml後可以實現onFinishInflate這個方法一些自定義的邏輯。

【工匠若水http://blog.csdn.net/yanbober轉載煩請註明出處,尊重分享成果】