Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android應用setContentView與LayoutInflater加載解析機制源碼分析

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

編輯:關於Android編程

 

1 背景

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

理解xml等控件是咋被顯示的原理,通常大家寫代碼都是直接在onCreate裡setContentView就完事,沒怎麼關注其實現原理。 前面分析《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類的實現類。

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

Window是一個抽象類,提供了繪制窗口的一組通用API。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPlBob25lV2luZG93ysdXaW5kb3e1xL7fzOW8zLPQyrXP1sDgoaO2+MfSuMPA4MTasr+w/LqswcvSu7j2RGVjb3JWaWV3ttTP86OsuMNEZWN0b3JWaWV3ttTP88rHy/nT0NOm08O0sL/aKEFjdGl2aXR5vefD5im1xLj5Vmlld6GjPC9wPg0KPHA+RGVjb3JWaWV3ysdQaG9uZVdpbmRvd7XExNqyv8Dgo6zKx0ZyYW1lTGF5b3V0tcTX08Dgo6zKx7bURnJhbWVMYXlvdXS9+NDQuabE3LXE0N7KzqOoy/nS1L3QRGVjb3JYWFijqaOsysfL+dPQ06bTw7Swv9q1xLj5VmlldyChozwvcD4NCjxwPtLAvt3D5s/yttTP87TTs+nP87W9vt/M5c7Sw8e/ydLUwOCxyMnPw+a52M+1vs3P8cjnz8KjujwvcD4NCjxwPldpbmRvd8rH0ru/6bXn19PGwaOsUGhvbmVXaW5kb3fKx9K7v+nK1rv6tefX08bBo6xEZWNvclZpZXe+zcrHtefX08bB0qrP1Mq+tcTE2sjdo6xBY3Rpdml0eb7NysfK1rv6tefX08bBsLLXsM671sOhozwvcD4NCjxoMyBpZD0="2-2-窗口phonewindow類的setcontentview方法">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文件




    
        ......
    

主界面布局文件:



    

APP運行界面:
這裡寫圖片描述

看見沒有,上面我們將主題設置為NoTitleBar,所以在generateLayout方法中的layoutResource變量值為R.layout.screen_simple,所以我們看下系統這個screen_simple.xml布局文件,如下:


    
    <framelayout android:foreground="?android:attr/windowContentOverlay" android:foregroundgravity="fill_horizontal|top" android:foregroundinsidepadding="false" android:id="@android:id/content" android:layout_height="match_parent" android:layout_width="match_parent">
</framelayout>

布局中,一般會包含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的完整流程分析完畢。

【工匠若水 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的布局文件,如下:



    

給出兩種不同的ListView的item布局文件。

textview_layout.xml文件:


textview_layout_parent.xml文件:




    

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( 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( cannot be the root element);
                }
                parseInclude(parser, parent, attrs, inheritContext);
            } else if (TAG_MERGE.equals(name)) {
                //merge節點必須是xml文件裡的根節點(這裡不該再出現merge節點)
                throw new InflateException( 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.
     *
     * 

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()方法等都可以放到該方法中,系統會幫忙回調。

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這個方法一些自定義的邏輯。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved