从资源文件到控件——LayoutInflater分析
本文也发布于本人的知乎文章:https://zhuanlan.zhihu.com/p/394073666
(注:源代码为android-8.1)
0. 前言
我在我的文章《Android源码阅读分析:从Activity开始(二)——加载布局》中简单介绍了
Activity
的如何加载布局的。在文章末尾提到,资源文件通过
inflater
方法转换为
View
。那么本篇文章就来分析一下
LayoutInflater
是如何工作的。
1. 创建LayoutInflater
LayoutInflater
的构造方法被
protected
修饰,也就是说,我们在创建
LayoutInflater
时,不能直接使用构造方法来创建新对象。官方的推荐获取
LayoutInflater
对象方式有两个:
- 通过
Activity.getLayoutInflater
方法获取
- 通过
Context.getSystemService
方式获取
这样能够保证获取到的
LayoutInflater
对象是已经与当前
context
关联上。那么,我就分别从这两个方法来看一下是如何获取对象的。
1.1 通过
Activity.getLayoutInflater
获取
LayoutInflater
对象
(frameworks/base/core/java/android/app/Activity.java)
public LayoutInflater getLayoutInflater() {
return getWindow().getLayoutInflater();
}
从上一篇文章中,我们知道了,
Activity.getWindow
方法所得到的是一个
PhoneWindow
对象,那么跟踪
PhoneWindow
的
getLayoutInflater
方法。
(framework/base/core/java/com/android/internal/policy/PhoneWindow.java)
public LayoutInflater getLayoutInflater() {
return mLayoutInflater;
}
哦?这里就是简简单单的返回了
mLayoutInflater
。那么,我们就要看看这个变量是在哪里创建或者赋值的。
经过查找,发现在
PhoneWindow
类里,只有一个地方是
mLayoutInflater
变量被赋值了。
(frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java)
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
这里调用了
LayoutInflater
的
from
方法。
(frameworks/base/core/java/android/view/LayoutInflater.java)
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;
}
可以看出,这里调用了
context.getSystemService
方法,这与官方推荐的第二种方法相同。
1.2 通过
Context.getSystemService
方式获取
LayoutInflater
对象
getSystemService
是
Context
的一个方法,通过传入
NAME
来获取对应的
Object
,然后转换为相应的服务对象。这里传入的参数值为
Context.LAYOUT_INFLATER_SERVICE
。通过查找,在
ContextThemeWrapper
类中找到处理
Context.LAYOUT_INFLATER_SERVICE
的方法。
(frameworks/base/core/java/androidview/ContextThemeWrapper.java)
@Override
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
这里又调用了
from(getBaseContext)
,而
baseContext
通常情况下就是
ContextImpl
。那么跟踪到
ContextImpl
的
getSystemService
方法。
(frameworks/base/core/java/android/app/ContextImpl.java)
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
在这里我们发现,是从
SystemServiceRegistry
里获取的系统服务,再继续跟踪。
(frameworks/base/core/java/android/app/SystemServiceRegistry.java)
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
从这里可以看出,
LayoutInflater
是从
SYSTEM_SERVICE_FETCHERS
里获取的,而
SYSTEM_SERVICE_FETCHERS
是一个
HashMap
,继续跟踪代码,发现
Context.LAYOUT_INFLATER_SERVICE
是在静态代码块中调用静态方法
registerService
内存入
SYSTEM_SERVICE_FETCHERS
的。
(frameworks/base/core/java/android/app/SystemServiceRegistry.java)
static {
...
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
...
}
到这里,我们终于发现了真正的
LayoutInflater
是怎么被创造出来的了。原来我们真正使用的
LayoutInflater
其实是它的子类
PhoneLayoutInflater
。
那么,我们再看一下
PhoneLayoutInflater
类的
cloneInContext
方法。这个方法在
LayoutInflater
中是抽象方法。
(frameworks/base/core/java/com/android/internal/policy/PhoneLayoutInflater.java)
public LayoutInflater cloneInContext(Context newContext) {
return new PhoneLayoutInflater(this, newContext);
}
通过这个方法,可以获取一个在新的
Context
下的
PhoneLayoutInflater
对象。
2. 构建View
通常情况下,我们使用
LayoutInflater
构建
View
时,会调用
inflate
方法,下面我们来看一下其实现。
(frameworks/base/core/java/android/view/LayoutInflater.java)
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
继续跟踪。
(frameworks/base/core/java/android/view/LayoutInflater.java)
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();
}
}
首先根据
resourceId
从
Context
中获取到
Resource
对象,然后调用
getLayout
得到一个XML解析器,之后调用重载方法
inflate
。
(frameworks/base/core/java/android/view/LayoutInflater.java)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
sychornized(mConstructorArgs) {
...
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
...
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// 获取根节点
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) {
// Empty
}
...
final String name = parser.getName();
...
// merge标签
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, inflaterContext, attrs, false);
} else {
// XML资源文件中的根节点View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
// 解析子节点
rInflateChildren(parser, temp, attrs, true);
...
if (root != null && attachToRoot) {
// 将解析出的根节点View添加到
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
result = temp;
}
}
...
}
return result;
}
}
这个方法的主要功能是根据参数和布局资源文件解析根节点
View
并返回,其内部调用了
rInflate
方法和
rInflateChildren
方法。其中,
rInflateChildren
方法内部也调用了
rInflate
方法。
(frameworks/base/core/java/android/view/LayoutInflater.java)
void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
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)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> 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 (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
这个方法通过递归方法解析出整个资源文件的布局树。其中核心的实例化
View
的方法为
createViewFromTag
。那么下面就重点看一下
createViewFromTag
方法是如何实现的。
(frameworks/base/core/java/android/view/LayoutInflater.java)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
...
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
...
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
// 如果name不含".",表示该控件为系统自带控件
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
}
...
}
return view;
}
...
}
在创建控件实例时,首先判断该控件是否为系统自带控件,如果是系统自带控件,则调用
onCreateView
方法,否则直接调用
createView
方法。我们先看一下
onCreateView
方法,该方法在
LayoutInflater
中其实就是在控件名称前加上
android.view.
前缀,之后再调用
createView
方法。
PhoneLayoutInflater
复写了
onCreateView
方法,其实只是增加了三个前缀
android.widget.
、
android.webkit.
、
android.app.
,最终仍然会调用
createView
方法。那么下面就看一下
createView
方法的实现。
(frameworks/base/core/java/android/view/LayoutInflater.java)
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 {
...
if (constructor == null) {
// 通过反射得到name对应的控件类
clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
...
// 获取构造器
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
if (mFilter != null) {
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
...
}
...
}
}
...
Object[] args = mConstructorArgs;
...
final View view = constructor.newInstance(args);
...
return view;
}
...
}
这个方法主要做了两件事,一个是获取或创建控件的构造器,另一个是通过构造器实例化控件。在创建控件的构造器时,用到了
mConstructorSignature
。
(frameworks/base/core/java/android/view/LayoutInflater.java)
static final Class<?>[] mConstructorSignature = new Class[] {Context.class, AttributeSet.class};
可以看到这里的得到的构造器是包含
Context
和
AttributeSet
类型的两个参数。也就是说,如果我们需要做自定义控件的话,那么必须要有这两个类型参数的构造方法,即
View(Context context, AttributeSet attrs)
方法。
至此,我们完成了从布局XML资源文件到控件的转换过程。
3. 总结
本篇文章主要分析了源码中
LayoutInflater
的创建过程以及其如何将布局XML资源文件转换为控件布局的过程。
总的来说,就是通过获取系统服务的方式,得到
LayoutInflater
对象。
LayoutInflater
则会对布局资源文件进行解析,根据解析得到的控件名称,获取控件构造器。再利用控件构造器创建控件实例。