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的繪製入口原始碼分析