从资源文件到控件——LayoutInflater分析
(注:源代码为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
则会对布局资源文件进行解析,根据解析得到的控件名称,获取控件构造器。再利用控件构造器创建控件实例。