眾所周知,Activity是Android系統的四大組件之一,扮演著界面展示的角色。作為Android開發人員,我們當然也對setContentView()方法非常熟悉,就是這么簡簡單單的一行代碼,調用它就可以加載我們寫好的xml布局。對于有追求的我們來說,必須知其然更要知其所以然。那么,本文就主要介紹一下Activity布局加載流程的源碼分析,而且源碼版本基于Android 8.0。在介紹之前,先給大家一張圖直觀地感受一下。
在開始梳理Activity整個布局加載流程之前,我們對照上圖,先來大致了解一下幾個概念:
- Window:抽象類,表示一個窗口,Android系統中的界面都是以窗口的形式存在;
- PhoneWindow: Window的具體實現類,Activity布局加載流程主要在此類中完成;
- WindowManager: Window的管理類,管理著Window的添加、更新和刪除;
- WindowManagerService(WMS):系統窗口管理服務類,具體管理著系統各種各樣的Window;
- DecorView:Window的頂級View,主要負責裝載各種View。
一、Activity中的setContentView()方法
當我們編寫一個Activity代碼時,會調用setContentView()方法來加載我們的xml布局,通常情況調用方式如下:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
... ...
}
}
那我們進入Activity的setContentView()方法,一探究竟。
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public Window getWindow() {
return mWindow;
}
Activity的setContentView()方法比較簡單,總共只有2行代碼。最主要的是它調用了getWindow()的setContentView()方法。那么,這個getWindow()方法獲取的是什么呢?在上面的代碼中我已經貼出來了,其實返回的就是一個Window對象,而且是以成員變量的形式返回。那么,我們自然而然就會想到這個成員變量mWindow是什么時候進行賦值的。在Activity啟動流程源碼解析一文中,我們通過分析知道,在Activity啟動過程中,AMS通過Binder機制,會跨進程調用到App進程中主線程ActivityThread的scheduleLaunchActivity()方法。之后,通過主線程的Handler,會調用到handleLaunchActivity()方法,緊接著在其中又會調用到performLaunchActivity()方法,關于這個方法我們具體來看一下。
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
... ...
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
} catch (Exception e) {
... ...
}
}
try {
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
if (activity != null) {
... ...
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
... ...
activity.mCalled = false;
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
... ...
if (!r.activity.mFinished) {
// 回調onStart()方法
activity.performStart();
r.stopped = false;
}
}
mActivities.put(r.token, r);
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
... ...
}
return activity;
}
由源碼可知,通過反射的方式創建了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,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
mUiThread = Thread.currentThread();
mMainThread = aThread;
mInstrumentation = instr;
mToken = token;
mIdent = ident;
mApplication = application;
mIntent = intent;
mReferrer = referrer;
mComponent = intent.getComponent();
mActivityInfo = info;
mTitle = title;
mParent = parent;
mEmbeddedID = id;
mLastNonConfigurationInstances = lastNonConfigurationInstances;
if (voiceInteractor != null) {
if (lastNonConfigurationInstances != null) {
mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
} else {
mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
Looper.myLooper());
}
}
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
mWindow.setColorMode(info.colorMode);
}
很明顯,我們可以看到之前所說的mWindow對象是在attach方法進行初始化的,并且mWindow成員變量是PhoneWindow類的實例。而且,我們從這也可以知道一個Activity對應著一個Window對象。看一眼文章開頭的那張圖,Activity的下一層為什么是PhoneWindow應該就可以理解了吧。
那么,再回到之前Activity的setContentView()方法,既然getWindow()返回的是一個PhoneWindow對象,那么getWindow().setContentView(layoutResID)自然也就是去調用PhoneWindow的setContentView()方法。
二、PhoneWindow中的setContentView()方法
經過上面的分析,我們知道調用Activity的setContentView()方法,之后會去調用PhoneWindow的setContentView()方法,那么我們一起來看一下具體實現。
// PhoneWindow # 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);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
成員變量mContentParent是一個ViewGroup對象,第一次mContentParent為空,所以會執行installDecor()方法,我們看下它的具體實現。
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
... ...
}
}
這個方法的源碼實現比較長,我們只關注重點部分。成員變量mDecor是一個DecorView對象,而DecorView繼承于FrameLayout,所以這里的mDecor其實就是一個FrameLayout對象。第一次mDecor為空,所以通過調用generateDecor()方法對它進行初始化,我們看一下具體實現。
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext().getResources());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
由源碼可知,這個方法其實就是通過new方式創建了一個DecorView對象并返回,沒什么好說的。
然后,我們再次回到installDecor()方法,創建完DecorView對象并將其賦值給mDecor。之后,便通過generateLayout()方法,并將mDecor作為入參來初始化成員變量mContentParent。
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
... ...
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
... ...
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
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(contentParent);
}
// Remaining setup -- of background and title -- that only applies
// to top-level windows.
if (getContainer() == null) {
final Drawable background;
if (mBackgroundResource != 0) {
background = getContext().getDrawable(mBackgroundResource);
} else {
background = mBackgroundDrawable;
}
mDecor.setWindowBackground(background);
final Drawable frame;
if (mFrameResource != 0) {
frame = getContext().getDrawable(mFrameResource);
} else {
frame = null;
}
mDecor.setWindowFrame(frame);
mDecor.setElevation(mElevation);
mDecor.setClipToOutline(mClipToOutline);
if (mTitle != null) {
setTitle(mTitle);
}
if (mTitleColor == 0) {
mTitleColor = mTextColor;
}
setTitleColor(mTitleColor);
}
mDecor.finishChanging();
return contentParent;
}
由源碼可知,該方法中會先根據主題設置去選擇layoutResource,這個layoutResource其實也就是DecorView的子View的布局。這里,我們挑著看一下R.layout.screen_title布局。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:fitsSystemWindows="true">
<!-- Popout bar for action modes -->
<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: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的onResourcesLoaded()方法,將layoutResource的布局轉換并添加到DecorView中。
// DecorView # onResourcesLoaded()
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
mStackId = getStackId();
if (mBackdropFrameRenderer != null) {
loadBackgroundDrawablesIfNeeded();
mBackdropFrameRenderer.onResourcesLoaded(
this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
getCurrentColor(mNavigationColorViewState));
}
mDecorCaptionView = createDecorCaptionView(inflater);
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
我們看到onResourcesLoaded()方法中通過LayoutInflater的inflate()方法解析之前的 layoutResource,并將解析之后的View添加到DecorView中。
接著,我們再回到generateLayout()方法中,在完成上面的步驟之后,就對contentParent進行初始化。
... ...
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
... ...
return contentParent;
// DecorView # findViewById()
public <T extends View> T findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
由源碼可知,最后是將id為ID_ANDROID_CONTENT(com.android.internal.R.id.content)的View賦值給了成員變量mContentParent。至此,完成了mContentParent的初始化。讓我們再回過頭來看PhoneWindow的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);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
由源碼可知,在完成對mContentParent的初始化之后,調用了mLayoutInflater.inflate(layoutResID, mContentParent)方法。很明顯,這個方法就是將我們傳遞過來的layoutId對應的xml布局文件進行解析,并且作為子View添加到mContentParent中,從而完成了把我們的xml布局文件對應的View添加到DecorView中。
至此,Activity、Window以及DecorView這三者的關系基本上理清楚了。但是,事情并沒有結束,因為這時候的DecorView 還沒有真正添加到Window上去,只是創建出對象并完成xml布局解析而已。這部分的內容屬于Activity布局繪制,打算另開篇幅進行具體展開。
總結
一個Activity包含一個Window對象,并且具體由PhoneWindow來實現。PhoneWindow將DecorView作為整個應用窗口的根View,而這個DecorView又將屏幕劃分為兩個區域一個是TitleView一個是ContentView(對應id為R.id.content),而我們所編寫的xml布局正是添加在ContentView中進行展示的。