学习任何一门开发语言的经典入门课就是“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层级视图,是否清晰一些了呢?