Android 從setContentView談Activity界面的加載過程

一、前言

作為一個(gè)Android開發(fā)人員,setContentView方法肯定相當(dāng)不陌生,因?yàn)樵谖覀兠恳粋€(gè)需要呈現(xiàn)頁面的Activity的onCreate方法中都會(huì)調(diào)用setContentView方法來加載我們事先寫好的布局文件。然而或許大部分人也和我一樣一直都是用用就好,也沒有深入思考該方法具體是怎樣將我們的布局文件呈現(xiàn)給用戶的。接下來我們來好好研究研究這個(gè)方法的作用原理吧!

二、Android窗口

既然我們想要知道Android的頁面加載過程,那么我們就得先了解Android系統(tǒng)中的窗口布局。一般來說,當(dāng)我們?cè)O(shè)置窗口的Theme為常見的樣式時(shí),Android的窗口如下圖所示: Android的窗口主要是圖中PhoneWindow所包含的部分:


Android窗口模型

Android窗口模型解析

Android常用的窗口布局文件為R.layout.screen_title,位于frameworks/base/core/res/layout/:

<!--
    This is an optimized layout for a screen, with the minimum set of features
    enabled.
    -->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:fitsSystemWindows="true">
        
        <FrameLayout
            android:layout_width="match_parent" 
            android:layout_height="?android:attr/windowTitleSize"
            style="?android:attr/windowTitleBackgroundStyle">
            <TextView android:id="@android:id/title" 
                style="?android:attr/windowTitleStyle"
                android:background="@null"
                android:fadingEdge="horizontal"
                android:gravity="center_vertical"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </FrameLayout>
        
        <FrameLayout android:id="@android:id/content"
            android:layout_width="match_parent" 
            android:layout_height="0dip"
            android:layout_weight="1"
            android:foregroundGravity="fill_horizontal|top"
            android:foreground="?android:attr/windowContentOverlay" />
    </LinearLayout>

可以看出,DecorView中包含一個(gè)Vertical的LinearLayout布局文件,文件中有兩個(gè)FrameLayout,上面一個(gè)FrameLayout用于顯示Activity的標(biāo)題,下面一個(gè)FrameLayout用于顯示Activity的具體內(nèi)容,也就是說,我們通過setContentView方法加載的布局文件/View將顯示在該FrameLayout中。

三、setContentView加載view的流程

  • 1、Activity中的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
    //getWindow()方法將返回與該Activity相關(guān)聯(lián)的Window對(duì)象
    getWindow().setContentView(layoutResID);
    
    /*
     * 當(dāng)該Activity是另一個(gè)Activity的子Activity、該Activity不含屬性值Window.FEATURE_ACTION_BAR   
     * 或者該Activity目前已有一個(gè)ActionBar時(shí),該方法不進(jìn)行任何操作,直接返回
     * 否則初始化窗口的ActionBar,并為其設(shè)置相應(yīng)的屬性值
     */
    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 addContentView(View view, ViewGroup.LayoutParams params) {
    getWindow().addContentView(view, params);
    initWindowDecorActionBar();
}

可以看到,在Activity的四個(gè)setContentView方法中,都分別調(diào)用了Window的相應(yīng)方法。

  • 2、 PhoneWindow中的setContentView方法
    Window中的setContentView方法均為抽象方法,所以跳過,直接看Window的實(shí)現(xiàn)類PhoneWindow中的setContentView方法
@Override
    public void setContentView(int layoutResID) {
        /*
         * private ViewGroup mContentParent:該變量即為Activity的根布局文件,這是mDecor自身或mDecor的子類
         * installDecor()方法用于加載mDecor,后面詳說
         * FEATURE_CONTENT_TRANSITIONS:窗口內(nèi)容發(fā)生變化時(shí)是否需要使用TransitionManager進(jìn)行過渡的標(biāo)識(shí)
         */
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            // 需要使用TransitionManager進(jìn)行過渡時(shí)的處理
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            //不需要過渡時(shí),通過inflate方法將layoutResID中的View樹添加到窗口中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        //請(qǐng)求設(shè)置Window內(nèi)容的屬性值,將其寫入一個(gè)WindowInsets類中
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
    @Override
    public void setContentView(View view) {
        //注意:當(dāng)使用該方法設(shè)置窗口布局文件時(shí),系統(tǒng)將默認(rèn)設(shè)置view的width和height均為MATCH_PARENT
        //這里便可以解釋上一篇博客《Android LayoutInflater.inflater方法詳解》中的Case1了
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        //該方法與上面setContentView(int layoutResID)唯一的不同點(diǎn)在于第二個(gè)if語句的else語句塊內(nèi)容如下
       //通過調(diào)用mContentParent的addView方法將view添加到窗口中
       //也就是說這兩個(gè)方法唯一的區(qū)別在于將view添加到窗口的方式不同,其余并無差別
       mContentParent.addView(view, params);   
    }

綜上所述,該方法的主要工作為;

  • 第一步:
    如果mContentParent 為空(即這是第一次調(diào)用setContentView方法),則installDecor()
    如果不是第一次調(diào)用該方法,且無需使用 TransitionManager進(jìn)行過渡,則直接將窗口中的所有子View均移除

  • 第二步:
    如果需要使用 TransitionManager進(jìn)行過渡,使用 TransitionManager進(jìn)行過渡
    否則采用恰當(dāng)?shù)姆绞綄iew添加到窗口中

四、部分方法詳解

1、installDecor()方法:

該方法位于PhoneWindow類中

private void installDecor() {
  // 如果mDecor為空,則生成一個(gè)Decor,并設(shè)置其屬性
  if (mDecor == null) {
   // 此句即mDecor = new DecorView(getContext(), -1)
   mDecor = generateDecor();
   
   /*
    * setDescendantFocusability用于設(shè)置mDecor中的子View的聚焦性
    * 該方法決定了mDecor與其中包含的子View之間關(guān)于焦點(diǎn)獲取的關(guān)系
    * FOCUS_AFTER_DESCENDANTS表示只有當(dāng)mDecor的子View都不愿意獲取焦點(diǎn)時(shí) 才讓mDecor獲取焦點(diǎn)
    */
   mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
   
   // 設(shè)置mDecor為整個(gè)Activity窗口的根節(jié)點(diǎn),從此處可以看出窗口根節(jié)點(diǎn)為一個(gè)DecorView
   mDecor.setIsRootNamespace(true);
   
   /*
    * if條件滿足時(shí),在animation時(shí)執(zhí)行mInvalidatePanelMenuRunnable這個(gè)Runnable動(dòng)作
    */
   if (!mInvalidatePanelMenuPosted
     && mInvalidatePanelMenuFeatures != 0) {
    mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
   }
  }
  
  // 如果mContentParent為空,則生成一個(gè)Decor,并設(shè)置其屬性
  // 后面會(huì)詳說generateLayout(DecorView decor)方法
  if (mContentParent == null) {
   mContentParent = generateLayout(mDecor);
   // Set up decor part of UI to ignore fitsSystemWindows if
   // appropriate.
   mDecor.makeOptionalFitsSystemWindows();
   
   /*
    * DecorContentParent位于com.android.internal.widget中,是一個(gè)接口
    * 由應(yīng)用程序窗口的頂層Decor實(shí)現(xiàn),該類主要為mDecor提供了許多title/window decor features
    */
   final DecorContentParent decorContentParent = (DecorContentParent) mDecor
     .findViewById(R.id.decor_content_parent);
     
   if (decorContentParent != null) {
    /*
     * decorContentParent非空時(shí) 
     * 1. 將decorContentParent賦值給mDecorContentParent 
     * 2. 設(shè)置窗口回調(diào)函數(shù) 
     * 3.設(shè)置窗口的title、icon、logo等屬性值 
     * 為了加強(qiáng)博客的可讀性,就未將這部分代碼貼出來,只將主要功能進(jìn)行了簡單介紹
     * 想要詳細(xì)了解的可以直接參看源碼
     */
   } else {
    /*
     * decorContentParent為空時(shí)根據(jù)窗口是否為一個(gè)包含Title的窗口決定是否顯示title
     * 如果窗口包含特征FEATURE_NO_TITLE,則隱藏窗口的title view 否則設(shè)置窗口的title
     */
   }
   
   if (mDecor.getBackground() == null
     && mBackgroundFallbackResource != 0) {
    mDecor.setBackgroundFallback(mBackgroundFallbackResource);
   }
   if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
    // Only inflate or create a new TransitionManager if the caller
    // hasn't already set a custom one.
    //源碼未貼出
   }
  }
 }

2、ViewGroup generateLayout(DecorView decor)方法

//返回當(dāng)前Activity的內(nèi)容區(qū)域視圖,即我們的布局文件顯示區(qū)域mContentParent
protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        //從當(dāng)前Window的Theme中獲取一組屬性值,賦給a
        TypedArray a = getWindowStyle();
        /*
   * 此處有段代碼未貼出,功能為:
   * 1. 根據(jù)Activity的Theme特征,為當(dāng)前窗口選擇布局文件的修飾feature
   * 2. Inflate the window decor
   */
        int layoutResource;
        int features = getLocalFeatures();
  /*
   * 此處有段代碼未貼出
   * 1. getLocalFeatures()返回一個(gè)用于描述當(dāng)前Window特征的整數(shù)值
   * 2. layoutResource為根據(jù)features所指代的窗口特征值而為當(dāng)前窗口選定的資源文件id
   * 3. 系統(tǒng)包含多個(gè)布局資源文件,位于frameworks/base/core/res/layout/
   * 4. 主要有:R.layout.dialog_titile_icons、R.layout.screen_title_icons
   *     R.layout.screen_progress、R.layout.dialog_custom_title
   *     R.layout.dialog_title   
   *     R.layout.screen_title    最常用的Activity窗口修飾布局文件
   *        R.layout.screen_simple   全屏的Activity窗口布局文件
   */
  //startChanging()方法內(nèi)容:mChanging = true;
        mDecor.startChanging();
  //將layoutResource資源文件包含的View樹添加到decor中
  //width和height均為MATCH_PARENT
  //并為mContentRoot和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");
        }
        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
            ProgressBar progress = getCircularProgressBar(false);
            if (progress != null) {
                progress.setIndeterminate(true);
            }
        }
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            registerSwipeCallbacks();
        }
        //后面包含一段只能應(yīng)用于頂層窗口的一些Remaining steps
        //主要用于設(shè)置一些title和background屬性
        return contentParent;
}

五、總結(jié)

1、setContentView方法工作流程

setContentView方法的具體實(shí)現(xiàn)是在PhoneWindow類中,主要通過如下幾個(gè)步驟完成xml布局資源文件或View的加載。

注意:
使用setContentView(View view)方法設(shè)置Activity的布局時(shí),系統(tǒng)會(huì)默認(rèn)將該view的width和height值均設(shè)為MATCH_PARENT,而不是使用view自己的屬性值,所以如果想通過一個(gè)View對(duì)象設(shè)置布局,又想使用自己設(shè)置的參數(shù)值時(shí),需要使用setContentView(View view, LayoutParams params)方法

  • 第一步:若是首次使用setContentView方法,則先創(chuàng)建一個(gè)DecorView對(duì)象mDecor,該對(duì)象是整個(gè)Activity窗口的根視圖;然后根據(jù)程序中選擇的Activity的Theme/Style等屬性值為窗口添加布局屬性和相應(yīng)的修飾文件,并通過findViewById方法獲取對(duì)應(yīng)的根布局文件添加到mDecor中,也就是說,第一次使用該方法時(shí)會(huì)將Activity顯示區(qū)域進(jìn)行初始化;若不是第一次使用該方法,則之前已完成初始化過程并獲得了mDecor和mContentParent對(duì)象,則只需要將之前添加到mContentParent區(qū)域的Views移除,空出該區(qū)域重新進(jìn)行布局即可,簡而言之,就是對(duì)mContentParent區(qū)域進(jìn)行刷新;

  • 第二步:通過inflate(加載xml文件)或addView(加載View)方法將Activity的布局文件添加到mContentParent區(qū)域;
    當(dāng)setContentView設(shè)置顯示OK以后,回調(diào)Activity的onContentChanged方法,通知Activity布局文件已經(jīng)成功加載完成,接下來我們便可以使用findViewById方法獲取布局文件中含有id屬性的view對(duì)象了;

2、淺談布局文件優(yōu)化技巧

  • 從上面的分析可知,在加載xml布局文件時(shí),系統(tǒng)是通過遞歸的方式從根節(jié)點(diǎn)到葉子節(jié)點(diǎn)一步一步對(duì)控件的屬性進(jìn)行解析的,所以xml文件的層次越深,效率越低,如果嵌套過多,還有可能導(dǎo)致棧溢出,所以在書寫布局文件時(shí),應(yīng)盡量對(duì)布局文件進(jìn)行優(yōu)化,通過使用相對(duì)布局等方式減少不必要的嵌套層次

  • 在源碼中,可以看到對(duì)merge標(biāo)簽進(jìn)行處理的過程。在某些場合下,merge標(biāo)簽的使用也可以有效減少布局文件的嵌套層次。如某些比較復(fù)雜的布局文件,需要將布局文件拆分開來,分為一個(gè)根布局文件和若干個(gè)子布局文件,這時(shí)可能子布局文件的根節(jié)點(diǎn)在添加到根布局文件中時(shí)并沒有太多意義,只會(huì)增加根布局文件的嵌套層次,這種情況下,在子布局文件處使用merge標(biāo)簽就可以去掉無謂的嵌套層次。不過merge標(biāo)簽的使用也是有限制的,首先merge標(biāo)簽只能用于一個(gè)xml文件的根節(jié)點(diǎn);其次,使用inflate方法來加載一個(gè)根節(jié)點(diǎn)為merge標(biāo)簽的布局文件時(shí),需要為該文件指定一個(gè)ViewGroup對(duì)象作為其父元素,同時(shí)需要設(shè)置attachToRoot屬性為true,否則會(huì)拋出異常;

  • 利用include標(biāo)簽增加布局文件的重用性和可讀性;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評(píng)論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,947評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評(píng)論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,268評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,817評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,039評(píng)論 0 285
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,551評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,502評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,662評(píng)論 1 366
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評(píng)論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,907評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評(píng)論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,255評(píng)論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,637評(píng)論 2 370

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