Android inflate解析
对于inflate,我相信大家都不陌生,它的作用是将一个layout.xml布局文件变为一个View对象。尤其在ListView、GridView、RecyclerView的Adapter还有组合自定义控件中,我们都会使用inflate()
方法去加载一个布局,作为每个Item的布局。这篇博客就来分析一下Android中inflate是怎样将xml文件变为View的。
使用inflate,一般是调用两个类中的方法:
使用
View
中的静态方法View.inflate()
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root)-
使用
LayoutInflater
中的inflate()
方法,在LayoutInflater
类中有几个重载方法public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) public View inflate(XmlPullParser parser, @Nullable ViewGroup root) public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
inflate()
方法内部调用过程
-
View#inflate()
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) { LayoutInflater factory = LayoutInflater.from(context); return factory.inflate(resource, root); }
View.inflate()
调用了LayoutInflater
中的inflate()
方法,所以上面的两个类中的方法实际是一样的。下面看一下LayoutInflater
的调用过程。 -
LayoutInflater#inflate()
// 内部调用 inflate(resource, root, root != null) public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); } // 内部调用 inflate(parser, root, attachToRoot) public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); final XmlResourceParser parser = res.getLayout(resource); try { // 调用方法 return inflate(parser, root, attachToRoot); } finally { parser.close(); } } // 内部调用 inflate(resource, root, root != null) public View inflate(XmlPullParser parser, @Nullable ViewGroup root) { return inflate(parser, root, root != null); } // 最终调用方法 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot){ // 解析xml文件,返回View对象 }
通过上面的调用过程可以看到,不管是View#inflate()
方法还是LayoutInflater#inflate()
,最终都是调用了同一个方法,那么我们就只需要接着看这个方法的解析过程就可以了。
获取 LayoutInflater
对象
在说 inflate()
的解析过程之前,我们先来看一下外部怎么调用LayoutInflater#inflate()
方法,这个方法不是静态的。不能通过类直接调用,我们需要创建 LayoutInflater
对象才行,但是查看源码我们发现 LayoutInflater
类是抽象的 public abstract class LayoutInflater
,并且构造方法也是protected
的,是不能被实例化的,那么我们看一下它的子类,发现只有一个子类,是属于AsyncLayoutInflater
的私有静态内部类: private static class BasicInflater extends LayoutInflater
,那么这肯定也是不能被实例化的,那么我们该怎样获取 LayoutInflater
对象了?可以通过以下几个方式。
// 1. 通过 LayoutInflater 的静态方法 from
public static LayoutInflater from(Context context)
// 2. 通过系统服务方式获取
LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
// 3. 在Activity中,直接通过方法获取
public LayoutInflater getLayoutInflater()
Activity 调用 getLayoutInflater()
方法追踪
Activity#getLayoutInflater()
方法:
@NonNull
public LayoutInflater getLayoutInflater() {
return getWindow().getLayoutInflater();
}
在Activity
中调用getLayoutInflater()
方法,调用的是继承至Window
的PhoneWindow
中的getLayoutInflater()
方法,在PhoneWindow
中LayoutInflater
对象的初始化在PhoneWindow
的构造方法中,调用的是LayoutInflate.form(context)
方法:
// PhoneWindow.java
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
mRenderShadowsInCompositor = Settings.Global.getInt(context.getContentResolver(),
DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR, 1) != 0;
}
@Override
public LayoutInflater getLayoutInflater() {
return mLayoutInflater;
}
在构造方法中也是也是通过 LayoutInflater
的静态方法 from()
获取:
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
最终也是通过系统服务方式获取,所以LayoutInflater对象最终都是通过getSystemService()方法获取的。
LayoutInflater#inflate(XmlPullParser, ViewGroup, boolean)
源码分析
// parser:XML文档解析器对象,通过pull解析方式解析xml文档
// Resources类中的XmlResourceParser getLayout(@LayoutRes int id)方法获取
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
// 初始化操作
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
// 返回结果,默认返回root对象,下面的代码会对result进行操作
View result = root;
try {
// 调用 advanceToRootNode(parser) 方法,直接将方法中的代码移动到这里来,具体代码如下
// ================= advanceToRootNode(parser) ================= //
// 将给定的解析器推进到第一个 START_TAG。如果没有找到开始标签,则抛出 InflateException
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!");
}
// ================= advanceToRootNode(parser) ================= //
// 获取第一个节点名称
final String name = parser.getName();
if (TAG_MERGE.equals(name)) {
// 如果节点是merge并且root为空或者attachToRoot为false,抛出异常
// (因为merge标签能够将该标签中的所有控件直接连在上一级布局上面,
// 从而减少布局层级,假如一个线性布局替换为merge标签,那么原线性布局下的多个控件将直接连在上一层结构上)
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 实例化 root 的子视图,并且将实例化后的View添加到root中(root.addView()方法)。
// 然后调用 root的onFinishInflate()
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 通过createViewFromTag()方法用于根据节点名来创建View对象,这里是创建根视图。
// 在方法内部调用了createView()方法,createView()方法中通过反射创建View对象并返回。
// 这里的 name 就是第一个节点名称,也就是xml布局的根节点,temp 就表示根布局的View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// root不为null时,获取root中的布局参数
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// root不为null并且attachToRoot为false时,将root的布局参数设置给temp
temp.setLayoutParams(params);
}
}
// 调用rInflateChildren()方法递归创建每一个孩子视图,rInflateChildren()方法内部会调用rInflate()方法。
// 在SDK版本小于23当中就是直接调用rInflate()方法
rInflateChildren(parser, temp, attrs, true);
// 如果root不为null 并且 attachToRoot为true时,就把temp加到root上,相当于给temp增加一个父节点
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 如果root为null 或者attachToRoot为false,那么就将temp赋值给result作为结果返回
if (root == null || !attachToRoot) {
result = temp;
}
}
}
// 返回 result
return result;
}
}
代码中已经有详细注释了,接着看 rInflateChildren()
和 rInflate()
的源码
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
// 调用 rInflate() 方法
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
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 {
// 调用createViewFromTag()方法创建View对象
final View view = createViewFromTag(parent, name, context, attrs);
// 父节点
final ViewGroup viewGroup = (ViewGroup) parent;
// 获取布局参数
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 递归调用rInflateChildren()方法,继续往下层inflate
rInflateChildren(parser, view, attrs, true);
// 将通过createViewFromTag()方法创建View对象添加到父节点中
viewGroup.addView(view, params);
}
}
// 判断是否完成inflate,如果完成了,调用onFinishInflate()方法,在自定义控件时可以重写这个方法获取相关参数
if (finishInflate) {
parent.onFinishInflate();
}
}
这样,就把整个布局文件都解析完成,形成了一个完整的DOM结构,最终会把最顶层的根布局返回,至此inflate()过程全部结束。
inflate过程总结
对inflate()
的总结,主要是root
参数和attachToRoot
参数设置不同值的总结:
- 如果
root
为null
,attachToRoot
不管是true
还是false
,返回的result
都是temp
,attachToRoot
没有意义(都是走result = temp
语句); - 如果
root
不为null
,attachToRoot
设为true
,则会给加载的布局文件的指定一个父布局,即root
(会走root.addView(temp, params)
这条语句,将temp
增加到root
中); - 如果
root
不为null
,attachToRoot
设为false
,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效(会走temp.setLayoutParams(params)
这条语句,将最外层的属性设置给temp); - 在不设置
attachToRoot
参数的情况下,attachToRoot = (root != null)
;root
为null
,attachToRoot
为alse
,root
不为null
,attachToRoot
为true
。
其实这个总结并不需要记住,通过查看inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
这个方法的源码就很容易得到。
扩展:Activity#setContentView(resId)
方法过程
// Activity#setContentView(@LayoutRes int layoutResID)
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
}
调用 Window
的 setContentView(int layoutResID)
方法,Window
就是PhoneWindow
// PhoneWindow#setContentView(int layoutResID)
@Override
public void setContentView(int layoutResID) {
// 省略其他无关代码,
// mLayoutInflater 就是 LayoutInflater,在构造方法创建
// mContentParent 就是 FrameLayout
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent
是 FrameLayout
的原因,可以在 《Activity 的组成》中知道。
最终调用了 LayoutInflater#inflate(XmlPullParser parser, @Nullable ViewGroup root)
方法,然后将我们的实际布局变为 View
对象,最后添加到表示页面内容的 FrameLayout
中。