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

一蚌堵、前言

作為一個Android開發(fā)人員介衔,setContentView方法肯定相當不陌生,因為在我們每一個需要呈現頁面的Activity的onCreate方法中都會調用setContentView方法來加載我們事先寫好的布局文件历极。然而或許大部分人也和我一樣一直都是用用就好,也沒有深入思考該方法具體是怎樣將我們的布局文件呈現給用戶的瑰谜。接下來我們來好好研究研究這個方法的作用原理吧!

二树绩、Android窗口

既然我們想要知道Android的頁面加載過程萨脑,那么我們就得先了解Android系統中的窗口布局。一般來說饺饭,當我們設置窗口的Theme為常見的樣式時渤早,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中包含一個Vertical的LinearLayout布局文件鹊杖,文件中有兩個FrameLayout悴灵,上面一個FrameLayout用于顯示Activity的標題,下面一個FrameLayout用于顯示Activity的具體內容骂蓖,也就是說积瞒,我們通過setContentView方法加載的布局文件/View將顯示在該FrameLayout中。

三登下、setContentView加載view的流程

  • 1茫孔、Activity中的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
    //getWindow()方法將返回與該Activity相關聯的Window對象
    getWindow().setContentView(layoutResID);
    
    /*
     * 當該Activity是另一個Activity的子Activity、該Activity不含屬性值Window.FEATURE_ACTION_BAR   
     * 或者該Activity目前已有一個ActionBar時被芳,該方法不進行任何操作缰贝,直接返回
     * 否則初始化窗口的ActionBar,并為其設置相應的屬性值
     */
    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的四個setContentView方法中,都分別調用了Window的相應方法赋朦。

  • 2篓冲、 PhoneWindow中的setContentView方法
    Window中的setContentView方法均為抽象方法,所以跳過宠哄,直接看Window的實現類PhoneWindow中的setContentView方法
@Override
    public void setContentView(int layoutResID) {
        /*
         * private ViewGroup mContentParent:該變量即為Activity的根布局文件壹将,這是mDecor自身或mDecor的子類
         * installDecor()方法用于加載mDecor,后面詳說
         * FEATURE_CONTENT_TRANSITIONS:窗口內容發(fā)生變化時是否需要使用TransitionManager進行過渡的標識
         */
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            // 需要使用TransitionManager進行過渡時的處理
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            //不需要過渡時毛嫉,通過inflate方法將layoutResID中的View樹添加到窗口中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        //請求設置Window內容的屬性值诽俯,將其寫入一個WindowInsets類中
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
    @Override
    public void setContentView(View view) {
        //注意:當使用該方法設置窗口布局文件時,系統將默認設置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)唯一的不同點在于第二個if語句的else語句塊內容如下
       //通過調用mContentParent的addView方法將view添加到窗口中
       //也就是說這兩個方法唯一的區(qū)別在于將view添加到窗口的方式不同承粤,其余并無差別
       mContentParent.addView(view, params);   
    }

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

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

  • 第二步:
    如果需要使用 TransitionManager進行過渡彻舰,使用 TransitionManager進行過渡
    否則采用恰當的方式將view添加到窗口中

四伐割、部分方法詳解

1、installDecor()方法:

該方法位于PhoneWindow類中

private void installDecor() {
  // 如果mDecor為空刃唤,則生成一個Decor隔心,并設置其屬性
  if (mDecor == null) {
   // 此句即mDecor = new DecorView(getContext(), -1)
   mDecor = generateDecor();
   
   /*
    * setDescendantFocusability用于設置mDecor中的子View的聚焦性
    * 該方法決定了mDecor與其中包含的子View之間關于焦點獲取的關系
    * FOCUS_AFTER_DESCENDANTS表示只有當mDecor的子View都不愿意獲取焦點時 才讓mDecor獲取焦點
    */
   mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
   
   // 設置mDecor為整個Activity窗口的根節(jié)點,從此處可以看出窗口根節(jié)點為一個DecorView
   mDecor.setIsRootNamespace(true);
   
   /*
    * if條件滿足時尚胞,在animation時執(zhí)行mInvalidatePanelMenuRunnable這個Runnable動作
    */
   if (!mInvalidatePanelMenuPosted
     && mInvalidatePanelMenuFeatures != 0) {
    mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
   }
  }
  
  // 如果mContentParent為空硬霍,則生成一個Decor,并設置其屬性
  // 后面會詳說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中笼裳,是一個接口
    * 由應用程序窗口的頂層Decor實現须尚,該類主要為mDecor提供了許多title/window decor features
    */
   final DecorContentParent decorContentParent = (DecorContentParent) mDecor
     .findViewById(R.id.decor_content_parent);
     
   if (decorContentParent != null) {
    /*
     * decorContentParent非空時 
     * 1. 將decorContentParent賦值給mDecorContentParent 
     * 2. 設置窗口回調函數 
     * 3.設置窗口的title崖堤、icon、logo等屬性值 
     * 為了加強博客的可讀性耐床,就未將這部分代碼貼出來密幔,只將主要功能進行了簡單介紹
     * 想要詳細了解的可以直接參看源碼
     */
   } else {
    /*
     * decorContentParent為空時根據窗口是否為一個包含Title的窗口決定是否顯示title
     * 如果窗口包含特征FEATURE_NO_TITLE,則隱藏窗口的title view 否則設置窗口的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)方法

//返回當前Activity的內容區(qū)域視圖胯甩,即我們的布局文件顯示區(qū)域mContentParent
protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        //從當前Window的Theme中獲取一組屬性值,賦給a
        TypedArray a = getWindowStyle();
        /*
   * 此處有段代碼未貼出堪嫂,功能為:
   * 1. 根據Activity的Theme特征偎箫,為當前窗口選擇布局文件的修飾feature
   * 2. Inflate the window decor
   */
        int layoutResource;
        int features = getLocalFeatures();
  /*
   * 此處有段代碼未貼出
   * 1. getLocalFeatures()返回一個用于描述當前Window特征的整數值
   * 2. layoutResource為根據features所指代的窗口特征值而為當前窗口選定的資源文件id
   * 3. 系統包含多個布局資源文件,位于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()方法內容: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();
        }
        //后面包含一段只能應用于頂層窗口的一些Remaining steps
        //主要用于設置一些title和background屬性
        return contentParent;
}

五、總結

1恶复、setContentView方法工作流程

setContentView方法的具體實現是在PhoneWindow類中怜森,主要通過如下幾個步驟完成xml布局資源文件或View的加載。

注意:
使用setContentView(View view)方法設置Activity的布局時谤牡,系統會默認將該view的width和height值均設為MATCH_PARENT,而不是使用view自己的屬性值副硅,所以如果想通過一個View對象設置布局,又想使用自己設置的參數值時翅萤,需要使用setContentView(View view, LayoutParams params)方法

  • 第一步:若是首次使用setContentView方法恐疲,則先創(chuàng)建一個DecorView對象mDecor,該對象是整個Activity窗口的根視圖套么;然后根據程序中選擇的Activity的Theme/Style等屬性值為窗口添加布局屬性和相應的修飾文件培己,并通過findViewById方法獲取對應的根布局文件添加到mDecor中,也就是說胚泌,第一次使用該方法時會將Activity顯示區(qū)域進行初始化漱凝;若不是第一次使用該方法,則之前已完成初始化過程并獲得了mDecor和mContentParent對象诸迟,則只需要將之前添加到mContentParent區(qū)域的Views移除茸炒,空出該區(qū)域重新進行布局即可,簡而言之阵苇,就是對mContentParent區(qū)域進行刷新壁公;

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

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

  • 從上面的分析可知芳绩,在加載xml布局文件時,系統是通過遞歸的方式從根節(jié)點到葉子節(jié)點一步一步對控件的屬性進行解析的撞反,所以xml文件的層次越深妥色,效率越低,如果嵌套過多遏片,還有可能導致棧溢出嘹害,所以在書寫布局文件時,應盡量對布局文件進行優(yōu)化吮便,通過使用相對布局等方式減少不必要的嵌套層次

  • 在源碼中笔呀,可以看到對merge標簽進行處理的過程。在某些場合下髓需,merge標簽的使用也可以有效減少布局文件的嵌套層次许师。如某些比較復雜的布局文件,需要將布局文件拆分開來僚匆,分為一個根布局文件和若干個子布局文件微渠,這時可能子布局文件的根節(jié)點在添加到根布局文件中時并沒有太多意義,只會增加根布局文件的嵌套層次白热,這種情況下敛助,在子布局文件處使用merge標簽就可以去掉無謂的嵌套層次粗卜。不過merge標簽的使用也是有限制的屋确,首先merge標簽只能用于一個xml文件的根節(jié)點;其次续扔,使用inflate方法來加載一個根節(jié)點為merge標簽的布局文件時攻臀,需要為該文件指定一個ViewGroup對象作為其父元素,同時需要設置attachToRoot屬性為true纱昧,否則會拋出異常刨啸;

  • 利用include標簽增加布局文件的重用性和可讀性;

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末识脆,一起剝皮案震驚了整個濱河市设联,隨后出現的幾起案子,更是在濱河造成了極大的恐慌灼捂,老刑警劉巖离例,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異悉稠,居然都是意外死亡宫蛆,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門的猛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來耀盗,“玉大人想虎,你說我怎么就攤上這事∨芽剑” “怎么了舌厨?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胡诗。 經常有香客問我邓线,道長,這世上最難降的妖魔是什么煌恢? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任骇陈,我火速辦了婚禮,結果婚禮上瑰抵,老公的妹妹穿的比我還像新娘你雌。我一直安慰自己,他們只是感情好二汛,可當我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布婿崭。 她就那樣靜靜地躺著,像睡著了一般肴颊。 火紅的嫁衣襯著肌膚如雪氓栈。 梳的紋絲不亂的頭發(fā)上侮东,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天创夜,我揣著相機與錄音,去河邊找鬼叉瘩。 笑死竟宋,一個胖子當著我的面吹牛提完,可吹牛的內容都是我干的。 我是一名探鬼主播丘侠,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼徒欣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蜗字?” 一聲冷哼從身側響起打肝,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挪捕,沒想到半個月后粗梭,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡担神,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年楼吃,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡孩锡,死狀恐怖酷宵,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情躬窜,我是刑警寧澤浇垦,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站荣挨,受9級特大地震影響男韧,放射性物質發(fā)生泄漏。R本人自食惡果不足惜默垄,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一此虑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧口锭,春花似錦朦前、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至荆隘,卻和暖如春恩伺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背椰拒。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工晶渠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耸三。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓乱陡,卻偏偏與公主長得像浇揩,于是被迫代替她去往敵國和親仪壮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內容