學習任何一門開發語言的經典入門課就是“Hello World”,Android雖然是以java爲基礎,但是也不能僅僅是在控制欄輸出"Hello World"這麼簡單就行了,我們總得在手機上跑起來,讓界面展示"Hello World"纔行,那麼我們要怎樣做呢?很簡單,新建項目這些就不用說了,新建一個佈局,添加一個android:text = "Hello World"
的TextView,通過Activity在onCreate方法中setContentView(R.layout.xxx)即可。雖然想要展示一個界面這麼簡單,但是背後的原理和流程也是需要知道的。
該篇文章源碼基於 android-28
先放一張網上關於Activity視圖層級的圖,看完這篇文章再回過頭來看這張圖:
通過setContentView,我們就給Activity設置了佈局,佈局中寫的任何View都能展示在手機上,那我們就從這個方法開始跟蹤:
//Activity.java
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
該方法通過getWindow()的返回值進行setContentView:
//Activity.java
public Window getWindow() {
return mWindow;
}
返回的是一個Window對象:
/**
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {
//。。。。省略類中的代碼,主要是看註釋
}
Window是一個抽象類,英文中註釋的意思是存在唯一的一個實現類是 android.view.PhoneWindow
,我們接着看PhoneWindow的setContentView方法:
//PhoneWindow.java
//把mContentParent的聲明放過來方便源碼閱讀
ViewGroup mContentParent;
@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
肯定是null,就會走第一個if判斷執行 installDecor()
方法:
//PhoneWindow.java
//聲明放過來方便源碼閱讀
private DecorView mDecor; //繼承自FrameLayout
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
//。。。省略代碼
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
//。。。省略代碼,省略的代碼是設置圖標、背景、title以及Acitivty動畫的,對我們該篇文章的分析沒有用
}
}
有兩行代碼關係到我們的流程,一個是 mDecor = generateDecor(-1);
,另一個是 mContentParent = generateLayout(mDecor);
,我們先看 generateDecor(-1)
方法,(返回值通過上面的聲明我們可以知道是一個DecorView,它是一個繼承自FrameLayout的ViewGroup):
// PhoneWindow.java
protected DecorView generateDecor(int featureId) {
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
這個方法最主要就是new了一個DecorView返回過去,把這個返回過去的DecorView又傳到了generateLayout(mDecor)
方法:
// PhoneWindow.java
protected ViewGroup generateLayout(DecorView decor) {
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);
}
//。。。省略部分代碼
int layoutResource;//注意這個layoutResource
int features = getLocalFeatures();
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);
} //。。。省略大量else if代碼
else{//直接到最後的else分支
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//注意這個方法
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
//。。。省略部分代碼
return contentParent;
我們在這段長代碼中可以看到很多熟悉的東西,比如第一行的 TypedArray a = getWindowStyle();
,是不是想起了我們自定義View的自定義屬性?再比如後面獲取屬性的if else 分支 R.styleable.Window_windowNoTitle
、R.styleable.Window_windowActionBar
,是否想到了我們在清單文件中給Application、Activity節點添加的style中寫的屬性?最後到了一個關鍵的局部變量 layoutResource
,通過各種style屬性的判斷給它賦值了各種layout文件,通過查看這些佈局文件你會發現一個特點,它們都有一個id爲 @android:id/content
的FrameLayout,記住這一點,特別是這個id: @android:id/content
layoutResource
被賦值之後調用了DecorView的 onResourcesLoaded
方法,傳進去了兩個參數:mLayoutInflater
、layoutResource
,第一個參數我們都知道是LayoutInflater,用於inflate佈局的,我們看這個方法:
//DecorView.java
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
//。。。省略部分代碼
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();
}
該方法會inflate一個view,這個view就是我們在PhoneWindow中經過大量判斷(是否有title、actionBar)後拿到的佈局文件id,返回的root被直接addView到DecorView自身了(前面可以知道它是一個ViewGroup)。
看到這裏我們可能會有疑惑了,現在我知道activity下面有PhoneWindow,PhoneWindow下有DecorView了,那我在Activity中setContentView傳入的佈局文件呢?我們回過頭看 installDecor
方法中,mContentParent = generateLayout(mDecor);
的返回值賦值給了 mContentParent
(也記住這一點),而這個返回值在 generateLayout
方法中是通過 findViewById得到的:
//將這個常量的聲明放到這裏,方便閱讀
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
我們可以看到,這個常量就是R.id.content,而那個 layoutResource
的每一個賦值的佈局文件中都有定義了這個id的FrameLayout,最後我們再回到最初的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
就是那個id爲 @android:id/content
的FrameLayout,而 mLayoutInflater.inflate(layoutResID, mContentParent)
這一行代碼就把我們在Activity中setContentView的佈局文件inflate到了這個FrameLayout中了。現在再去看那張Activity層級視圖,是否清晰一些了呢?