概述
Android 开发的过程中,我们肯定会使用到布局加载,无论是Activity、Fragment的创建,还是 ListView 适配器中 convertView 视图的加载,最常用的方式都是将一个布局文件加载成一系列的 View 对象,然后才会继续进行相应的操作;
这时我们就会想,Android 系统是如何将我们的布局文件加载成一系列的View对象、加载布局文件时传入的其它参数有什么作用;
布局文件的加载方式
我们先来看下Activity中加载视图的方法:
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initActionBar();
}
可以看出Activity的 setContentView()
方法实际上调用的是 Window 类里面的方法,而 window 类是一个抽象类且只有一个实现类 PhoneWindow (这里不清楚的可以网上找下资料),所以我们直接来看 PhoneWindow 里面的具体实现方法:
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
我们可以看到,我们传进来的布局文件最终是交给了 LayoutInflater
对象来加载,同时传入了一个 mContentParent 对象,从上面的代码我们可以分析出,mContentParent 对象是一个 ViewGroup 类型的对象;
接下来我们看一下另外一种常见的视图加载方式,通过 View 的静态方法 inflate()
加载:
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
LayoutInflater factory = LayoutInflater.from(context);
return factory.inflate(resource, root);
}
很明显此方法也是通过 LayoutInflater
对象来加载布局文件;
LayoutInflater 源码解析
根据上面的解析,我们知道,不管是 Activity 中的 setContentView()
还是通过View.inflate()
,最终都是调用了 LayoutInflater.inflate()
来加载布局文件;
我们直接来看inflate()
主要几个重载方法的源码:
public View inflate(XmlPullParser parser, ViewGroup root) {
return inflate(parser, root, root != null);
}
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
// 将布局文件交给 xml 解析器
XmlResourceParser parser = getContext().getResources().getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context)mConstructorArgs[0];
// 上下文对象
mConstructorArgs[0] = mContext;
// 前面说到过的传递进来的 ViewGroup(可以为null)
View result = root;
try {
// Look for the root node.
// 寻找布局的根节点(最外层的View)
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();
// merge 标签
if (TAG_MERGE.equals(name)) {
... 省略代码 ...
rInflate(parser, root, attrs, false);
} else {
// 布局中的根视图
View temp;
if (TAG_1995.equals(name)) {
temp = new BlinkLayout(mContext, attrs);
} else {
// 根据 tag 创建根视图
temp = createViewFromTag(root, name, attrs);
}
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
// 如果传递进来的ViewGroup不为空,则在这里添加 LayoutParam 参数
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.
// 如果传进来的ViewGroup不为null,同时第三个参数为true,
// 则将加载完成的View作为child添加到 ViewGroup 中
// 同时返回的 View 为原来的ViewGroup
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.
// 如果传递进来的ViewGroup为空或者第三个参数为false
// 则将从布局文件中加载的View返回
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
... 省略 catch ...
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return result;
}
}
通过分析源码,我们可以知道布局文件是通过 pull解析器 来解析整个布局文件的;
先粗略的看下整个方法中的代码逻辑,从中我们可以分析出inflate()
方法参数的作用,代码中也写了注释,这里做一个归纳:
- 如果root(ViewGroup)不为null,attachToRoot(boolean)默认为true;
- 如果root(ViewGroup)不为null,且 attachToRoot(boolean)为true,则从布局文件加载上来的View会被当作child加入到root里面,同时将 root 当作返回值(这里就可以理解为什么一定要传入ViewGroup而不是View了);
- 如果root(ViewGroup)不为null,且 attachToRoot(boolean)为false,那么从布局文件加载上来的View将会有 layout 相关的属性(layout_width等),同时加载的View将会作为返回值;
- 如果root(ViewGroup)为null,那么,attachToRoot(boolean)参数将是一个无效的参数,此时从布局加载上来的顶层View的layout相关属性将会失效,同时加载的View将会作为返回值;
以上几点就是对加载布局文件时,几个参数的理解,此时我们还有一个问题没有解决,就是我们的View是如何从布局文件中创建的还不清楚,只是知道调用了 xml 解析器来解析了我们的布局文件(解析出来的数据还没有转换成Java内存中的对象);
关于View的创建以及ViewGroup的遍历
首先我们来考虑一个问题,就是布局文件中的View是如何转换成我们Java内存中的View的对象,也就是我们的View是如何被创建的,这里我们首先想到了反射,通过反射我们可以在代码运行的时候来创建对象;
我们可以仔细的阅读一下上面那段代码,可以找都一个很关键的点:
temp = createViewFromTag(root, name, attrs);
,这里传递进来的参数为ViewGroup、以及我们通过 xml 解析拿到的根视图的 name 和 属性;
View createViewFromTag(View parent, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
try {
View view;
// 这些factory参数我们都没有设置过,因此我们直接跳过这里
if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);
else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);
else view = null;
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);
}
// 正常情况下我们的view创建走的是这一段代码
// 这里将会来判断我们的View是系统控件还是我们的自定义控件
// 这里也很好的解释了我们的自定义控件为什么需要写全类名,而系统控件只需要写类名称即可
if (view == null) {
if (-1 == name.indexOf('.')) { // 系统控件
view = onCreateView(parent, name, attrs);
} else { // 自定义控件
view = createView(name, null, attrs);
}
}
return view;
} catch (Exception e) {
.....
}
}
这里如果使用过自定义控件的应该都知道,在布局文件中使用自定义控件,我们需要写入全类名(文件夹之间用 .
来分隔),而如果是使用系统控件,我们只需要写入控件的类名即可,这么设计的原因就在于系统控件的包名是指定的,可以通过拼接的方式拿到全类名,而自定义控件的包路径是由我们自己定义的,因此在布局文件中使用的时候需要指定全类名,我们可以通过分析源码来验证我们上面所说的:
protected View onCreateView(View parent, String name, AttributeSet attrs)
throws ClassNotFoundException {
return onCreateView(name, attrs);
}
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
// 返回系统控件的包名
return createView(name, "android.view.", attrs);
}
在拿到我们系统控件的包名后,同样也是调用了 createView()
方法,显然我们能够根据方法名看出来此方法就是用来创建我们的View的,直接上代码:
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);
// 缓存中没有,就通过我们前面拿到的完整包名利用反射来家在
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
// 这里如果是系统控件就拼接包名,如果是自定义控件就name就为完整的包名
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
// 这里将我们拿到的构造函数缓存起来
sConstructorMap.put(name, constructor);
} else {
... 省略代码 ...
}
Object[] args = mConstructorArgs;
args[1] = attrs;
// 这里就是通过反射来创建我们的View对象;
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;
} catch (Exception e) {
... 这里我们不看异常 ...
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
整个方法大致就是通过反射来创建对应的View对象,如果是第一次加载则会将我们的构造方法缓存起来方便下次使用;到这里我们就能理解我们的View是如何从布局文件中加载到Java内存中的对象,使用的是我们前面所说的Java的反射机制(关于Java的反射机制可以去网上找资料);
我们都知道Android中的视图是以树的形式展现的,这里我们只是加载了我们的根视图,其它子View的加载又是在什么时候,这里细心的朋友可能已经注意到了,我们的 inflate()
方法中还有另外一个关键的方法rInflate()
,代码中我也写了注释,接下来我们直接来看下这个方法的源码:
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();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_INCLUDE.equals(name)) { // 解析include标签
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, parent, attrs);
} else if (TAG_MERGE.equals(name)) { // 解析merge标签
throw new InflateException("<merge /> must be the root element");
} else if (TAG_1995.equals(name)) {
... 省略代码 ...
} else {
// 这里有没有很熟悉,我们前面的根视图也是通过这个方法来加载的
final View view = createViewFromTag(parent, name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 通过递归的方式遍历加载整个视图树(深度优先)
rInflate(parser, view, attrs, true);
// 将我们解析得到的View添加到我们传递进来的parent中
viewGroup.addView(view, params);
}
}
if (finishInflate) parent.onFinishInflate();
}
这里同样是根据我们前面所说的createViewFromTag(parent, name, attrs)
方法来创建我们的View,然后通过递归的方式,遍历整个视图树(跟以前学习代码的时候,递归遍历windows文件夹系统非常类似),然后将我们新创建的View添加到传进来的ViewGroup中,直至遍历完最后一个View;
到这里我们就可以理解为什么google官方推荐我们布局文件的深度最好不要超过三层,因为每增加一层视图树,都会增加我们遍历的时间,从而影响性能;
总结
本片文章主要是讲解了Android系统中是如何加载布局文件的,首先讲解了不同的加载方式其实都是调用了同一个类对象来加载的,然后就是讲解了布局文件加载中,各个参数的作用(这个在开发中非常有用),最后就是分析了下布局文件中的控件是如何加载到Java内存当中以及整个视图树的加载(这个以了解为目标,知道就可以)。
主要以通过分析源码的方式来理解整个流程,这样会比通过几个案例的结果来分析印象会更加的深刻;