Android Activity的UI繪製流程之setContentView方法詳解

概述

對於Android開發人員來說,想必對setContentView方法不會陌生,每當我們創建一個Activity時,都會重寫該Activity的onCreate方法,在該方法中我們必須要調用setContentView方法來顯示我們指定的佈局或者View。那麼setContentView方法又是如何將我們指定的佈局或者View放入到指定的Activity中顯示出來的呢?

今天這篇博客主要就是講解Activity的UI繪製流程,而setContentView就是Activity的UI繪製起始過程。

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
}

這裏先貼一張圖,便於大家更好的理解這個流程
這裏寫圖片描述

PhoneWindow對象

當我們創建一個Activity時,該類都是默認繼承自Activity方法,所以首先我們需要進入到Activity方法中,然後找到該類的setContentView方法,該方法還有幾個重載的方法,這裏我們主要討論的是定義資源id參數layoutResID的方法,代碼如下:

public void setContentView(@LayoutRes int layoutResID) {
   getWindow().setContentView(layoutResID);
   initWindowDecorActionBar();
}

通過這個方法我們可以看出,在該方法中調用了 getWindow().setContentView(layoutResID),那麼這個getWindow又是什麼呢?

mWindow = new PhoneWindow(this, window);

public Window getWindow() {
    return mWindow;
}

setContentView方法

通過源碼源碼可知,getWindow方法返回的時一個Window類對象,而Window是系統定義一個抽象類,在Activity中我們實例化的是Window的一個實現類PhoneWindow。所以我們需要進入到PhoneWindow類中,查看setContentView方法具體實現。

但是通過Eclipse或者Android studio是不能直接查看到PhoneWindow的源代碼的,要想查看該類的源碼需要去sdk的源碼中查找,具體路徑爲: /sdk/sources/android-21/com/android/internal/policy/impl/PhoneWindow.java。

進入該類之後找到setContentView方法,

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

installDecor方法

通過源碼可知,當第一次初始化的時候會執行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) {
         mContentParent = generateLayout(mDecor);
     }
}
protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

在installDecor方法中首先會判斷mDecor對象是否爲空,如果爲空則會調用generateDecor方法,generateDecor方法很簡單就是生成一個DecorView對象。

在mDecor對象在執行完成之後又會判斷mContentParent對象是否爲空,如果爲空則會通過generateLayout方法生成mContentParent,大家注意這個時候mDecor是作爲參數傳入到generateLayout方法中,不用質疑在該方法中會對mDecor做一些操作,那麼或許有人會疑問這個DecorView又是什麼呢?

DecorView

DecroView,其實就是PhoneWindow對象的一個內部類,該類繼承自FrameLayout,對於DecorView,這裏先不做過多的詳細介紹,大家只需要知道DecroView其實就是作爲Activity的頂級佈局顯示出來的就可以了。

private final class DecorView extends FrameLayout   implements RootViewSurfaceTaker {
    ....
}

generateLayout方法

generateLayout方法如下:

protected ViewGroup generateLayout(DecorView decor) {
  //1,獲取<Application android:theme=""/>, <Activity/>節點指定的themes或者代碼requestWindowFeature()中指定的Features, 並設置
  TypedArray a = getWindowStyle();
  //...
  
  //2,獲取窗口Features, 設置相應的修飾佈局文件,這些xml文件位於frameworks/base/core/res/res/layout下
  int layoutResource;
  int features = getLocalFeatures();
  if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
    if (mIsFloating) {
      TypedValue res = new TypedValue();
      getContext().getTheme().resolveAttribute(com.android.internal.R.attr.dialogTitleIconsDecorLayout, res, true);
      layoutResource = res.resourceId;
    } else {
      layoutResource = com.android.internal.R.layout.screen_title_icons;
  }
  removeFeature(FEATURE_ACTION_BAR);
  } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
  layoutResource = com.android.internal.R.layout.screen_progress;
  //...
  
  mDecor.startChanging();
  //3, 將上面選定的佈局文件inflate爲View樹,添加到decorView中
  View in = mLayoutInflater.inflate(layoutResource, null);
  decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
  //將窗口修飾佈局文件中id="@android:id/content"的View賦值給mContentParent, 後續自定義的view/layout都將是其子View
  ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
  if (contentParent == null) {
    throw new RuntimeException("Window couldn't find content container view");
  }
  //...
}

在該方法中,首先會通過getWindowStyle方法獲取window的樣式閒逛屬性並對Window進行一系列的初始化,這裏大家可以看看代碼,方法開始時這裏通過一系列的判斷調用requestFeature方法,對於requestFeature方法想必大家也不陌生,在開發中我們會經常在activity中調用該方法來設置FEATURE_NO_TITLE等屬性。

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   getWindow().requestFeature(Window.FEATURE_NO_TITLE);
   setContentView(R.layout.activity_main);
}

而且該方法的調用必須是在setContentView方法之前,或許在一開始大家並不明白爲什麼要在setContentView方法之前調用requestFeature方法,但是現在應該就明白了。因爲在setContentView方法中會間接的初始化Window的屬性,這裏會調用requestFeature等方法,如果在開發中我們在setContentView方法之後調用requestFeature方法改變一些屬性值,那麼此時window的初始化已經完成,在調用requestFeature方法就沒有作用了。

接下來,在generateLayout方法中,mLayoutInflater會根據layoutResource創建一個View對象,這個View對象in會被放入到decor中,也就是之前創建的DecroView中。

而layoutResource的獲取則和之前requestFeature方法初始化有關,這裏我們分析下默認情況下layoutResource的值,也就是 layoutResource = R.layout.screen_simple;,這裏我們可以看看screen_simple的佈局,該佈局的文件位置在sdk/platforms/android-21/data/res/layout/screen_simple.xml。當然,如果大家有興趣也可以看看其他情況下如actionbar下的佈局。


<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>

在該佈局中的FrameLayout的id爲content,想必大家就不陌生了,我們通過setContentView方法指定的佈局最終就是給這個FrameLayout添加一個子佈局。

最後,對DecorView添加完view對象之後,會通過該一下代碼獲取一個ViewGroup對象,這個對象就是generateLayout方法按最終返回的ViewGroup對象。

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

最終我們在回到setContentView方法中,在該方法中我們會填充指定給activity的佈局,並且將mContentParent作爲view的跟佈局,而這個mContentParent就是通過generateLayout方法創建的。

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

總結

對於setContentView方法的分析就到此爲止,這裏我們總結一下:

  1. 當我們創建一個Activity時,會有一個PhoneWindow的對象被創建,PhoneWindow是抽象類Window的具體實現

  2. 在PhoneWindow中有一個內部類DecorView,DecroView是繼承自FrameLayout,該類是所有應用窗口的根View

  3. 在DecorView中會添加一個具體的View,該View會根據不同的theme和feature而不同,但是有一個共同點就是在該View中會有一個id爲”@android:id/content”的FrameLayout存在,該類將作爲activity中顯示指定佈局的父佈局存在,也就是activity顯示的佈局江北添加到這個FrameLayout中

最後貼上一張圖,通過hierarchyviewer工具獲取的activity的view的結構,也可以一一印證上面的結論。
這裏寫圖片描述

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