
UI 優(yōu)化系列專(zhuān)題,來(lái)聊一聊 Android 渲染相關(guān)知識(shí),主要涉及 UI 渲染背景知識(shí)、如何優(yōu)化 UI 渲染兩部?jī)?nèi)容。
UI 優(yōu)化系列專(zhuān)題
- UI 渲染背景知識(shí)
《View 繪制流程之 setContentView() 到底做了什么?》
《View 繪制流程之 DecorView 添加至窗口的過(guò)程》
《深入 Activity 三部曲(3)View 繪制流程》
《Android 之 LayoutInflater 全面解析》
《關(guān)于渲染,你需要了解什么?》
《Android 之 Choreographer 詳細(xì)分析》
- 如何優(yōu)化 UI 渲染
《Android 之如何優(yōu)化 UI 渲染(上)》
《Android 之如何優(yōu)化 UI 渲染(下)》
setContentView() 相信大家肯定不會(huì)感到陌生,幾乎每個(gè) Activity 都會(huì)使用該方法為其添加一個(gè) xml 布局界面。但是你真的了解 setContentView 方法嗎?為什么通過(guò)它就可以展示出我們添加的 xml 布局界面呢?
關(guān)于 Activity 的 View 加載過(guò)程大家肯定聽(tīng)說(shuō)過(guò) Window、PhoneWindow、DecorView 等內(nèi)容,它們之間是什么關(guān)系?我們先通過(guò)幾個(gè)問(wèn)題來(lái)了解下。
相關(guān)問(wèn)題
- setContentView 方法到底做了什么?為什么調(diào)用后可以顯示我們?cè)O(shè)置的布局?
- PhoneWindow 是什么?它和 Window 是什么關(guān)系?
- DecorView 是干什么用的?和我們添加的布局又有什么關(guān)系?
- requestFeatrue 方法為什么要在 setContentView 方法之前?
- Layoutinflater 到底怎么把 XML 布局文件添加到 DecorView 上?
- <include> 標(biāo)簽為什么不能作為布局的根節(jié)點(diǎn)?
- <merge> 標(biāo)簽為什么要作為布局資源的根節(jié)點(diǎn)?
- inflate( int resource, ViewGroup root, boolean attachToRoot) 參數(shù) root 和 attachToRoot 的作用和規(guī)則?
- AppComatActivity 實(shí)現(xiàn)原理是怎樣的?它是如何完成布局兼容的?
如果以上問(wèn)題你都能夠熟練并正確的回答出來(lái),那么恭喜你可以直接跳過(guò)該篇文章了。
1. 從 setContentView 開(kāi)始
打開(kāi) Activity 源碼找到 setContentView 方法如下:
public void setContentView(@LayoutRes int layoutResID) {
//調(diào)用getWindow
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
getWindow 方法返回一個(gè) Window 對(duì)象:
public Window getWindow(){
return mWindow;
}
但是 Window 本質(zhì)上是一個(gè)抽象類(lèi),在 Window 的源碼中對(duì)其介紹是這樣的:
Window 是一個(gè)顯示頂層窗口的外觀,包括一些基礎(chǔ)行為的封裝(如 findViewById()、事件分發(fā) dispatch 等),而且每一個(gè) Window 實(shí)例必須添加到 WindowManager 里面,它提供了標(biāo)準(zhǔn)的 UI 策略,比如背景、標(biāo)題區(qū)域等。它的唯一實(shí)現(xiàn)類(lèi)是 PhoneWindow。
此時(shí)我們需要去跟蹤下 Window 的創(chuàng)建過(guò)程,翻閱 Activity 源碼發(fā)現(xiàn)在它的 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) {
//content實(shí)際類(lèi)型是ContextImpl
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
//可以看到Window的類(lèi)型是PhoneWindow
//創(chuàng)建當(dāng)前 Activity 的 Window 實(shí)例。
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
//... 省略
}
當(dāng)啟動(dòng)一個(gè) Activity 時(shí)首先創(chuàng)建該 Activity 實(shí)例,隨后就會(huì)調(diào)用它的 attach 方法(這部分內(nèi)容主要在 ActivityThread performLaunchActivity())。在 attach 方法中我們可以看到 Window 的實(shí)際類(lèi)型是 PhoneWindow。
也就是當(dāng)我們通過(guò) Activity 的 setContentView 實(shí)際是調(diào)用了 PhoneWindow 的 setContentView 方法:
public void setContentView(int layoutResID) {
//mContentParent是ViewGroup
//我們的setContentView設(shè)置的布局實(shí)際就是被添加到該容器中
//它是我們添加布局文件的直接根視圖
if (mContentParent == null) {
//mContentParent默認(rèn)為null
//安裝當(dāng)前DecorView
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 {
//解析布局資源,添加到mContentParent
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
//回調(diào)Activity的onContentChanged方法
cb.onContentChanged();
}
//表示已經(jīng)設(shè)置過(guò)布局
//如果此時(shí)使用requestFeature則會(huì)拋出異常
mContentParentExplicitlySet = true;
}
mContentParent 是一個(gè) ViewGroup,它的實(shí)際類(lèi)型是 FrameLayout,實(shí)際我們通過(guò) setContentView 設(shè)置的 View 就被添加到該容器。也就是它是我們布局文件的直接父視圖(下面會(huì)分析到)。
方法最后 mContentParentExplicitlySet,在 setContentView 方法執(zhí)行完畢后置為 true,表示當(dāng)前窗口已經(jīng)設(shè)置完成。后面我們會(huì)分析道,如果此后在調(diào)用 requestFeature 方法設(shè)置 Window 窗口的 Feature 將會(huì)拋出異常。
mContentParent 默認(rèn)為 null,此時(shí)執(zhí)行 installDecor 方法,為當(dāng)前 Window 創(chuàng)建 DecorView 視圖,DecorView 是我們整個(gè) Activity 的最頂級(jí)視圖,它的實(shí)際類(lèi)型是 FrameLayout:
private void installDecor() {
mForceDecorInstall = false;
//mDecor是DecorView,繼承自FrameLayout
if (mDecor == null) {
//為當(dāng)前Window創(chuàng)建DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
//mContentParent默認(rèn)為null
if (mContentParent == null) {
//找到當(dāng)前主題布局中,內(nèi)容的父容器
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
//... 省略
}
每個(gè) Window 有且僅有一個(gè) DecorView,DecorView 用來(lái)描述窗口的視圖,看下它的創(chuàng)建過(guò)程 generateDecor 方法如下:
protected DecorView generateDecor(int featureId) {
Context context;
//mUseDecorContext構(gòu)造方法中默認(rèn)置為true
//表示使用DecorContext山下文
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
//使用DecorContext
context = new DecorContext(applicationContext, getContext().getResources());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
//使用應(yīng)用程序上下文
context = getContext();
}
//直接創(chuàng)建DecorView
return new DecorView(context, featureId, this, getAttributes());
}
方法的最后可以看到直接創(chuàng)建 DecorView 并返回(DecorView 繼承自 FrameLayout)?;氐?installDecor 方法,看下 setContentView 方法的直接父容器 mContentParent 的創(chuàng)建過(guò)程 generateLayout 方法(注意這時(shí)候 mContentParent 與 DecorView 還沒(méi)有任何關(guān)聯(lián)):
//我們給Window設(shè)置的相關(guān)屬性就是在generateLayout時(shí)加進(jìn)來(lái)的
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//獲取當(dāng)前Window的Style
//這個(gè)是不是很熟悉,
TypedArray a = getWindowStyle();
//Window是否是Floating
//浮窗類(lèi)型時(shí) Dialog 就是Floating 類(lèi)型
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
//是否需要標(biāo)題欄
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
//對(duì)Feature狀態(tài)為進(jìn)行設(shè)置
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);
}
if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
requestFeature(FEATURE_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
requestFeature(FEATURE_ACTION_MODE_OVERLAY);
}
if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) {
requestFeature(FEATURE_SWIPE_TO_DISMISS);
}
if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,
false)) {
setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS
& (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowTranslucentNavigation,
false)) {
setFlags(FLAG_TRANSLUCENT_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION
& (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowOverscan, false)) {
setFlags(FLAG_LAYOUT_IN_OVERSCAN, FLAG_LAYOUT_IN_OVERSCAN & (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER & (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowEnableSplitTouch,
getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB)) {
setFlags(FLAG_SPLIT_TOUCH, FLAG_SPLIT_TOUCH & (~getForcedWindowFlags()));
}
a.getValue(R.styleable.Window_windowMinWidthMajor, mMinWidthMajor);
a.getValue(R.styleable.Window_windowMinWidthMinor, mMinWidthMinor);
if (a.hasValue(R.styleable.Window_windowFixedWidthMajor)) {
if (mFixedWidthMajor == null) mFixedWidthMajor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedWidthMajor,
mFixedWidthMajor);
}
if (a.hasValue(R.styleable.Window_windowFixedWidthMinor)) {
if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedWidthMinor,
mFixedWidthMinor);
}
if (a.hasValue(R.styleable.Window_windowFixedHeightMajor)) {
if (mFixedHeightMajor == null) mFixedHeightMajor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedHeightMajor,
mFixedHeightMajor);
}
if (a.hasValue(R.styleable.Window_windowFixedHeightMinor)) {
if (mFixedHeightMinor == null) mFixedHeightMinor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedHeightMinor,
mFixedHeightMinor);
}
if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) {
requestFeature(FEATURE_CONTENT_TRANSITIONS);
}
if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) {
requestFeature(FEATURE_ACTIVITY_TRANSITIONS);
}
//這個(gè)地方是不是很熟悉,Window是否是透明的
mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);
/**以上都是對(duì)Window一些狀態(tài)進(jìn)行設(shè)置*/
//requestFeature為什么要在setContentView之前?
final Context context = getContext();
final int targetSdk = context.getApplicationInfo().targetSdkVersion;
final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB;
final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP;
final boolean targetHcNeedsOptions = context.getResources().getBoolean(
R.bool.target_honeycomb_needs_options_menu);
final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE);
if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) {
setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_TRUE);
} else {
setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_FALSE);
}
if (!mForcedStatusBarColor) {
mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000);
}
if (!mForcedNavigationBarColor) {
mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000);
}
WindowManager.LayoutParams params = getAttributes();
// Non-floating windows on high end devices must put up decor beneath the system bars and
// therefore must know about visibility changes of those.
if (!mIsFloating && ActivityManager.isHighEndGfx()) {
if (!targetPreL && a.getBoolean(
R.styleable.Window_windowDrawsSystemBarBackgrounds,
false)) {
setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS & ~getForcedWindowFlags());
}
if (mDecor.mForceWindowDrawsStatusBarBackground) {
params.privateFlags |= PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND;
}
}
if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) {
decor.setSystemUiVisibility(
decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB) {
if (a.getBoolean(
R.styleable.Window_windowCloseOnTouchOutside,
false)) {
setCloseOnTouchOutsideIfNotSet(true);
}
}
if (!hasSoftInputMode()) {
params.softInputMode = a.getInt(
R.styleable.Window_windowSoftInputMode,
params.softInputMode);
}
if (a.getBoolean(R.styleable.Window_backgroundDimEnabled,
mIsFloating)) {
/* All dialogs should have the window dimmed */
if ((getForcedWindowFlags() & WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {
params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
}
if (!haveDimAmount()) {
params.dimAmount = a.getFloat(
android.R.styleable.Window_backgroundDimAmount, 0.5f);
}
}
if (params.windowAnimations == 0) {
params.windowAnimations = a.getResourceId(
R.styleable.Window_windowAnimationStyle, 0);
}
// The rest are only done if this window is not embedded; otherwise,
// the values are inherited from our container.
if (getContainer() == null) {
if (mBackgroundDrawable == null) {
if (mBackgroundResource == 0) {
mBackgroundResource = a.getResourceId(
R.styleable.Window_windowBackground, 0);
}
if (mFrameResource == 0) {
mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0);
}
mBackgroundFallbackResource = a.getResourceId(
R.styleable.Window_windowBackgroundFallback, 0);
}
if (mLoadElevation) {
mElevation = a.getDimension(R.styleable.Window_windowElevation, 0);
}
mClipToOutline = a.getBoolean(R.styleable.Window_windowClipToOutline, false);
mTextColor = a.getColor(R.styleable.Window_textColor, Color.TRANSPARENT);
}
// Inflate the window decor.
/**生成對(duì)應(yīng)的Window Decor 要根據(jù)當(dāng)前設(shè)置的Features屬性
* 加載不同的 DecorView 的xml布局*/
int layoutResource;
//獲取當(dāng)前Window的Features
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 {
// 這是最簡(jiǎn)單的一個(gè),看下 DecorView 要加載布局文件是怎樣的?
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
mDecor.startChanging();
//將DecorView的xml文件添加到DecorView
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//在DecorView對(duì)應(yīng)的布局中,查找id為content的FrameLayout,該容器便是我們布局直接父容器
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);
}
//... 省略
mDecor.finishChanging();
//返回DecorView對(duì)應(yīng)布局中的id為content的FrameLayout
//它實(shí)際上就是我們setContentView的直接根視圖
return contentParent;
}
generateLayout 方法雖然較長(zhǎng),但是工作內(nèi)容并不復(fù)雜,我們首先看 getWindowStyle 方法:
public final TypedArray getWindowStyle() {
synchronized (this) {
if (mWindowStyle == null) {
//styleable是不是很熟悉,在一些自定義控件時(shí)經(jīng)常用到
mWindowStyle = mContext.obtainStyledAttributes(
com.android.internal.R.styleable.Window);
}
return mWindowStyle;
}
}
styleable 是不是很熟悉,在一些自定義控件時(shí)經(jīng)常會(huì)用到。實(shí)際上我們給 Window 設(shè)置的相關(guān)屬性就是在 generateLayout 方法進(jìn)行設(shè)置的,例如非常熟悉和經(jīng)常使用到的 :
//Window是否是Floating狀態(tài)
//Dialog時(shí)Window就是Floating
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
//Window是否包含標(biāo)題欄
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
//Window是否是透明的
mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);
然后它們都會(huì)調(diào)用 requestFeature 方法對(duì)當(dāng)前 Window 的 Feature 狀態(tài)位進(jìn)行設(shè)置。
public boolean requestFeature(int featureId) {
if (mContentParentExplicitlySet) {
//在setContentView方法最后會(huì)將該標(biāo)志位置為true,如果
//在setCotnentView方法后再執(zhí)行requestFeature將會(huì)拋出異常。
throw new AndroidRuntimeException("requestFeature() must be called before adding content");
}
final int features = getFeatures();
final int newFeatures = features | (1 << featureId);
if ((newFeatures & (1 << FEATURE_CUSTOM_TITLE)) != 0 &&
(newFeatures & ~CUSTOM_TITLE_COMPATIBLE_FEATURES) != 0) {
//不能既有自定義標(biāo)題欄,又有其他標(biāo)題欄
throw new AndroidRuntimeException(
"You cannot combine custom titles with other title features");
}
if ((features & (1 << FEATURE_NO_TITLE)) != 0 && featureId == FEATURE_ACTION_BAR) {
return false; // Ignore. No title dominates.
}
if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_NO_TITLE) {
//沒(méi)有標(biāo)題欄
removeFeature(FEATURE_ACTION_BAR);
}
//... 省略
}
注意方法中 if (mContentParentExplicitlySet) 如果滿足則直接拋出異常。該標(biāo)志位在上面也有分析到, setContentView 方法最后會(huì)將其置為 true。即 requestFeature 方法必須在 setContentView 方法之前。那為什么要在 setContentView 方法之前呢?下面分析到。
要根據(jù)當(dāng)前 Feature 加載不同的 DecorView 的 XML 布局文件。注意查看源碼中 generateLayout 方法的下半部分,我們以最簡(jiǎn)單的 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>
可以看到布局本身就是一個(gè) LinearLayout,包含上下兩部:標(biāo)題欄 ViewStub 區(qū)域,內(nèi)容區(qū)域 id 為 content 的 FrameLayout(這就是 Window 中的 mContentParent,即 setContentView 的直接父容器)。
然后將DecorView 對(duì)應(yīng)的 xml 布局文件添加到 DecorView 中:
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
DecorView 的 onResourcesLoaded 方法如下:
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
mStackId = getStackId();
//判斷當(dāng)前DecorView是否包含DecorCaptionView
mDecorCaptionView = createDecorCaptionView(inflater);
//通過(guò)LayoutInflater完成xml布局加載
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
//DecorCaptionView也是DecorView子視圖
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
//此時(shí)直接添加到DecorCaptionView中
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// 添加到DecorView中
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
也就是說(shuō),Window 會(huì)根據(jù)當(dāng)前設(shè)置的 Feature 為 DecorView 添加一個(gè)對(duì)應(yīng)的 xml 布局文件,該布局文件主要?jiǎng)澐稚舷聝刹糠郑渲邪粋€(gè) id 為 content 的 FrameLayout,它會(huì)賦值給 PhoneWindow 中的 mContentParent,表示 setContentView 的的父容器。
在 DecorView 中找到對(duì)應(yīng)的 mContentParent(就是 id 為 content 的 FrameLayout):
ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
在 generateLayout 方法最后返回該 contentParent,此時(shí)賦值給 PhoneWidow 的成員 mContentParent。
重新回到 PhoneWindow 的 setContentView 方法。將我們這是的布局文件添加 contentParent 過(guò)程如下:
// 解析布局資源,添加到mContentParent
mLayoutInflater.inflate(layoutResID, mContentParent);
至此,我們可以回答文章開(kāi)頭的第一個(gè)問(wèn)題了,先通過(guò)一張圖了解下 Activity 加載 UI - 類(lèi)圖關(guān)系和視圖結(jié)構(gòu)。

每個(gè) Activity 都有一個(gè)關(guān)聯(lián)的 Window 對(duì)象(該對(duì)象實(shí)際類(lèi)型為 PhoneWindow,PhoneWindow 為 Window 的唯一實(shí)現(xiàn)類(lèi))用來(lái)描述應(yīng)用程序窗口。
每個(gè)窗口內(nèi)部又包含了一個(gè) DecorView 對(duì)象,DecorView 繼承自 FrameLayout;DecorView 用來(lái)描述窗口的視圖 — xml 布局(我們通過(guò) setContentView 方法設(shè)置的 xml 布局最終被添加到該 DecorView 對(duì)應(yīng) xml 布局中 id 為 content 的 FrameLayout 中,下面分析到)。
另外 requestFeature 必須要在 setContentView 方法之前,因?yàn)橐鶕?jù)該 Feature 為 DecorView 添加一個(gè)對(duì)應(yīng)的 xml 布局文件;該布局包含上下兩部分,標(biāo)題欄和內(nèi)容區(qū)域(id 為 content 的 FrameLayout)。
2. LayoutInfalter 解析過(guò)程
需要回到 setContentView 方法,看下我們?cè)O(shè)置的布局是如何添加到 mContentParent 中:
//layoutResID通過(guò)setContentView設(shè)置的布局資源id
//mContentParent就是id為content的FrameLayout
mLayoutInflater.inflate(layoutResID, mContentParent);
關(guān)于 LayoutInflater 大家肯定不會(huì)感到陌生,它可以將我們傳入的 xml 布局文件解析成對(duì)應(yīng)的 View 對(duì)象。
/**
* resource 表示當(dāng)前布局資源id
* root 表示布局的父容器,可以為null
* attachToRoot 是否將布局資源添加到父容器root上
* */
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource);
try {
//重點(diǎn)看下inflate方法
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
首先通過(guò) Resources 獲取一個(gè) XML 資源解析器,我們重點(diǎn)關(guān)注 inflate 方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
//獲取在XML設(shè)置的屬性
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
//注意root容器在這里,在我們當(dāng)前分析中該root就是mContentParent
View result = root;
try {
// 查找xml布局的根節(jié)點(diǎn)
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
//找到起始根節(jié)點(diǎn)
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
//獲取到節(jié)點(diǎn)名稱(chēng)
final String name = parser.getName();
//判斷是否是merge標(biāo)簽
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
//此時(shí)如果ViewGroup==null,與attachToRoot==false將會(huì)拋出異常
//merge必須添加到ViewGroup中,這也是merge為什么要作為布局的根節(jié)點(diǎn),它要添加到上層容器中
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 否則創(chuàng)建該節(jié)點(diǎn)View對(duì)象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
//如果contentParent不為null,在分析setContentView中,這里不為null
if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
//解析Child
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
//添加到ViewGroup
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
//此時(shí)布局根節(jié)點(diǎn)為temp
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw i.e
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw i.e
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
while 循環(huán)部分,找到 xml 布局文件的根節(jié)點(diǎn),如果 if (type != XmlPullParser.START_TAG) 未找到根節(jié)點(diǎn)直接拋異常了。否則獲取到該節(jié)點(diǎn)名稱(chēng),判斷如果是 merge 標(biāo)簽,此時(shí)需要注意參數(shù) root 和 attachToRoot,root 必須不為null,并且 attachToRoot 必須為 true,即 merge 內(nèi)容必須要添加到 root 容器中。
如果不是 merge 標(biāo)簽,此時(shí)根據(jù)標(biāo)簽名 name 直接創(chuàng)建該 View 對(duì)象,rInflate 和 rInflateChildren 都是去解析子 View,rInflateChildren 方法實(shí)際也是調(diào)用到了 rInflate 方法:
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
//還是調(diào)用rInflate方法
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
區(qū)別在于最后一個(gè)參數(shù) finishInflate,它的作用是標(biāo)志當(dāng)前 ViewGroup 樹(shù)創(chuàng)建完成后回調(diào)其 onFinishInflate 方法。
如果根標(biāo)簽是 merge 此時(shí) finishInflate 為 false,這也很容易理解,此時(shí)的父容器為 inflate 中傳入的 ViewGroup,它是不需要再次回調(diào) onFinishInflate() 的。該過(guò)程如下:
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
//獲取到節(jié)點(diǎn)名稱(chēng)
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
//include標(biāo)簽
if (parser.getDepth() == 0) {
//include如果為根節(jié)點(diǎn)則拋出異常了
//include不能作為布局文件的根節(jié)點(diǎn)
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
//如果此時(shí)包含merge標(biāo)簽,此時(shí)也會(huì)拋出異常
//merge只能作為布局文件的根節(jié)點(diǎn)
throw new InflateException("<merge /> must be the root element");
} else {
//創(chuàng)建該節(jié)點(diǎn)的View對(duì)象
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
//添加到父容器
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
//回調(diào)ViewGroup的onFinishInflate方法
parent.onFinishInflate();
}
}
while 循環(huán)部分,parser.next() 獲取下一個(gè)節(jié)點(diǎn),如果獲取到節(jié)點(diǎn)名為 include,此時(shí) parse.getDepth() == 0 表示根節(jié)點(diǎn),直接拋出異常,即 <include /> 不能作為布局的根節(jié)點(diǎn)。
如果此時(shí)獲取到節(jié)點(diǎn)名稱(chēng)為 merge,也是直接拋出異常了,即 <merge /> 只能作為布局的根節(jié)點(diǎn)。
否則創(chuàng)建該節(jié)點(diǎn)對(duì)應(yīng) View 對(duì)象,rInflateChildren 遞歸完成以上步驟。并將解析到的 View 添加到其直接父容器 viewGroup.addView()。
注意方法的最后通知調(diào)用每個(gè) ViewGroup 的 onFinishInflate(),大家是否有注意到這其實(shí)是入棧的操作,即最頂層的 ViewGroup 最后回調(diào) onFinishInflate()。
至此,我們可以回答文章開(kāi)頭提出的第二個(gè)問(wèn)題了,再來(lái)通過(guò)一張流程圖熟悉下整個(gè)解析過(guò)程:

在 inflater 解析布局資源過(guò)程中,首先找到布局的根節(jié)點(diǎn) START_TAG,如果未找到直接拋出異常。否則獲取到當(dāng)前節(jié)點(diǎn)的名稱(chēng)。
如果節(jié)點(diǎn)名稱(chēng)為 merge ,會(huì)判斷 inflate 方法參數(shù) if ( root(ViewGroup)!= null && attachToot == true ),表示布局文件要直接添加到 root 中,否則拋出異常(<merge /> can be used only with a valid ViewGroup root and attachToRoot=true);
繼續(xù)解析子節(jié)點(diǎn)的過(guò)程中如果再次解析到 merge 標(biāo)簽,則直接拋出異常,<merge /> 標(biāo)簽必須作為布局的根節(jié)點(diǎn)(<merge /> must be the root element)。
如果解析到節(jié)點(diǎn)名稱(chēng)為 include,會(huì)判斷當(dāng)前節(jié)點(diǎn)深度是否為 0,0 表示當(dāng)前處于根節(jié)點(diǎn),此時(shí)直接拋出異常,即 <include /> 不能作為布局文件的根節(jié)點(diǎn)(<include /> cannot be the root element)。
3. 偷梁換柱之為兼容而生的 AppCompatActivity
在 Android Level 21 之后,Android 引入了 Material Design 的設(shè)計(jì),為了支持 Material Color、調(diào)色版、Toolbar 等各種新特性,AppCompatActivity 就應(yīng)用而生。Google 考慮到仍然有很大部分低于 5.0 版本的設(shè)備,所有將 AppCompatActivity 放在了 support v7 包內(nèi)。
接下來(lái)我們就看下 AppCompatActivity 是如何實(shí)現(xiàn) UI 兼容設(shè)計(jì)的。
//AppCompatActivity的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
與 Activity 有所不同,AppCompatActivity 的 setContentView 方法中首先調(diào)用 getDelegate 方法得到一個(gè)代理對(duì)象。
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
//創(chuàng)建AppCompatDelegate對(duì)象
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
創(chuàng)建當(dāng)前 AppCompatDelegate 過(guò)程如下:
//AppCompatDelegate的create方法
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
根據(jù)當(dāng)前系統(tǒng)版本創(chuàng)建對(duì)應(yīng)的 AppCompatDelegate 對(duì)象,AppCompatActivity 其實(shí)就是通過(guò)引入 AppCompatDelegate 來(lái)解決兼容問(wèn)題。
這里需要說(shuō)明的是,各 Delegate 實(shí)際根據(jù)版本由高到低繼承關(guān)系,即 AppCompatDelegateImplN extends AppCompatDelegateImplV23 extends AppCompatDelegateImplV14 extends AppCompatDelegateImplV11 extends AppCompatDelegateImplV9。
setContentView 實(shí)際調(diào)用到 AppCompatDelegate 的第一個(gè)實(shí)現(xiàn)類(lèi) AppCompatDelegateImplV9 中:
public void setContentView(int resId) {
//創(chuàng)建一個(gè)SubDecor
ensureSubDecor();
//獲取SubDecor中content區(qū)域,此時(shí)setContentView的直接父容器為SubDecor中id為content的FrameLayout
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//將布局添加到SubDecor的Content區(qū)域
LayoutInflater.from(mContext).inflate(resId, contentParent);
//回調(diào)到Activity的onContentChanged,
mOriginalWindowCallback.onContentChanged();
}
ensureSubDecor 方法是要?jiǎng)?chuàng)建一個(gè) SubDecor,SubDecor 實(shí)際與 PhoneWindow 中的 DecorView 類(lèi)似,它的出現(xiàn)就是為了兼容布局。
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
//創(chuàng)建SubDecor,
mSubDecor = createSubDecor();
//是否設(shè)置了標(biāo)題
CharSequence title = getTitle();
if (!TextUtils.isEmpty(title)) {
onTitleChanged(title);
}
applyFixedSizeWindow();
onSubDecorInstalled(mSubDecor);
//表示當(dāng)前Window已經(jīng)安裝SubDecor
mSubDecorInstalled = true;
PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
if (!isDestroyed() && (st == null || st.menu == null)) {
invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
}
}
}
這里主要看下 SubDecor 的創(chuàng)建過(guò)程 createSubDecor 方法,該方法過(guò)程與 PhoneWindow 創(chuàng)建 DecorView 類(lèi)似,區(qū)別是 Delegate 中找的都是 AppCompat 的屬性,也就是做的兼容相關(guān)的事情。
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
a.recycle();
throw new IllegalStateException(
"You need to use a Theme.AppCompat theme (or descendant) with this activity.");
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
//調(diào)用到Window中requestFeature
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
}
mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
a.recycle();
/**
* 以上根據(jù)style設(shè)置Window的Feature
* createSubDecor 方法的上半部分
*/
//確保Window中已經(jīng)安裝DecorView
mWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
//根據(jù)style加載SubDecor的xml布局
if (!mWindowNoTitle) {
//不需要標(biāo)題欄類(lèi)型窗口
if (mIsFloating) {
//Floating 類(lèi)型窗口
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
// Floating windows can never have an action bar, reset the flags
mHasActionBar = mOverlayActionBar = false;
} else if (mHasActionBar) {
//含有 ActionBar
TypedValue outValue = new TypedValue();
mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);
Context themedContext;
if (outValue.resourceId != 0) {
themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
} else {
themedContext = mContext;
}
//解析SubDecor對(duì)應(yīng)的xml布局文件
subDecor = (ViewGroup) LayoutInflater.from(themedContext)
.inflate(R.layout.abc_screen_toolbar, null);
//同樣包含一個(gè)id為content
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(R.id.decor_content_parent);
mDecorContentParent.setWindowCallback(getWindowCallback());
/**
* Propagate features to DecorContentParent
*/
if (mOverlayActionBar) {
mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
}
if (mFeatureProgress) {
mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
}
if (mFeatureIndeterminateProgress) {
mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
}
}
} else {
if (mOverlayActionMode) {
//解析DecorView對(duì)應(yīng)xml布局文件
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_screen_simple_overlay_action_mode, null);
} else {
//解析DecorView對(duì)應(yīng)xml布局文件
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
}
//... 省略
}
if (subDecor == null) {
throw new IllegalArgumentException(
"AppCompat does not support the current theme features: { "
+ "windowActionBar: " + mHasActionBar
+ ", windowActionBarOverlay: "+ mOverlayActionBar
+ ", android:windowIsFloating: " + mIsFloating
+ ", windowActionModeOverlay: " + mOverlayActionMode
+ ", windowNoTitle: " + mWindowNoTitle
+ " }");
}
if (mDecorContentParent == null) {
mTitleView = (TextView) subDecor.findViewById(R.id.title);
}
/**
* 以下為 createSubDecor 方法的下半部分
* 偷梁換柱過(guò)程,將 SubDecor 對(duì)應(yīng)布局中 content 替換原 DecorView 中 id 為 content 的 FrameLayout。
*/
// Make the decor optionally fit system windows, like the window's decor
ViewUtils.makeOptionalFitsSystemWindows(subDecor);
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
// There might be Views already added to the Window's content view so we need to
// migrate them to our content view
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
//這里很關(guān)鍵,將原來(lái)Window中id為content的FrameLayout(mContentParent)設(shè)置為NO_ID
windowContentView.setId(View.NO_ID);
//最新創(chuàng)建的SubDecor中內(nèi)容區(qū)域FramLayout的id設(shè)置為content
//偷梁換柱
contentView.setId(android.R.id.content);
// The decorContent may have a foreground drawable set (windowContentOverlay).
// Remove this as we handle it ourselves
if (windowContentView instanceof FrameLayout) {
((FrameLayout) windowContentView).setForeground(null);
}
}
// 將新創(chuàng)建的SubDecor添加到DecorView的內(nèi)容區(qū)域(mContentParent容器)
mWindow.setContentView(subDecor);
//... 省略
return subDecor;
}
可以看到方法的上半部分,獲取一系列 AppCompat 兼容屬性,設(shè)置 Window 的 Feature 屬性;然后方法的中間部分,注意 mWindow.getDecorView() 作用是創(chuàng)建當(dāng)前 Window 的 DecorView 整個(gè)過(guò)程(文章上面已經(jīng)做了分析);然后根據(jù) Feature 加載 SubDecor 對(duì)應(yīng)的 xml 布局文件,這里我們以最簡(jiǎn)單的 abc_screen_simple.xml 布局文件為例:
<android.support.v7.widget.FitWindowsLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/action_bar_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
<android.support.v7.widget.ViewStubCompat
android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/abc_action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
//這里包含一個(gè)FrameLayout
<include layout="@layout/abc_screen_content_include" />
</android.support.v7.widget.FitWindowsLinearLayout>
abc_screen_simple.xml 布局文件中 include 一個(gè) abc_screen_content_include.xml 布局文件,如下:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<android.support.v7.widget.ContentFrameLayout
android:id="@id/action_bar_activity_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</merge>
ContentFrameLayout 繼承自 FrameLayout,注意該 FrameLayout 最后會(huì)替代原 PhoneWindow 中 DecorView 對(duì)應(yīng)布局內(nèi) id 為 content 的 FrameLayout。這一過(guò)程也是 AppCompatActivity 中偷梁換柱的核心內(nèi)容,一起來(lái)看下這個(gè)重要過(guò)程,注意查看 createSubDecor 方法的下半部分:
獲取到當(dāng)前 SubDecor 中 content 容器,也就是上面 include 布局內(nèi) ContentFrameLayout。
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
然后拿到 PhoneWindow 中 DecorView 內(nèi) content 容器(mContentParent),注意這個(gè)原本是我們 setContentView() 的直接父容器。
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
重要的偷梁換柱過(guò)程:
// 這里很關(guān)鍵,將原來(lái)Window中id為content的FrameLayout(mContentParent)設(shè)置為NO_ID
windowContentView.setId(View.NO_ID);
// 最新創(chuàng)建的SubDecor中內(nèi)容區(qū)域ContentFramLayout的id設(shè)置為content
// 偷梁換柱
contentView.setId(android.R.id.content);
即將 DecorView 中原 content 容器 id 置為 NO_ID,將 SubDecor 中 content 容器 id 置為 content。經(jīng)過(guò)前面的分析我們知道通過(guò) setContentView 添加布局最終會(huì)被添加到一個(gè) id 為 content 的 FrameLayout,此時(shí)該 content 實(shí)際變?yōu)?SubDecor 中 ContentFrameLayout容器。偷梁換柱完成。
然后將 SubDecor 添加到 DecorView 中,此時(shí)原 DecorView 中 content 容器實(shí)際添加的是 SubDecor。
// 將新創(chuàng)建的SubDecor添加到DecorView的內(nèi)容區(qū)域(mContentParent容器)
mWindow.setContentView(subDecor);
最后我們重新回到 Delegate 的 setContentView 方法,看下我們?cè)O(shè)置的布局如何添加到 SubDecor 中的 content (ContentFrameLayout)容器的:
// 將布局添加到SubDecor的Content區(qū)域
LayoutInflater.from(mContext).inflate(resId, contentParent)
通過(guò)一張結(jié)構(gòu)圖了解下 AppCompatActivity 整個(gè) UI 視圖關(guān)系,注意與前面分析的 Activity 做下比較,最主要的差別在 DecorView 的 content 容器。此時(shí)已經(jīng)替換成了 SubDecor。

至此,我們可以回答文章開(kāi)頭的第三個(gè)問(wèn)題了。
AppCompatActivity 通過(guò)引入 AppCompatDelegate 來(lái)兼容不同版本的 Material Design 支持。
在 AppCompatDelegate 中做了一個(gè)巧妙的偷梁換柱操作,即在原 DecorView 的 content 區(qū)域添加一個(gè) SubDecor(兼容布局),我們通過(guò) setContentView 設(shè)置的布局最終被添加到該 SubDecor 的 content 容器中,這樣完成布局兼容操作。
其實(shí)我們可以看出 Google 工程師在處理兼容時(shí)也很“暴力”,這也是沒(méi)有辦法的辦法,因?yàn)橹巴诘目犹嗔耍@么多版本,為了做兼容費(fèi)了很多心思,有些心思設(shè)計(jì)的也非常巧妙。
總結(jié)
每個(gè) Activity 都有一個(gè)關(guān)聯(lián)的 Window 對(duì)象,用來(lái)描述應(yīng)用程序窗口,每個(gè)窗口內(nèi)部又包含一個(gè) DecorView 對(duì)象,DecorView 對(duì)象用來(lái)描述窗口的視圖 — xml 布局。
AppCompatActivity 在 DecorView 中又添加了一個(gè) SubDecor 視圖 — xml 布局,解決布局兼容性問(wèn)題。
以上便是個(gè)人在學(xué)習(xí) View 的加載過(guò)程心得和體會(huì),文中如有不妥或有更好的分析結(jié)果,歡迎大家指出。
文章如果對(duì)你有幫助,請(qǐng)留個(gè)贊吧!