在我们Activity中,我们要加载页面布局文件,通过setContentView()
方法就能将我们用XML编写的布局文件载入。今天我们通过源代码来对这个过程进行分析。
看一段简单代码:
public class Main2Activity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
}
}
我们进入setContentView()
方法,可以看到:
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
只有两行,由于涉及到layoutResID
,我们继续去看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.
*/
public Window getWindow() {
return mWindow;
}
其实也就是返回了一个Window
对象,那么Window
类又是一个什么类呢?
我们去了解下Window
类:
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {
/** Flag for the "options panel" feature. This is enabled by default. */
public static final int FEATURE_OPTIONS_PANEL = 0;
/** Flag for the "no title" feature, turning off the title at the top
* of the screen. */
public static final int FEATURE_NO_TITLE = 1;
……
}
截取了部分代码,我们可以看到其实Window
是一个抽象类,接着我们去看下关于这个类的描述,其中有一句话特别重要,就是这个Window
类只有唯一的一个实现类PhoneWindow
,好了,对于:
getWindow().setContentView(layoutResID);
我们就直接去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
是什么?
// 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.
ViewGroup mContentParent;
看下对这个字段的注释,注释上说这是放置窗口内容的视图,它要么是mDecor
本身,要么是是内容所去mDecor
的孩子。其实说一千道一万,只是说了mContentParent
是我们放置内容的父布局。那么我们的mContentParent
是什么时候初始化的呢?带着这个问题,我们去寻找,最后我们可以发现:
private void installDecor() {
mContentParent = generateLayout(mDecor);
}
可以看到它的初始化是在installDecor()
方法中,而我们的installDecor()
又是在当mContentParent == null
时才会被调用,所以这里的mContentParent
是空的。
既然是空的,那我们接下来就是要去installDecor()
这个方法。
方法很长,首先来了解下mDecor
。其实就是DecorView
,而DecorView
是FrameLayout
的一个子类。
函数有点长,看重点:
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);
...
}
省略了部分代码,在mContentParent == null
之前都是初始化eDecor
,由于刚刚我们说过mContentParent
就是空,所以接下来,我们要去看看这个generateLayout(mDecor)
方法。
走了好半天,终于走到了关键部分了,真的是不容易。走过去以后,发现这个函数不是一般的长,怎么办?
看重点!!!
我们往下略着看,看到有个地方有点意思:
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);
}
看到没,这里就是控制版本在11之前和11之后是否显示菜单按钮的控制部分。看,读源码的过程还会有意外的收获。
嗯,我们接着往下看。
发现了一段注释
// Inflate the window decor.
嗯,看到这句我们就知道重点要来了。
看完了一大推的if-else语句之后,终于看到了一句:
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
对呀,我们没有设置任何关于Feature的属性,所以也很好理解为啥一下子就来到了这里。
既然有布局可以看,那么我们就看看上面的布局是个什么。
<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>
很简单有木有?
往下看,其实我们可以发现,mDecor
是在
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
将上面的xml
布局inflate
出来,然后添加到我们的mDecor
上的。
然后通过mDecor
的findViewById()
,找到上面xml
中的FrameLayout
,然后在最后返回。
终于把这个过程屡清楚了。其实就是inflate
一个xml
,然后将里面的FrameLayout
返回出来。
所以我们的mContentParent
,最后的引用就是刚刚那个FrameLayout
。
一个installDecor()
找的我们真是肝肠寸断。
最终我们还是回到了这里:
@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.removeAllViews()
,再接着就是mLayoutInflater.inflate(layoutResID, mContentParent)
,进去看看吧,这个方法不止一点重要。
进去后发现这TM不就是用了XmlPullParser
来解析xml的么?
我们看到其中一部分源码:
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
}
这里就是如果merge
标签没有parent
,就直接抛异常,然后调用:
rInflate(parser, root, inflaterContext, attrs, false);
看看这个函数的实现:
/**
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
*
* Note: Default visibility so the BridgeInflater can
* override it.
*/
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException(" cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException(" must be the root element");
} else {
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 (finishInflate) {
parent.onFinishInflate();
}
}
不短,但也不长,代码很简单,看看就懂了,我们只需要关注一句:
rInflateChildren(parser, view, attrs, true);
返回到函数调用的地方,我们在看看else里面,直接通过
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
获取到了根节点,最后的最后也走到了:
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
看函数意思,其实我们很容易理解,这个函数就是去inflate子布局的。
进入里面一看,我擦,最后又去了上面的rInflate
,不用说咯,这就是递归,而且还是以深度优先的递归,厉害了。
最后我们的mContentParent
上就有了布局啦,真的不容易啊,给自己一杯奶茶,庆祝下吧。