上一章我们分析了 Activity 启动的时候调用 setContentView 加载布局的过程, 但是分析过程中我们留了两个悬念, 一个是将资源文件中的 layout 中 xml 布局文件通过 inflate 加载到 Activity 中的过程, 另一个是开始测量, 布局和绘制的过程, 第二个我们放到 measure 过程中分析, 这一篇先分析第一个 inflate 过程.
Android 系统源码分析 --View 绘制流程之 - setContentView
Android 系统源码分析 --View 绘制流程之 - onMeasure
Android 系统源码分析 --View 绘制流程之 - onLayout
Android 系统源码分析 --View 绘制流程之 - onDraw
Android 系统源码分析 --View 绘制流程之 - 硬件加速
Android 系统源码分析 --View 绘制流程之 - addView
Android 系统源码分析 --View 绘制流程之 - 弹性效果
LayoutInflater.inflate 方法基本上每个开发者都用过, 也有很多开发者了解过它的两个方法的区别, 也有一些开发者去研究过源码, 我这里再重复分析这个方法的源码其实一是做个记录, 二是指出我认为的几个重点, 帮助我们没有看过源码的人去了解将 xml 布局加载到代码中的过程. 这里我们需要重点关注三个问题, 然后根据对源码的分析来解决这三个问题, 帮助我们详细了解 inflate 的过程及影响, 那么这篇文章的目的就达到了.
问题:
LayoutInflater.inflate 两个个方法是什么?
这两个方法会给我们的视图显示带来什么影响?
View 视图的宽, 高是什么时候解析的?
第一个问题: LayoutInflater.inflate 两个个方法是什么?
这个问题是最简单的, 基本上这两个方法都使用过, 但是使用的结果却是不一样的. 下面我贴出来这两个方法的代码:
- public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
- public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
虽然是两个方法, 但是第一个方法最终会调用第二个方法:
- public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
- return inflate(resource, root, root != null);
- }
调用第二个方法的时候第三个参数是与第二个参数 ViewGroup 是否为空有关的, 这个参数具体作用我们后面代码流程分析再说. 我们先看使用的几种情况:
- // 第一种情况
- LayoutInflater.from(mContext).inflate(R.layout.screen_simple, mParentView);
- // 第二种情况
- LayoutInflater.from(mContext).inflate(R.layout.screen_simple, null);
- // 第三种情况
- LayoutInflater.from(mContext).inflate(R.layout.screen_simple, mParentView, false);
- // 第四种情况
- LayoutInflater.from(mContext).inflate(R.layout.screen_simple, mParentView, true);
- // 第五种情况
- LayoutInflater.from(mContext).inflate(R.layout.screen_simple, null, false);
- // 第六种情况
- LayoutInflater.from(mContext).inflate(R.layout.screen_simple, null, true);
这里罗列了所有用法, 但是不同的用法可能对我们的显示效果是有影响的, 那么就到了第二个问题, 下面通过分析代码过程来看看到底有什么影响. 还有第三个问题, 是我之前面试的时候被问到的, 之前看 inflate 源码没有很详细, 所以没有回答上来, 这次也一起分析一下, 这个宽, 高可能很多人觉得是和其他属性一起解析的, 其实不是, 这个是单独解析的, 就是因为 View 的宽, 高是单独解析的, 所以会有一些问题出现, 可能有些开发者也遇到这个坑, 通过这篇文章分析你会的到答案, 并且可以准确填上你的坑.
在上面六种情况中是有一样的:
如果 mParentView 不是 null, 那么: 1,4 是一样的, 2,5 是一样的, 3 是一样, 6 是一样,
如果 mParentView 是 null, 那么: 1,2,3,5 是一样, 4,6 是一样的.
代码流程
先看一张流程图:
- 1.LayoutInflater.inflate
- public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
- return inflate(resource, root, root != null);
- }
- 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 方法调用最终调用到第二个是三个参数的方法, 只不过第三个参数是与第二个参数有关系的, 这个关系就是 root 是不是 null, 如果不是 null, 传递 true, 反之传递 false.
- 2.LayoutInflater.inflate
- public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
- synchronized (mConstructorArgs) {
- ...
- View result = root;
- try {
- int type;
- ...
- final String name = parser.getName();
- ...
- // 要加载的布局根标签是 merge, 那么必须传递 ViewGroup 进来, 并且要添加到该 ViewGroup 上
- 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 {// 根标签不是 merge
- // temp 是要解析的 xml 布局中的根布局视图
- final View temp = createViewFromTag(root, name, inflaterContext, attrs);
- ViewGroup.LayoutParams params = null;
- // 1.root 不为空会解析宽, 高属性 (如果不添加的话, 那么会将属性设置给 xml 的根布局)
- if (root != null) {
- // root 存在才会解析 xml 根布局的宽高 (如果 xml 文件中设置的话)
- params = root.generateLayoutParams(attrs);
- // 不将该 xml 布局添加到 root 上的话
- if (!attachToRoot) {
- temp.setLayoutParams(params);
- }
- }
- // 递归解析 temp(xml 文件中的根布局) 下所有视图, 并按树形结构添加到 temp 中
- rInflateChildren(parser, temp, attrs, true);
- // 2.root 视图不为空, 并且需要添加到 root 上面, 那么调用 addView 方法并且设置 LayoutParams 属性
- if (root != null && attachToRoot) {
- root.addView(temp, params);
- }
- // 3.root 为空, 或者不添加到 root 上, 那么就会将该 xml 的根布局赋值给 result 返回,
- // 但是这里是没有解析也没有设置宽高的
- if (root == null || !attachToRoot) {
- result = temp;
- }
- }
- } catch (XmlPullParserException e) {
- ...
- }
- return result;
- }
- }
这里开始 layout 布局的最开始解析, 首先 if 语句是判断根视图, 也就是最外层视图是 merge 标签的时候, 必须传入的 root 不是 null, 并且第三个参数 attachToRoot 必须是 true, 否则抛出异常. 如果 root 不为 null, 并且 attachToRoot==true, 那么调用 rInflate 方法继续解析. 如果不是 merge 标签, 那么解析过程由外向内开始解析, 所以首先解析最外层的根视图并保存为 temp, 这里如果 root 不是 null, 那么就要获取 LayoutParam 属性, 这个方法下面再看, 然后判断如果 attachToRoot 是 false 的话那么就给 temp 设置属性, 如果为 true 就没有设置. 然后调用 rInflateChildren 方法递归解析 temp 下面的所有视图, 并按树形结果添加到 temp 中. 接着判断 root 不为 null, 并且 attachToRoot 为 true, 那么将 temp 添加到 root 中并且设置属性值, 所以这里可以看出, attachToRoot 参数是是否将解析出来的 layout 布局添加到 root 上面, 如果添加则会有属性值.
** 所以这里的重点就是 root 决定 layout 布局是否被设置 ViewGroup.LayoutParams 属性, 而 attachToRoot 决定解析出来的视图是否添加到 root 上面.** 这里我们先看获取的 ViewGroup.LayoutParams 属性包含了那几个属性值.
- 3.ViewGroup.generateLayoutParams
- public LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new LayoutParams(getContext(), attrs);
- }
这里只是 new 了一个新对象 LayoutParams, 我们看看这个 LayoutParams 对象的构造函数做了什么
- public LayoutParams(Context c, AttributeSet attrs) {
- TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
- setBaseAttributes(a,
- R.styleable.ViewGroup_Layout_layout_width,
- R.styleable.ViewGroup_Layout_layout_height);
- a.recycle();
- }
这里调用 setBaseAttributes 函数:
- protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
- width = a.getLayoutDimension(widthAttr, "layout_width");
- height = a.getLayoutDimension(heightAttr, "layout_height");
- }
到这里基本明确了, 这里就是获取视图的宽, 高属性值的, 也就是我们 layout 布局中视图的宽, 高值. 宽, 高包括以下几种:
- public static final int FILL_PARENT = -1;
- public static final int MATCH_PARENT = -1;
- public static final int WRAP_CONTENT = -2;
只有具体值, 也就是我们设置的 layout_width 和 layout_height 值, 其实上面第一种已经被第二个取代了.
所以我们这里看到了视图的宽, 高就是通过 ViewGroup.generateLayoutParams 来获取的, 如果没有调用那么解析的视图就没有有效的宽, 高, 如果需要具体值就要自己手动设置了. 也就是在调用 LayoutInflater.inflate 方法的时候想让自己设置的宽, 高有效, 传入 root 就不能是 null, 否则不会获取有效的宽, 高参数, 在后面显示视图的时候系统会配置默认的宽, 高, 而不是我们设置的宽, 搞. 这个后面会再分析.
还有一种情况就是我想获取宽, 高, 但是不想添加到 root 上, 而是我手动添加到别的 ViewGroup 上面需要怎么办, 那就是调用三个参数的 inflate 方法, root 参数不是 null,attachToRoot 设置为 false 就可以了
- 4.LayoutInflater.rInflate
- 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)) { // requestFocus
- parseRequestFocus(parser, parent);
- } else if (TAG_TAG.equals(name)) { // tag
- parseViewTag(parser, parent, attrs);
- } else if (TAG_INCLUDE.equals(name)) { // include
- if (parser.getDepth() == 0) {// include 不能是根标签
- throw new InflateException("<include /> cannot be the root element");
- }
- parseInclude(parser, context, parent, attrs);
- } else if (TAG_MERGE.equals(name)) { // merge
- // merge 必须是根标签
- throw new InflateException("<merge /> must be the root element");
- } else {// 正常 View
- 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);
- // parent 下的所有 view 解析完成就会添加到 parent 上
- viewGroup.addView(view, params);
- }
- }
- // parent 下所有视图解析并 add 完成就会调用 onFinishInflate 方法, 所以我们可以根据这个方法判断是否解析完成
- if (finishInflate) {
- parent.onFinishInflate();
- }
- }
上面第 2 步中, 如果根标签是 merge 那么直接调用这个方法继续解析下一层, 这里有五种情况, 前两种我们不分析, 基本不用, 我们分析下面我们常用的: 如果是 include 标签, 那么就要判断 include 的层级, 如果 include 下没有其他层级, 那么会抛出异常, 也就是 include 下必须有 layout 布局, 然后会调用 parseInclude 来解析 include 标签的布局文件; 另外就是 merge 嵌套 merge 也是不行的, 会抛出异常; 最后就是正常视图, 通过 createViewFromTag 来创建该视图, 然后解析宽, 高, 这里是直接解析了, 只有最外层是要判断 root 的, 然后调用 rInflateChildren, 这里 rInflateChildren 还是会调用这里的方法, 也就是形成递归解析下一层视图并添加到外面一层视图上面, 这里都是有宽, 高属性的. 最后有一个 if 语句, 这里的意思是每个 ViewGroup 下面的所有层级的视图解析完成后, 会调用这个 ViewGroup 的 onFinishInflate 方法, 通知视图解析并添加完成, 所以我们在自定义 ViewGroup 的时候可以通过这个方法来判断你自定义的 ViewGroup 是否加载完成.
下面我们再看 parseInclude 方法是如何解析 include 标签视图的
- 5.LayoutInflater.parseInclude
- private void parseInclude(XmlPullParser parser, Context context, View parent,
- AttributeSet attrs) throws XmlPullParserException, IOException {
- int type;
- // include 标签必须在 ViewGroup 使用, 所以这里 parent 必须是 ViewGroup
- if (parent instanceof ViewGroup) {
- ...
- if (layout == 0) {
- final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
- throw new InflateException("You must specify a valid layout"
- + "reference. The layout ID" + value + "is not valid.");
- } else {// include 中 layout 的指向 id 必须有效
- ...
- try {
- ...
- final String childName = childParser.getName();
- if (TAG_MERGE.equals(childName)) {// merge
- // The <merge> tag doesn't support Android:theme, so
- // nothing special to do here.
- rInflate(childParser, parent, context, childAttrs, false);
- } else {// 正常 View
- final View view = createViewFromTag(parent, childName,
- context, childAttrs, hasThemeOverride);
- final ViewGroup group = (ViewGroup) parent;
- ...
- ViewGroup.LayoutParams params = null;
- try {
- // include 是否设置了宽高
- params = group.generateLayoutParams(attrs);
- } catch (RuntimeException e) {
- // Ignore, just fail over to child attrs.
- }
- // 如果 include 没有设置宽高, 则获取 layout 指向的布局中的宽高
- if (params == null) {
- params = group.generateLayoutParams(childAttrs);
- }
- view.setLayoutParams(params);
- // Inflate all children.
- rInflateChildren(childParser, view, childAttrs, true);
- ...
- group.addView(view);
- }
- } finally {
- childParser.close();
- }
- }
- } else {// include 必须在 ViewGroup 中使用
- throw new InflateException("<include /> can only be used inside of a ViewGroup");
- }
- ...
- }
这里首先判断 include 标签的上一个层级是不是 ViewGroup, 如果不是那么抛出异常, 也就是 include 必须在 ViewGroup 内使用. 如果是在 ViewGroup 中使用, 那么接着判断 layout 的 id 是否有效的, 如果不是, 那么就要抛出异常, 也就是 include 必须包含有效的视图布局, 然后开始解析 layout 部分视图, 如果跟布局是 merge, 那么调用解析对应 merge 的方法 rInflate, 也就是步骤 4, 如果是正常的 View 视图, 那么通过 createViewFromTag 方法获取视图, 然后获取 include 标签的宽, 高, 如果 include 中没有设置才获取 include 包含的 layout 中的宽, 高, 也就是 include 设置的宽, 高优先于 layout 指向的布局中的宽, 高, 所以这里要注意了. 获取完成会设置对应的宽高属性, 然后调用 rInflateChildren 递归完成 layout 下所有层级视图的加载. 基本的逻辑就差不多了, 其实并不复杂, 还有个方法需要简单介绍下 - createViewFromTag, 根据 xml 中的标签也就是视图的名字加载 View 实体.
- 6.LayoutInflater.createViewFromTag
- View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
- boolean ignoreThemeAttr) {
- ...
- try {
- View view;
- ...
- if (view == null) {
- ...
- try {
- // 系统自带的 View(直接使用名字, 不用带包名, 所以没有 ".")
- if (-1 == name.indexOf('.')) {
- view = onCreateView(parent, name, attrs);
- } else {// 带有包名的 View(例如自定义的 View, 或者引用的 support 包中的 View)
- view = createView(name, null, attrs);
- }
- } finally {
- ...
- }
- }
- return view;
- } catch (InflateException e) {
- ...
- }
- }
这个方法里有两行注释, 我解释一下, 我们在 xml 布局中有两种写法, 一种是系统自带的视图, 例如: FrameLayout,LinearLayout 等, 一种是自定义的或者是 Support 包中的也就是带有包名的视图:
- <?xml version="1.0" encoding="utf-8"?>
- <RelativeLayout xmlns:Android="http://schemas.android.com/apk/res/android"
- Android:layout_width="match_parent"
- Android:layout_height="match_parent">
- <Android.support.v7.widget.RecyclerView
- Android:id="@+id/recyclerview"
- Android:layout_width="match_parent"
- Android:layout_height="match_parent"
- Android:layout_below="@+id/header_rl"
- Android:scrollbars="vertical"/>
- <ProgressBar
- Android:id="@+id/progress"
- Android:layout_centerInParent="true"
- Android:layout_width="wrap_content"
- Android:layout_height="wrap_content"/>
- </RelativeLayout>
上面这个布局就是包含两种, 系统自带的就是 ProgressBar, 还有就是带有包名的, 这两种解析方法是有区别的. 系统自带的用 onCreateView 方法创建 View, 带有包名的通过 createView 方法创建. 我们先看第一个:
- 7.LayoutInflater.onCreateView
- protected View onCreateView(String name, AttributeSet attrs)
- throws ClassNotFoundException {
- // 系统正常 View 要添加前缀, 比如: LinearLayout, 添加完前缀就是 Android.view.LinearLayout
- return createView(name, "android.view.", attrs);
- }
系统的视图都在 Android.view 包下, 所以要添加前缀 "android.view.", 添加完也是完整的视图名称, 就和自定义的是一样的, 最终还是调用 createView 方法:
- 8.LayoutInflater.createView
- public final View createView(String name, String prefix, AttributeSet attrs)
- throws ClassNotFoundException, InflateException {
- ...
- Class<? extends View> clazz = null;
- try {
- if (constructor == null) {
- // Class not found in the cache, see if it's real, and try to add it
- clazz = mContext.getClassLoader().loadClass(
- prefix != null ? (prefix + name) : name).asSubclass(View.class);
- ...
- constructor = clazz.getConstructor(mConstructorSignature);
- ...
- } else {
- // If we have a filter, apply it to cached constructor
- if (mFilter != null) {
- // 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);
- ...
- constructor = clazz.getConstructor(mConstructorSignature);
- ...
- } else if (allowedState.equals(Boolean.FALSE)) {
- ...
- }
- }
- }
- ...
- final View view = constructor.newInstance(args);
- ...
- return view;
- } catch (NoSuchMethodException e) {
- ...
- }
- }
这里就很简单了就是根据完整的路径名称加载出对应的 Class 文件, 然后创建对应的 Constructor 文件, 通过调用 Constructor.newInstance 创建对应的 View 对象, 这就是将 xml 文件解析成 java 对象的过程.
总结
LayoutInflate.inflate 方法很重要, 这是我们将 xml 布局解析成 java 对象的必须过程, 所以掌握这个方法的原理非常重要, 上面分析的时候也提出一些重点的内容, 所以我们再总结一下, 方便记忆:
inflate 方法的第二个参数 root 不为 null, 加载 xml 文件时根视图才有具体宽, 高属性;
inflate 方法的第三个参数 attachToRoot 是 true 时, 解析的 xml 布局会被添加到 root 上, 反之不添加;
调用两个参数的 inflate 方法时, 参数 attachToRoot = (root != null);
include 设置的宽, 高优先于 layout 指向的布局中设置的宽, 高;
include 不能是根标签;
merge 必须是根标签
include 必须有有效的 layout id
代码地址:
直接拉取导入开发工具 (Intellij idea 或者 Android studio)
https://gitlab.com/yuchuangu85/android-25
来源: https://juejin.im/post/5bfa7f2e51882539c60cdaa3