1. 程式人生 > >View的繪製流程之一:setContentView()方法原始碼分析

View的繪製流程之一:setContentView()方法原始碼分析

一、知識儲備

由 Activity 的啟動流程,我們知道 Activity 的啟動順序如下:

--> 棧頂的Activity的onPause() 
--> Instrumentation的newActivity() /*建立Activity*/
--> 待啟動Activity的attach()
--> 待啟動Activity的onCreate()
--> 待啟動Activity的onResume()
--> ActivityThread.handleResumeActivity() /*將DecorView新增到Window*/

也就是說當我們啟動一個 Activity 後會先呼叫這個 Activity的 onCreate()方法,在這個方法中會呼叫 setContentView()方法,setContentView()方法會將 Activity的 Layout佈局檔案載入到 DecorView中,然後 DecorView會在 Activity啟動流程中的一個 handleResumeActivity()方法被載入 Window中,繼而在成功載入到 Window後會開始繪製 Activity的 Layout 佈局檔案,那麼我們現在先分析 setContentView()方法到底完成了什麼工作,以 API23的原始碼分析


二、原始碼分析

Step 1:

我們直接看到 Activity的 setContentView()方法中
★ Activity # setContentView()

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

★ Activity # getWindow()

public Window getWindow() {
    return
mWindow; }

 
getWindow() 方法會獲取 Window抽象類的實現類 PhoneWindow,initWindowDecorActionBar() 方法用於初始化標題欄;那麼 mWindow 在什麼時候初始化了???

當 Activity 通過反射被創建出來後就會呼叫 Activity 的 attach() 方法,而在這個方法中會初始化 mWindow ,我們看到這個方法
★ Activity # attach()

final void attach(Context context, ActivityThread aThread,
                  Instrumentation instr, IBinder token, int ident,
                  Application application, Intent intent, ActivityInfo info,
                  CharSequence title, Activity parent
, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor) { attachBaseContext(context); mFragments.attachHost(null /*parent*/); mWindow = new PhoneWindow(this); mWindow.setCallback(this); .... if (info.uiOptions != 0) { mWindow.setUiOptions(info.uiOptions); } mUiThread = Thread.currentThread(); mMainThread = aThread; ..... }

也就是說在 Activity建立的時候 mWindow 就會初始化,這個 mWindow 實現將 Activity 的佈局新增到 DecorView 的功能

Step 2:

我們接著看到 PhoneWindow 的 setContentView() 方法中
★ PhoneWindow # setContentView()

@Override
public void setContentView(int layoutResID) {
    // mContentParent其實是DecorView佈局中的一個FrameLayout,這個FrameLayout用來擺放Activity的Layout佈局
    if (mContentParent == null) {
        // 這個方法會建立DecorView,同時初始化mContentParent
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        ....
    } else {
        // 載入 Activity的佈局檔案到 mContentParent
        // mContentParent就是系統的com.android.internal.R.id.content 這個View
        // layoutResID:Activity的 Layout佈局id
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    ....
}

首先,判斷是否安裝了 DecorView,如果還沒有就呼叫 installDecor() 方法安裝 DecorView
接著,將 Activity 的 Layout 佈局檔案載入到 DecorView 中

mContentParent 是 DecorView 的佈局檔案,用來存放 Activity 的佈局的;所以說現在重點就是這個 installDecor() 方法了;

結構圖

Step 3:

我們現在看到 installDecor() 方法
★ PhoneWindow # installDecor()

private void installDecor() {
    if (mDecor == null) {
        // 初始化DecorView
        mDecor = generateDecor();
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    }
    // 獲取存放Activity的Layout佈局的一個FrameLayout
    if (mContentParent == null) {
        // generateLayout()方法會通過 findViewById(ID_ANDROID_CONTENT)獲取到DecorView佈局中的
        // 一個FrameLayout,可以存放 Activity的Layout佈局檔案
        mContentParent = generateLayout(mDecor);

        ....    
    }
}

首先,因為是第一次進到這個方法,所以 mDecor 肯定還沒有初始化,所以會先呼叫 generateDecor() 方法初始化 DecorView
然後,呼叫 generateLayout() 方法初始化 mContentParent 佈局

我們看到 PhoneWindow 類的 generateDecor() 方法
★ PhoneWindow # generateDecor()

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

★ PhoneWindow.DecorView # 構造方法

public DecorView(Context context, int featureId) {
    super(context);
    mFeatureId = featureId;
    mShowInterpolator = AnimationUtils.loadInterpolator(context,
            android.R.interpolator.linear_out_slow_in);
    mHideInterpolator = AnimationUtils.loadInterpolator(context,
            android.R.interpolator.fast_out_linear_in);
    mBarEnterExitDuration = context.getResources().getInteger(
            R.integer.dock_enter_exit_duration);
}

 
擴充套件:瞭解 DecorView
我們來了解一下 DecorView,在這個之前,我們先分清楚 mDecor 和 mContentParent

  • mDecor:這是一個 DecorView 物件,這個 DecorView 繼承自 FrameLayout
  • mContentRoot:這個是 DecorView的佈局檔案 View,相當於 Activity的佈局檔案
  • mContentParent:這個 DecorView的佈局 View 中的一個 FrameLayout,有點像 Activity 佈局檔案中最外層那個 LinearLayout 、RelativeLayout等

DecorView是 PhoneWindow的一個內部類,用於存放 Activity 的 Layout 佈局,也就是說一個 Activity會對應一個 DecorView,同時這個 DecorView會在呼叫 setContentView() 時初始化
★ PhoneWindow.DecorView # 類結構

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {

DecorView 中有幾個比較重要的方法:

dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()
onMeasure()
onDraw()
draw()
onLayout()
onAttachedToWindow()
onDetachedFromWindow()

Step 4:

接下來我們分析一下 DecorView 的 佈局是如何初始化的
★ PhoneWindow # generateLayout()

protected ViewGroup generateLayout(DecorView decor) {
    // 1、根據當前的主題應用一些資料
    TypedArray a = getWindowStyle();
    ....
    // 2、根據設定的主題樣式選擇不同的 DecorView佈局,一般來說都是用 R.layout.screen_simple這個佈局
    int layoutResource;
    int features = getLocalFeatures();
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        layoutResource = R.layout.screen_swipe_dismiss;
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        ....
    } .... {
    } else {
        layoutResource = R.layout.screen_simple;
    }
    mDecor.startChanging();
    // 3、載入 DecorView的 Layout佈局檔案
    View in = mLayoutInflater.inflate(layoutResource, null);
    // decor就是呼叫 generateLayout()方法傳進來的 DecorView物件
    // 將載入成功的佈局新增到 DecorView中
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    // mContentRoot就是 DecorView的佈局檔案
    mContentRoot = (ViewGroup) in;
    // 4、獲取 DecorView中存放 Activity的佈局FrameLayout
    ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
    ....
    // 最後返回這個ViewGroup
    return contentParent;
}

初始化 DecorView 的過程如下:
1. 根據當前設定的主題樣式應用一些資料
2. 根據設定的主題樣式選擇不同的 DecorView 佈局,一般來說都是用 R.layout.screen_simple 這個佈局
3. 載入 DecorView 佈局檔案,並儲存到 mContentRoot
4. 呼叫 findViewById()方法獲取 DecorView佈局檔案的一個 FrameLayout,然後返回

我們看一下 DecorView 常用的佈局檔案,也就是 R.layout.screen_simple
★ R.layout.screen_simple

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

可見,Activity 的佈局檔案會被新增到這個 FrameLayout 中
 

現在,我們回過頭來看(Step 2)中的一段程式碼
★ PhoneWindow # setContentView()

@Override
public void setContentView(int layoutResID) {
    // mContentParent其實是DecorView佈局中的一個FrameLayout,這個FrameLayout用來擺放Activity的Layout佈局
    if (mContentParent == null) {
        // 這個方法會建立DecorView,同時初始化mContentParent
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        ....
    } else {
        // 載入 Activity的佈局檔案到 mContentParent
        // mContentParent就是系統的com.android.internal.R.id.content 這個View
        // layoutResID:Activity的 Layout佈局id
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    ....
}

當安裝了 DecorView,並初始了 mContentParent後,就會將 Activity 的 佈局檔案載入到 mContentParent 中,也就是載入到 DecorView 的佈局檔案中的 FrameLayout中

三、總結

  • 當 Activity 被通過反射創建出來後就會呼叫 Activity 的 attach() 方法,而在這個方法中會初始化 mWindow ,也就是建立我們的 PhoneWindow

  • setContentView() 主要是初始化 PhoneWindow中的 DecorView,然後將初始化好的 DecorView儲存在 PhoneWindow的 mDecor屬性中(注意:這個 DecorView其實還沒有和我們的 Activity 聯絡起來),最後將 Activity 的佈局檔案載入到 DecorView 的佈局檔案中的一個 FrameLayout 中

四、延伸知識點

問題:現在我們已經將 Activity 的 Layout 佈局檔案新增到了 DecorView 的 FrameLayout 中,但是這個 Activity 的 Layout 佈局什麼時候繪製???
:在 Activity 啟動流程中的 ActivityThread.handleResumeActivity() 方法中會通過呼叫 Activity 的 getWindwo() 方法獲取到 PhoneWindow,然後再呼叫 PhoneWindow 的 的 getDecorView() 方法獲取到 DecorView ,然後會將這個 DecorView 新增到遠端的 WindowManagerService 中,當成功新增後就會呼叫方法對 DecorView 的佈局進行繪製,具體的細節可以看下面的文章
View的繪製流程之二:View的繪製入口原始碼分析