Android最重要的东西是四大组件,相信大家初学Android时都是从四大组件开始学起的。其中Activity是最先接触到的,也是用到最多的,因为它太重要了,它的职责是显示与交互,显示的重任就交给了布局文件Layout。相信大部分初学者会对xml布局文件是如何加载到Activity里成为界面视图感到好奇,甚至一些同学都没有想过这个问题,他们会说:setContentView(layoutResId), so easy! 然而忽略了这一句代码背后的故事。当我们静下来去学习他们背后的过程与原理后,你会发现过程很复杂,原理很简单,收获很丰富,感觉很棒滴!今天,我们就从最简单且用到最多的开始学习—布局文件加载之谜。
加载布局文件有两种方式:一、setContentView(layoutResId)。二、首先获得一个LayoutInflater对象inflate,然后inflate.inflate(layoutResId)即可。这两种方式用的都很多,下面我们就以setContentView为着手点开始分析。
Step1:进入Activity源码查看setContentView方法。
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
}
public Window getWindow() {
//Window对象,本质上是一个PhoneWindow对象
return mWindow;
}
其中Window是一个抽象类,mWindow是一个PhoneWindow对象,它是通过mWindow = PolicyManager.makeNewWindow(this);创建出来的。PhoneWindow是Android中的最基本的窗口系统,是Activity和整个View系统交互的接口。
Step2: 继续跟踪PhoneWindow类的setContentView方法。
public void setContentView(int layoutResID) {
//第一次调用, 则mContentParent为null,且mDecor也为null。
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null) {
cb.onContentChanged();
}
}
首先判断mContentParent是否存在,如果是第一次调用setContentView, 那么mContentParent不存在,则调用installDecor方法创建mDecor和mContentParent对象。接着我们看到mLayoutInflater.inflate(layoutResID,mContentParent);这么一行,mLayoutInflater就是一个LayoutInflater对象,这说明我们上面讲的两种加载资源文件的方式最终都归为第二种,即将布局文件通过LayoutInflater对象转换为View树,并添加到mContentParent视图中,最后交给WindowManagerService显示。题外话,从这段代码的判断逻辑可以看出,我们可以多次调用setContentView来改变我们的界面。
Step3:为了更好的了解mContentParent这个对象,我们跟踪installDecor方法。
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
...
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
}
首先判断mDecor如果为空,则调用generateDecor()创建一个DecorView(该类是FrameLayout子类,即一个ViewGroup视图)。如果mContentParent为空,则生成一个mContentParent对象,mContentParent对象也是一个FrameLayout视图,其过程是通过窗口的风格和属性选择一个系统的布局文件,通过mLayoutInflater.inflate加载该系统布局文件,然后将id为content的FrameLayout赋值给mContentParent,这里我们还是逃不过加载布局文件。Activity视图的层级关系如下图:(此图片为引用)
由于generateDecor和generateLayout不是这次的主题,在这里就不继续深究了,我们回到Step2中mLayoutInflater.inflate(layoutResID,mContentParent);这段代码,这才是加载布局文件的开始。
Step4:进入LayoutInflater类跟踪inflate方法。
public View inflate(int resource, ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
if (DEBUG) System.out.println("INFLATING from resource: " + resource);
XmlResourceParser parser = getContext().getResources().getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
在函数中传入resource获取到该资源文件的解析器XmlResourceParser,使用解析器parser对象来解析xml布局文件。XmlResourceParser是专门解析xml文件的解析器,小巧易用,解析速度快,读取到xml的声明返回START_DOCUMENT;读取到xml的结束返回END_DOCUMENT; 读取到xml的开始标签返回START_TAG;读取到xml的结束标签返回END_TAG;读取到xml的文本返回 TEXT。在这里不做深入的研究,有机会做一个专门的介绍。
Step5:继续进入inflate(parser, root, attachToRoot)方法。
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final AttributeSet attrs = Xml.asAttributeSet(parser);
...
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName(); //节点名
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, attrs, false);
} else {
// Temp is the root view that was found in the xml
View temp;
if (TAG_1995.equals(name)) {
temp = new BlinkLayout(mContext, attrs);
} else {
temp = createViewFromTag(root, name, attrs);
}
ViewGroup.LayoutParams params = 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);
}
}
// Inflate all children under temp
rInflate(parser, temp, attrs, true);
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
//...
} finally {
...
}
return result;
}
}
这个方法代码比较多,但不复杂,注释也很充分,较容易理解。首先解析布局type并校验是否合法,获取布局的根节点名创建根视图,接着判断传入进来的root视图,如果不为null,则为该根视图赋值外面父视图的布局参数。然后调用rInflate函数为根视图添加所有子节点视图。
Step6:rInflate方法递归布局文件的根视图的所有子节点,将解析到的View构成视图树。
void rInflate(XmlPullParser parser, View parent, final 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();
//处理<requestFocus />, <include />, <merge />, <blink />标签的情况
....
// View节点
else {
//根据节点名构建一个View实例对象
final View view = createViewFromTag(parent, name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
//调用generateLayoutParams()方法返回一个LayoutParams实例对象.
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//̐继续递归
rInflate(parser, view, attrs, true);
//将该View以特定LayoutParams值添加至父View
viewGroup.addView(view, params);
}
}
if (finishInflate) parent.onFinishInflate();
}
这段代码也很容易理解,首先校验type是否合法,然后根据节点名称处理各种标签的情况,如果不是标签,则根据名称创建一个视图,以此视图为起点继续递归构建视图树。其中finalView view = createViewFromTag(parent, name, attrs),由节点名等参数构建一个view实例对象, 在此方法中查找name是否包含符号点,如果没找到则说明不含有包名,就使用默认的包名android.view.
最终会进入
createView来构建一个View对象。
Step7: 进入最终如何生成View对象的方法createView
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
// ͨmContext.getClassLoader()4ӔخameĀ΄
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
// ͨɤõԬʽ
constructor = clazz.getConstructor(mConstructorSignature);
sConstructorMap.put(name, constructor);
...
Object[] args = mConstructorArgs;
args[1] = attrs;
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// always use ourselves when inflating ViewStub later
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(this);
}
return view;
} ...
}
这里很明显,通过mContext.getClassLoader()来加载name的类文件,然后利用反射机制实例化一个View对象,这个对象就是我们在界面上看到的一个个元素。至此,setContentView和inflate方法这两种方式加载布局文件,直到最后生成视图树的过程就完成了。视图树生成好了之后就是交给WindowManagerService来管理及显示了。
针对上面复杂的过程,我们做一个简单的总结。布局文件是如何被显示成为视图的呢?很简单,首先PhoneWindow会生成DecorView和mContentParent,这是布局文件的显示区域。接着根据布局文件id生成一个xml文件解析器XmlResourceParser,利用此解析器来递归文件里的每个节点,根据节点名来判断是各种标签还是视图名称。然后根据名称来加载相应的类,利用反射机制生成实例,将此实例加入到父视图中,这样便形成了视图树。最后将视图树加入到mContentParent中,这样Activity的整个视图便组织完成了。