一、概述
View rootView = LayoutInflater.from(getContext()).inflate(R.layout.bitable_grid_view, this, false);
Android 开发者对上面这行代码应该不陌生,我们通常用这个方法来渲染一个布局。其中的原理是怎么样的呢?
本文主要分析 LayoutInflater 的创建过程以及 inflate 方法的原理。
二、LayoutInflater 是如何创建的
从 LayoutInflater#from 方法看起。
|
|
从代码中可以看出,LayoutInflater 是通过 Context#getSystemService 的方式获取的。
Context 是一个抽象类,它的具体实现类为 ContextImpl。所以应该看 ContextImpl 的 getSystemService 是如何实现的。
ContextImpl#getSystemService
|
|
ContextImpl#getSystemService 方法又委托了 SystemServiceRegistry。
SystemServiceRegistry 中有一个 HashMap<String, ServiceFetcher<?>> ,该 HashMap 以服务名称为 key,以服务相对应的 ServiceFetcher 作为 value。
|
|
具体的获取方法为
|
|
根据服务名称去获取相应的 ServiceFetcher,
- 如果
ServiceFetcher不为空,则调用ServiceFetcher.getService方法获取相应服务的引用。- 如果是第一次调用会先创建,然后直接返回
- 否则直接返回缓存的值
- 如果
ServiceFetcher为空,则返回 null。
SYSTEM_SERVICE_FETCHER 数据初始化
SYSTEM_SERVICE_FETCHERS 是一个键为 String, 值为 ServiceFetcher<?> 的静态 HashMap 常量,其中的数据是在静态代码块中插入的。
LayoutInflater 对应的 ServiceFetcher 就是在这个时候存储进去的。
|
|
为什么要放在 CachedServiceFetcher 中而不是直接创建呢?
ServiceFetcher 是一个抽象类,系统使用的一个具体实现类为 CachedServiceFetcher。从名字来看,主要是为了实现懒加载,当首次需要使用才触发初始化,避免浪费资源。
CachedServiceFetcher#getService 方法的实现
|
|
小结
通过 from 的方式获取 LayoutInflater,最终调用的是 SystemServiceRegistry 的 getService 方法,该方法会从 ContextImpl 中维护的缓存数组中获取服务,如果不存在,则调用 createService 方法创建一个 PhoneLayoutInflater。(注:LayoutInflater 是一个抽象类,系统创建的是它的子类—— PhoneLayoutInflater)
onCreateView 是 PhoneLayoutInflater 中最重要的方法。为什么说它重要,后面会提到。
|
|
三、布局创建过程
一般我们在渲染 ListView 或者 RecyclerView 中的列表项时,都会调用 inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) 方法(有时也会使用两个参数的方法)。可以看到该方法内部会调用 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)。
|
|
|
|
|
|
以上的 inflate 方法主要有以下几步
- 解析 xml 的根标签
- 如果根标签是 merge,调用 rInflate 进行解析,rInflate 会将 merge 标签下的所有子 View 直接添加到根标签中
- 如果标签是普通元素,调用 createFromTag,生成相应的 view。
- 调用 rInflate 解析 temp 根元素下的所有子 View,并且将这些子 View 都添加到 temp 下
- 返回解析到的根视图。
我们先从解析单个元素的 createViewFromTag 方法看起。
|
|
onCreateView 方法和 createView 方法有何不同?
前面的介绍中,我们有提到 LayoutInlflater 创建的实际类型为 PhoneLayoutInlflater ,PhoneLayoutInlflater 覆写了 onCreateView 方法,该方法就是在 View 的标签名前添加一个 "android.widget." 或 "android.webkit." 或 "android.app."前缀。然后再传递给 createView 解析。
- 也就是说内置 View 和自定义 View 最终都调用了 createView 进行解析。
为什么要这么设计呢?
这是为了方便开发者在 xml 文件中更方便使用系统内置的 View(只需要写 View 名称而不需要写完整的路径),如果是自定义控件或者第三方库,写完整路径。
createView 的具体实现如下
|
|
createView 方法中,如果控件名有前缀就先构造 View 的完整路径,并且将该类加载到虚拟机中
- 然后获取该类的构造函数并缓存起来,再通过构造函数来创建该 View 的对象,
- 最后将 View 对象返回,这就是解析单个 View 的过程
|
|
rInflate() 通过深度优先遍历来构造视图树。每解析到一个 View 元素就会递归调用 rInflate,直到这条路径下的最后一个元素,
然后在回溯过来将每个 View 元素添加到它们的 parent 中。
- 通过 rInflate 的解析之后,整棵视图树就构建完毕。当调用了 Activity 的 onResume 之后,之前通过 setContentView 设置的内容就会出现在我们的视野中。
四、常见问题
inflate 方法有多种重载,它们的区别在哪里?
三个参数的 infalte 方法的各个参数的含义是什么?
LayoutInflater#inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
resource 参数为布局资源。是一个 xml 文件。
若 root 不为空时
- 如果 attachToRoot 为 true,则它是 xml 文件的根布局的父布局。(也就是说,渲染出来的布局被 add root 中。同时,inflate 方法的返回值为 root)
- 如果 attachToRoot 为 false,它只是为 xml 文件的根布局 提供一组 LayoutParams 值。同时 inflate 方法返回的 xml 文件的根布局。
两个参数的 inflate 方法,内部也是调用三个参数的 infalte 方法
|
|
当 root 不为空的时候,inflate(@LayoutRes int resource, @Nullable ViewGroup root) 方法默认将渲染后的布局 add 到 root 中。
还有另外两种重载,区别仅在于它们的第一个参数都是 XmlPullParser,root 和 attachToRoot 参数表现与上述一致。
小结
创建流程大致如下:
首先查找根标签
- 如果是 merge,调用 rInflate
- 否则,调用
createViewFromTag- 如果是系统内置控件(通过名称中是否含有「.」来判断),调用
PhoneLayoutInflater.onCreateView()方法添加前缀,- 处理后将完整路径传给
LayoutInflater.createView()方法,该方法内部会通过反射的方式创建出对应的 view 实例。
- 处理后将完整路径传给
- 否则,直接调用
LayoutInflater.createView()进行解析。该方法内部会通过反射的方式创建出对应的 view 实例。
- 如果是系统内置控件(通过名称中是否含有「.」来判断),调用
五、拓展用法
前面提到在布局创建过程中,会调用 createViewFromTag 方法。
createViewFromTag 方法中,有下一段代码。
|
|
|
|
我们可以自己实现 Factory 中的 onCreateView 方法。系统在填充 view 前会回调该方法,可以返回什么样的 view 已经其中的属性。
1.全局替换字体属性
因为字体是 TextView 的一个属性,为了加一个属性,我们就没必要去全部的布局中进行更改,只需要上我们的 onCreateView 中,发现是 TextView,就去设置我们对应的字体。
|
|
2.动态换肤功能
基本原理和上面的更换字体类似,我们可以对做了标记的 View 进行识别,然后在 onCreateView 遍历到它的时候,更改它的一些属性,比如背景色等,然后再交给系统去生成 View。
3.无需编写 shape、selector,直接在 xml 设置值
无需自定义 View,彻底解放 shape,selector 吧
文章中讲到我们如果要设置控件的角度等属性值,不需要再去写特定的 shape 或者 selector 文件,直接在 xml 中写入:
|
|
其原理也是通过自定义 Factory,在Factory2#onCreateView方法里面,判断 attrs 的参数名字,比如发现名字是我们制定的 stroke_color 属性,就去通过代码手动帮他去设置这个值,我们来查看下它的 onCreateView 方法:
|
|
六、参考资料与学习资源推荐
- 《Android 源码设计模式解析与实战》
- Android 技能树 — LayoutInflater Factory 小结
由于本人水平有限,可能出于误解或者笔误难免出错,如果发现有问题或者对文中内容存在疑问请在下面评论区告诉我,谢谢!