Activity 在界面创建时需要将 XML 布局文件中的内容加载进来,正如我们在 ListView 或者 RecyclerView 中需要将 Item 的布局加载进来一样,都是使用 LayoutInflater 来进行操作的。
LayoutInflater 实例的获取有多种方式,但最终是通过(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
来得到的,也就是说加载布局的 LayoutInflater
是来自于系统服务的。
由于 Android 系统源码中关于 Content 部分采用的是装饰模式,Context 的具体功能都是由 ContextImpl
来实现的。通过在 ContextImpl 中找到getSystemService
的代码,一路跟进,得知最后返回的实例是PhoneLayoutInflater
。
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
LayoutInflater 只是一个抽象类,而 PhoneLayoutInflater 才是具体的实现类。
inflate 方法加载 View
使用 LayoutInflater 时常用方法就是inflate
方法了,将一个布局文件 ID 传入并最后解析成一个 View 。
LayoutInflater 加载布局的 inflate 方法也有多种重载形式:
View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
而这两者的差别就在于是否要将 resource
布局文件加载到 root
布局中去。
不过有点需要注意的地方,若 root
为 null,则在 xml 布局中为 resource
设置的属性会失效,只是单纯的加载布局。
// temp 是 xml 布局中的顶层 View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) { // root
// root 不为 null 才会生成 layoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 如果不添加到 root 中,则直接把布局参数设置给 temp
temp.setLayoutParams(params);
}
}
// 加载子 View
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
root.addView(temp, params);//添加到布局中,则布局参数用到 addView 中去
}
if (root == null || !attachToRoot) {
result = temp;
}
跟进createViewFromTag
方法查看 View 是如何创建出来的。
View view; // 最后要返回的 View
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs); // 是否设置了 Factory2
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs); // 是否设置了 Factory
} else {
view = null;
}
if (view == null && mPrivateFactory != null) { // 是否设置了 PrivateFactory
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) { // 如果的 Factory 都没有设置过,最后在生成 View
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) { // 系统控件
view = onCreateView(parent, name, attrs);
} else { // 非系统控件,自定义的 View
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
如果设置过 Factory 接口,那么将由 Factory 中的 onCreateView 方法来生成 View 。
关于 LayoutInflater.Factory
的作用,就是用来在加载布局时可以自行去创建 View,抢在系统创建 View 之前去创建。
关于 LayoutInflater.Factory
的使用场景,现在比较多的就是应用的换肤了。
若没有设置过 Factory 接口,则是判断是否为自定义控件或者系统控件,不管是 onCreateView 方法还是 createView 方法,内部最终都是调用到了 createView 方法,通过它来生成 View 。
// 通过反射生成 View 的参数,分别是 Context 和 AttributeSet 类
static final Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
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;
if (constructor == null) { // 从缓存中得到 View 的构造器,没有则调用 getConstructor
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);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) { // 过滤,是否允许生成该 View
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs); // 不允许生成该 View
}
}
}
Object[] args = mConstructorArgs;
args[1] = attrs;
final View view = constructor.newInstance(args); // 通过反射生成 View
return view;
在 createView 方法内部,首先从 View 的构造器缓存中查找是否有对应的缓存,若没有则生成构造器并且放到缓存中去,若有构造器则看能否通过过滤,是否允许该 View 生成。
最后都满足条件的则是通过 View 的构造器反射生成了 View 。
在生成 View 时采用 Constructor.newInstance
调用构造函数,而参数所需要的变量就是mConstructorSignature
变量所定义的,分别是 Context
和 AttributeSet
。可以看到,在最后生成 View 时也传入了对应的参数。
采用 Constructor.newInstance
的形式反射生成 View ,是为了解耦,只需要有了类名,就可以加载出来。
由此可见,LayoutInflater 加载布局仍然是需要传递 Context
的,不光是为了得到 LayoutInflater ,在反射生成 View 时同样会用到。
深度遍历加载布局
如果需要加载的布局只有一个控件,那么 LayoutInflater 返回那个 View 工作也就结束了。
若布局文件中有多个需要加载的 View ,则通过rInflateChildren
方法继续加载顶层 View 下的 View ,最后通过rInflate
方法来加载。
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
// 若 while 条件不成立,则加载结束了
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(); // 从 XmlPullParser 中得到 name 出来解析
if (TAG_REQUEST_FOCUS.equals(name)) { // 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("<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); // 顶层 View 添加 子 View
}
}
if (finishInflate) { // 遍历解析
parent.onFinishInflate();
}
}
rInflate
方法首先判断是否解析结束了,若没有,则从 XmlPullParser 中加载出下一个 View 进行处理,中间还会对不同的类型进行处理,比如TAG_REQUEST_FOCUS
、TAG_TAG
、TAG_INCLUDE
、TAG_MERGE
等等。
最后仍然还是通过createViewFromTag
来生成 View ,并以这个生成的 View 为父节点,开始深度遍历,继续调用rInflateChildren
方法加载布局,并把这个 View 加入到它的父 View 中去。
至于为什么生成 View 的方法名字createViewFromTag
从字面上来看是来自于 Tag
标签,想必是和 XmlPullParser
解析布局生成的内容有关。
参考
- http://www.sunnyang.com/661.html?utm_source=tuicool&utm_medium=referral
- http://blog.csdn.net/lmj623565791/article/details/51503977
- https://segmentfault.com/a/1190000003813755
- http://blog.csdn.net/panda1234lee/article/details/9009719
知识星球
公众号音视频开发进阶对应的知识星球,一个编程开发领域的专业圈子,贩卖知识和技巧! ※ 入群须知:了解该星球能提供的价值和帮助,在提问时务必阐述好背景,附带相关的信息。 iOS 用户可以加我微信 ezglumes 邀请你进星球,有疑问也可以加我微信咨询。 ※ 星球内容: 基础教程: 在知识星球连载的干货教程,可以在专栏中找到,随着时间的推移,教程也会越来越多: - 音视频基础概念 - WebRTC 入门教程及源码实践 - 播放器教程及源码实践 - OpenGL 和特效开发教程 - Vulkan 入门教程 部分内容可以在博客 https://glumes.com 中检索到,后面会在星球里持续更新. 干货分享: 涵盖了移动开发和音视频工程领域的绝大部分,从项目实战角度出发,提升能力,包括但不限于以下领域: - Android/iOS 移动开发 - Camera 开发 - 短视频编辑 SDK 项目实践 - 在线直播和推流 - WebRTC 开发 - 播放器基础和提高 - OpenGL 图像渲染及特效开发 - C++ 基础和提高 - FFmpeg 使用和分析 - 干货资源和书籍分享 不止于技术方面的,各种 IT 新闻、茶余饭后、生活趣事也欢迎大家分享!!! 技术答疑解惑: 针对上述基础教程和干货分享的答疑,另外还有音视频和 IT 开发中的各种交流讨论。 - 基础知识点答疑 - 工业项目实践答疑 - 问题排查思路分析 一个 BUG 排查很久,不如来星球里提个问题,效率提升百倍。 求职和面试辅导: 一站式职场服务,每份工作都值得用心对待!!! - 面试题和面试经验分享 - 简历修改和模拟面试 - 大厂内推和信息同步 - 职场经验分享 - 职业规划和发展分析 ※ 星主和合伙人介绍 星主是公众号音视频开发进阶的作者,也是网站 https://glumes.com 的作者,曾参与过抖音、剪映等头部音视频 APP 底层 SDK 的开发。 合伙人也是在头条、快手从事音视频架构师的职位,具有多年的音视频开发经验,能力圈覆盖了音视频的绝大多数领域,资深音视频从业人员为你保驾护航。
微信公众号
扫描下面的二维码关注我的微信公众号《音视频开发进阶》,推送更多精彩内容!
添加我的微信 ezglumes 拉你入音视频与图形图像技术群一起交流学习~

原创文章,转载请注明来源: Android 布局加载之 LayoutInflater