Android應用setContentView與LayoutInflater加載解析機制源碼分析

1 背景

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

理解xml等控件是咋被顯示的原理,通常大家寫代碼都是直接在onCreate裏setContentView就完事,沒怎麼關注其實現原理。
所以接下來主要分析的就是View或者ViewGroup對象是如何添加至應用程序界面(窗口)顯示的。我們準備從Activity的setContentView方法開始來說(因爲默認Activity中放入我們的xml或者Java控件是通過setContentView方法來操作的,當調運了setContentView所有的控件就得到了顯示)。

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

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

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

在開始分析Activity組合對象Window的setContentView方法之前請先明確如下關係

這裏寫圖片描述

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

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

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

PhoneWindow是Window的具體繼承實現類。而且該類內部包含了一個DecorView對象,該DectorView對象是所有應用窗口(Activity界面)的根View。

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();
        }
    }
 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方法,如下:
可以看見generateDecor方法僅僅是new一個DecorView的實例。

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

回到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添加至窗口裏,上面的過程可以重點概括爲:

創建一個DecorView的對象mDecor,該mDecor對象將作爲整個應用窗口的根視圖。

依據Feature等style theme創建不同的窗口修飾佈局文件,並且通過findViewById獲取Activity佈局文件該存放的地方(窗口修飾佈局文件中id爲content的FrameLayout)。

將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的完整流程分析完畢。

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添加至窗口裏,重點概括爲:

創建一個DecorView的對象mDecor,該mDecor對象將作爲整個應用窗口的根視圖。

依據Feature等style theme創建不同的窗口修飾佈局文件,並且通過findViewById獲取Activity佈局文件該存放的地方(窗口修飾佈局文件中id爲content的FrameLayout)。

將Activity的佈局文件添加至id爲content的FrameLayout內。

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

如下就是整個Activity的分析簡單關係圖:
這裏寫圖片描述
ayoutInflater的使用中重點關注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這個方法一些自定義的邏輯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章