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

1、前言

    做爲一個Android開發人員,setContentView方法確定至關不陌生,由於在咱們每個須要呈現頁面的Activity的onCreate方法中都會調用setContentView方法來加載咱們事先寫好的佈局文件。然而或許大部分人也和我同樣一直都是用用就好,也沒有深刻思考該方法具體是怎樣將咱們的佈局文件呈現給用戶的。接下來咱們來好好研究研究這個方法的做用原理吧! java

2、Android窗口

    既然咱們想要知道Android的頁面加載過程,那麼咱們就得先了解Android系統中的窗口布局。通常來講,當咱們設置窗口的Theme爲常見的樣式時,Android的窗口以下圖所示: Android的窗口主要是圖中PhoneWindow所包含的部分: android

     

    Android經常使用的窗口布局文件爲R.layout.screen_title,位於frameworks/base/core/res/layout/: web

<!--
    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中。 app

3、setContentView加載view的流程

    一、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的相應方法。 ide

     二、PhoneWindow中的setContentView方法

        Window中的setContentView方法均爲抽象方法,因此跳過,直接看Window的實現類PhoneWindow中的setContentView方法 函數

@Override
    public void setContentView(int layoutResID) {
        /*
         * private ViewGroup mContentParent:該變量即爲Activity的根佈局文件,這是mDecor自身或mDecor的子類
         * installDecor()方法用於加載mDecor,後面詳說
         * FEATURE_CONTENT_TRANSITIONS:窗口內容發生變化時是否須要使用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添加到窗口中
       //也就是說這兩個方法惟一的區別在於將view添加到窗口的方式不一樣,其他並沒有差異
       mContentParent.addView(view, params);   
    }

    綜上所述,該方法的主要工做爲; 佈局

  • 第一步: post

    • 若是mContentParent 爲空(即這是第一次調用setContentView方法),則installDecor() 優化

    • 若是不是第一次調用該方法,且無需使用 TransitionManager進行過渡,則直接將窗口中的全部子View均移除 spa

  • 第二步:

    • 若是須要使用 TransitionManager進行過渡,使用 TransitionManager進行過渡

    • 不然採用恰當的方式將view添加到窗口中 

4、部分方法詳解

    一、installDecor()方法:

          該方法位於PhoneWindoe類中

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窗口的根節點,今後處能夠看出窗口根節點爲一個DecorView
   mDecor.setIsRootNamespace(true);
   
   /*
    * if條件知足時,在animation時執行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.
    //源碼未貼出
   }
  }
 }

    二、ViewGroup generateLayout(DecorView decor)方法

//返回當前Activity的內容區域視圖,即咱們的佈局文件顯示區域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;
}

5、總結

    一、setContentView方法工做流程

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

    1. 第一步:如果首次使用setContentView方法,則先建立一個DecorView對象mDecor,該對象是整個Activity窗口的根視圖;而後根據程序中選擇的Activity的Theme/Style等屬性值爲窗口添加布局屬性和相應的修飾文件,並經過findViewById方法獲取對應的根佈局文件添加到mDecor中,也就是說,第一次使用該方法時會將Activity顯示區域進行初始化;若不是第一次使用該方法,則以前已完成初始化過程並得到了mDecor和mContentParent對象,則只須要將以前添加到mContentParent區域的Views移除,空出該區域從新進行佈局便可,簡而言之,就是對mContentParent區域進行刷新;

    2. 第二步:經過inflate(加載xml文件)或addView(加載View)方法將Activity的佈局文件添加到mContentParent區域;

    3. 當setContentView設置顯示OK之後,回調Activity的onContentChanged方法,通知Activity佈局文件已經成功加載完成,接下來咱們即可以使用findViewById方法獲取佈局文件中含有id屬性的view對象了;

    注意:使用setContentView(View view)方法設置Activity的佈局時,系統會默認將該view的width和height值均設爲MATCH_PARENT,而不是使用view本身的屬性值,因此若是想經過一個View對象設置佈局,又想使用本身設置的參數值時,須要使用setContentView(View view, LayoutParams params)方法 

    二、淺談佈局文件優化技巧

    1. 從上面的分析可知,在加載xml佈局文件時,系統是經過遞歸的方式從根節點到葉子節點一步一步對控件的屬性進行解析的,因此xml文件的層次越深,效率越低,若是嵌套過多,還有可能致使棧溢出,因此在書寫佈局文件時,應儘可能對佈局文件進行優化,經過使用相對佈局等方式減小沒必要要的嵌套層次

    2. 在源碼中,能夠看到對merge標籤進行處理的過程。在某些場合下,merge標籤的使用也能夠有效減小布局文件的嵌套層次。如某些比較複雜的佈局文件,須要將佈局文件拆分開來,分爲一個根佈局文件和若干個子佈局文件,這時可能子佈局文件的根節點在添加到根佈局文件中時並無太多意義,只會增長根佈局文件的嵌套層次,這種狀況下,在子佈局文件處使用merge標籤就能夠去掉無謂的嵌套層次。不過merge標籤的使用也是有限制的,首先merge標籤只能用於一個xml文件的根節點;其次,使用inflate方法來加載一個根節點爲merge標籤的佈局文件時,須要爲該文件指定一個ViewGroup對象做爲其父元素,同時須要設置attachToRoot屬性爲true,不然會拋出異常;

    3. 利用include標籤增長佈局文件的重用性和可讀性;

相關文章
相關標籤/搜索