【本文出自大圣代的技術(shù)專欄 http://blog.csdn.net/qq_23191031】
【轉(zhuǎn)載煩請注明出處,尊重他人勞動成果就是對您自己的尊重】

不喜歡看文字的可以直接到文字尾,看圖說話。
1, 前言
- 在前面《【Android 控件架構(gòu)】詳解Android控件架構(gòu)與常用坐標系》的文章中我們提到了
setContentView()方法,當時只是匆匆?guī)н^,并沒有闡明具體流程。而這篇文章就是從Activity中的setContentView()方法出發(fā)結(jié)合上篇的視圖框架,詳細分析setContentView()的工作原理。還是貼一張圖復習一下吧。

- 從上面的文章中我們知道
setContentView()方法是用來設置ContentView布局地,當系統(tǒng)調(diào)用了setContentView()方法所有的控件就得到了顯示,但是你有想過Android系統(tǒng)是如何讓xml文件加載到界面并顯示出來的呢?setContentView()中具體是如何實現(xiàn)的呢?就讓我們在這些疑問來進入下面的探討吧。
2 從setContentView說起(基于Api 25 Android 7.1.1)
本來是想基于Api 26來看的,可是后來才想起來 Android 8.0的源碼還沒發(fā)布。。。
2-1 Activity源碼中的setContentView
經(jīng)過閱讀Android的源碼發(fā)現(xiàn),系統(tǒng)為我們提供了三個setContentView()的重載方法,他們都調(diào)用了getWindow()中的setContentView()方法。
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
那么 getWindow()方法有事做什么的呢,咱們繼續(xù)往下看。
2-2 關(guān)于窗口Window類的一些關(guān)系
getWindow()的作用
/**
* Retrieve the current {@link android.view.Window} for the activity.
* This can be used to directly access parts of the Window API that
* are not available through Activity/Screen.
*
* @return Window The current window, or null if the activity is not
* visual.
*/
// 如果返回為null表示,則表示當前Activity不在窗口上
public Window getWindow() {
return mWindow;
}
...
mWindow = new PhoneWindow(this, window);
通過源碼我們可以看到getWindow()方法返回的就是PhoneWindow的實例對象(PhoneWindow是抽象類Window的唯一實現(xiàn)類 PhoneWindow在線源碼地址)
public class PhoneWindow extends Window implements MenuBuilder.Callback {
private final static String TAG = "PhoneWindow";
...
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;
private ViewGroup mContentRoot;
...
}
而在PhoneWindow中我們看到了作為成員變量的 mDecor,(在Android 7.1.1中DecorView已經(jīng)不再是PhoneWindow的內(nèi)部類了,而且包都換了,有圖有真相)。


查看DecorView之后發(fā)現(xiàn)public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks,看見沒有,DecorView才是Activity的根布局(root view),他繼承了 FrameLayout負責Activity視圖的加載,而DecorView本身則是由PhoneWindow加載的。PhoneWindow是如何加載DecorView的呢,咱們帶著問題繼續(xù)往下看
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
private static final String TAG = "DecorView";
private static final boolean DEBUG_MEASURE = false;
private static final boolean SWEEP_OPEN_MENU = false;
// The height of a window which has focus in DIP.
private final static int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
// The height of a window which has not in DIP.
private final static int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
....
}
一言不可就上圖:


2-3 PhoneWindow中的setContentView方法
在Window類中setContentView方法是抽象的,所以我們直接去看PhonWindow類中關(guān)于 setContentView方法的實現(xiàn)過程
@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) {
//創(chuàng)建DecorView,并添加到mContentParent上
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)通知表示完成界面加載
cb.onContentChanged();
}
}
源碼中的第一步就是驗證mContentParent是否為 null,如果為null則表示程序是第一次運行,執(zhí)行installDecor。如果不為null則會判斷當前是否設置了FEATURE_CONTENT_TRANSITIONS(這個屬性表示內(nèi)容加載時需不需要過場動畫,默認為false)。如果沒有使用過場動畫則移除mContentParent中的所有view(所以說 setContentView方法可以多次調(diào)用,因為他會移除掉所有的控件);
如果在初始化mContentParent之后,用戶設置了啟用轉(zhuǎn)場動畫則使用Scene開啟過度,否則mLayoutInflater.inflate(layoutResID, mContentParent);將我們的資源文件通過LayoutInflater對象轉(zhuǎn)化為控件樹添加到mContentParent中。
再來看下PhoneWindow類的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法源碼,如下:
422 @Override
423 public void setContentView(View view) {
424 setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
425 }
@Override
428 public void setContentView(View view, ViewGroup.LayoutParams params) {
429 // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
430 // decor, when theme attributes and the like are crystalized. Do not check the feature
431 // before this happens.
432 if (mContentParent == null) {
433 installDecor();
434 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
435 mContentParent.removeAllViews();
436 }
437
438 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
439 view.setLayoutParams(params);
440 final Scene newScene = new Scene(mContentParent, view);
441 transitionTo(newScene);
442 } else {
443 mContentParent.addView(view, params);
444 }
445 mContentParent.requestApplyInsets();
446 final Callback cb = getCallback();
447 if (cb != null && !isDestroyed()) {
448 cb.onContentChanged();
449 }
450 mContentParentExplicitlySet = true;
451 }
看見沒有,我們其實只用分析setContentView(View view, ViewGroup.LayoutParams params)方法即可,如果你在Activity中調(diào)運setContentView(View view)方法,實質(zhì)也是調(diào)運setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams設置為了MATCH_PARENT而已。
所以直接分析setContentView(View view, ViewGroup.LayoutParams params)方法就行,可以看見該方法與setContentView(int layoutResID)類似,只是少了LayoutInflater將xml文件解析裝換為View而已,這里直接使用View的addView方法追加道了當前mContentParent而已。
2-4 installDecor()方法 源碼分析
2614 private void installDecor() {
2615 mForceDecorInstall = false;
2616 if (mDecor == null) {
2617 mDecor = generateDecor(-1);
2618 mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
2619 mDecor.setIsRootNamespace(true);
2620 if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
2621 mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
2622 }
2623 } else {
2624 mDecor.setWindow(this);
2625 }
2626 if (mContentParent == null) {
//根據(jù)窗口的風格修飾,選擇對應的修飾布局文件,并且將id為content的FrameLayout賦值給mContentParent
2627 mContentParent = generateLayout(mDecor);
//......
2674 } else {
2675 mTitleView = (TextView) findViewById(R.id.title);
2676 if (mTitleView != null) {
//根據(jù)FEATURE_NO_TITLE隱藏,或者設置mTitleView的值
2677 if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
2678 final View titleContainer = findViewById(R.id.title_container);
2679 if (titleContainer != null) {
2680 titleContainer.setVisibility(View.GONE);
2681 } else {
2682 mTitleView.setVisibility(View.GONE);
2683 }
2684 mContentParent.setForeground(null);
2685 } else {
2686 mTitleView.setText(mTitle);
2687 }
2688 }
2689 }
我在源碼中發(fā)現(xiàn)了一個很重要的東西,請看第2677行?。?!,這就在最根本上解釋了:為什么要在setContentView()方法之前設置requestWindowFeature(Window.FEATURE_NO_TITLE)才能不顯示TitleActionBar部分,達到全屏的效果。
言歸正傳,installDecor()方法一進來就判斷mDcor是否為空,為空怎么辦創(chuàng)建一個嘍,咦generateDecor(-1)傳一個 -1 是什么鬼???代碼規(guī)范呢!Google也可以這么寫代碼么??......咳咳。
2263 protected DecorView generateDecor(int featureId) {
//......
2281 return new DecorView(context, featureId, this, getAttributes());
2282 }
ps:怎么又一大堆,看來7.1.1的源碼和5.1.1的差異真是不小啊。啥,Androdi5.1.1里面的長啥樣?
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
不看不知道,一看嚇一跳??匆姏]有,一共兩行。這里就不展開討論了.....
2-5 generateLayout()方法 源碼分析
在源碼 2626行,我們看到當 mContentParent == null的時候使用generateLayout(mDecor)方法創(chuàng)建一個mContentParent出來。generateLayout(mDecor)看名字好像倒是像用來設置layout的。
2284 protected ViewGroup generateLayout(DecorView decor) {
2285 // Apply data from current theme.
//首先通過WindowStyle中設置的各種屬性,對Window進行requestFeature或者setFlags
2287 TypedArray a = getWindowStyle();
2288
//...
2299 mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
2300 int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
2301 & (~getForcedWindowFlags());
2302 if (mIsFloating) {
2303 setLayout(WRAP_CONTENT, WRAP_CONTENT);
2304 setFlags(0, flagsToUpdate);
2305 } else {
2306 setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
2307 }
2309 if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
2310 requestFeature(FEATURE_NO_TITLE);
2311 } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
2312 // Don't allow an action bar if there is no title.
2313 requestFeature(FEATURE_ACTION_BAR);
2314 }
//....
//...根據(jù)當前sdk的版本確定是否需要menukey
2413 WindowManager.LayoutParams params = getAttributes();
2491 // Inflate the window decor.
2492
2493 int layoutResource;
2494 int features = getLocalFeatures();
//......
//根據(jù)設定好的features值選擇不同的窗口修飾布局文件,得到layoutResource值
//把選中的窗口修飾布局文件添加到DecorView對象里,并且指定contentParent值
2495 // System.out.println("Features: 0x" + Integer.toHexString(features));
2496 if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
2497 layoutResource = R.layout.screen_swipe_dismiss;
2498 } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
2499 if (mIsFloating) {
2500 TypedValue res = new TypedValue();
2501 getContext().getTheme().resolveAttribute(
2502 R.attr.dialogTitleIconsDecorLayout, res, true);
2503 layoutResource = res.resourceId;
2504 } else {
2505 layoutResource = R.layout.screen_title_icons;
2506 }
2507 // XXX Remove this once action bar supports these features.
2508 removeFeature(FEATURE_ACTION_BAR);
2509 // System.out.println("Title Icons!");
2510 } else if {
//......
2552
2553 mDecor.startChanging(); //通知 開始改變
2554 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
2555
2556 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
//......
2604 mDecor.finishChanging();//通知 改變完成
2605
2606 return contentParent;
2607 }
}
從整體角度來講這個方法就是根據(jù)用戶設置的風格、標簽為窗口選擇不同的主布局文件,DecorView做為根視圖將該窗口根布局添加進去,然后獲取id為content的FrameLayout返回給mContentParent對象。所以installDecor方法實質(zhì)就是產(chǎn)生mDecor和mContentParent對象。 哎!我怎么沒看見DecorView添加布局的代碼呢?別急下邊就告訴你怎么回事。
在進入這個方法時,系統(tǒng)就會調(diào)用getWindowStyle() 在當前的Window的theme中獲取我們的Window屬性,對我們的Window設置各種requestFeature,setFlags等等。
getWindowStyle()為抽象類Window提供的方法,具體源碼如下:
665 public final TypedArray getWindowStyle() {
666 synchronized (this) {
667 if (mWindowStyle == null) {
668 mWindowStyle = mContext.obtainStyledAttributes(
669 com.android.internal.R.styleable.Window);
670 }
671 return mWindowStyle;
672 }
673 }
我們順藤摸瓜找到屬性位置 源碼地址
<!-- The set of attributes that describe a Windows's theme. -->
<declare-styleable name="Window">
<attr name="windowBackground" />
<attr name="windowContentOverlay" />
<attr name="windowFrame" />
<attr name="windowNoTitle" />
<attr name="windowFullscreen" />
<attr name="windowOverscan" />
<attr name="windowIsFloating" />
<attr name="windowIsTranslucent" />
<attr name="windowShowWallpaper" />
<attr name="windowAnimationStyle" />
<attr name="windowSoftInputMode" />
<attr name="windowDisablePreview" />
<attr name="windowNoDisplay" />
<attr name="textColor" />
<attr name="backgroundDimEnabled" />
<attr name="backgroundDimAmount" />
所以這里就是解析我們?yōu)锳ctivit設置theme的地方,至于theme一般可以在AndroidManifest.xml文件中設置。


接下來就到關(guān)鍵的部分了,2494-2510行:通過對features和mIsFloating的判斷,獲取不同的主布局文件為layoutResource進行賦值,值可以為R.layout.screen_custom_title;R.layout.screen_action_bar;等等。
經(jīng)過上面的源碼我們可以看到設置features,除了theme中設置的,我們還可以在代碼中進行:
//通過java文件設置:
requestWindowFeature(Window.FEATURE_NO_TITLE);
//通過xml文件設置:
android:theme="@android:style/Theme.NoTitleBar"
其實我們平時requestWindowFeature()設置的features值就是在這里通過getLocalFeature()獲取的;而android:theme屬性也是通過這里的getWindowStyle()獲取的。兩方式具體流程不同,但是效果是一樣的。
所以這下你應該就明白在java文件設置Activity的屬性時必須在setContentView方法之前調(diào)用requestFeature()方法的原因了吧。
我靠,我還是沒看見DecorView添加布局的代碼啊 ,這就來:
源碼 2554行,進行了如下操作:
2554 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
看名字是在進行資源文件的加載,具體是怎么操作的呢:
1801 void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
//......
1813 final View root = inflater.inflate(layoutResource, null);
//......
1824 addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
//......
1826 mContentRoot = (ViewGroup) root;
//......
1828 }
在源碼1824行,系統(tǒng)將 layoutResource 所代表的主布局文件。添加到 DecorView 中,而在源碼中第 2556行我們可以看到,系統(tǒng)又在DecorView中需找一個ID_ANDROID_CONTENT布局賦值給contentParent。
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
而 ID_ANDROID_CONTENT又是個什么東西呢?我在Windows抽象類中找到了它的源碼。注釋說的很明確,每一個主布局都擁有id為content的控件。通過mContentRoot = (ViewGroup) root;我們可以清楚的知道,layoutResource既為整個窗口的根布局。
/**
* The ID that the main layout in the XML layout file should have.
*/
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
隨手貼幾個布局文件加以證明:
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>
R.layout.screen_simple_overlay_action_mode
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<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" />
<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>
同樣在Windows抽象類中找到了findViewByID方法的源碼,findViewByID的作用就是將在DecoreView中需找 id為content的FragmentLayout賦值給 contentParent
1252 /**
1253 * Finds a view that was identified by the id attribute from the XML that
1254 * was processed in {@link android.app.Activity#onCreate}. This will
1255 * implicitly call {@link #getDecorView} for you, with all of the
1256 * associated side-effects.
1257 *
1258 * @return The view if found or null otherwise.
1259 */
1260 @Nullable
1261 public View findViewById(@IdRes int id) {
1262 return getDecorView().findViewById(id);
1263 }
最后generateLayout()的最后系統(tǒng)還會調(diào)用Callback接口的成員函數(shù)onContentChanged來通知對應的Activity組件視圖內(nèi)容發(fā)生了變化。至此Android setContentView()方法分析完成。
3,總結(jié)
圖片被縮小了不清楚,不要緊。請右鍵 - 在新標簽中打開圖片。

由此就組成了我們在《【Android 控件架構(gòu)】詳解Android控件架構(gòu)與常用坐標系》一篇中提到的視圖框架(圖中contentView就是源碼中的contentParent)

4,參考:
如果說我比別人看得更遠些,那是因為我站在了巨人的肩上