乐趣区

从LayoutInflaterinflate看View的创建过程

从 LayoutInflater.inflate 看 View 的创建过程

背景:

在 Activity 中,我们通过在 onCreate 方法中调用 setContentView 传入一个 Layout 的资源 id 就可以完成布局的创建,该方法最终会调用到 PhoneWindow 的 setContentView 方法,这个方法里面有这样一行代码:

mLayoutInflater.inflate(layoutResID, mContentParent);

而如果我们要通过引入 Layout 来创建 View(比如在 Fragment 的 OnCreateView 中,或者 RecyclerView 的 onCreateViewHolder 中),我们可以看到这样一行代码:

inflater.inflate(R.layout.xxx, container, false);

所以 Android 是如何将 XML 解析成 View 的实体对象的?我们便从 inflate 方法开始探究。


LayoutInflater

将布局 XML 文件实例化为其对应的 View 对象。必须使用 android.app.Activity#getLayoutInflater()或 Context#getSystemService 来获取已连接到当前上下文已经正确配置了的 LayoutInflater 实例,就像 LayoutInflater.from(this)中的源码这样:

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;
}

所以我们有三种获取 LayoutInflater 的方式,其中通过 Activity 的 getLayoutInflater 方法最终调用到了 PhoneWindow 的相关方法,在 PhoneWindow 的构造方法中:

public PhoneWindow(Context context) {super(context);
    mLayoutInflater = LayoutInflater.from(context);
}

本质上还是通过 getSystemService 来获取。


inflate:

将指定的 XML 文件填充到 View 的层次结构中去。最终各个方法都是调用到了三个参数的重载方法

final XmlResourceParser parser = res.getLayout(resource);
return inflate(parser, root, attachToRoot);

其中 XmlResourceParser 的作用可以理解为它将 XML 格式的信息解析出来,然后传递给 inflate 方法。需要注意的是,该方法需要依赖于 Xml 的预处理(XmlResourceParser 完成),所以并不能在运行时加载 XML 文件来解析。

在第 492 行处创建了当前 XML 文件对应的根布局 temp。然后从上图源码,可以总结出 attachToRoot 参数的影响:

  • root 为 null,attchToRoot 无意义,具体 529 行,inflate 返回的是当前 XML 对应的根布局。
  • root 不为 null 且 attachToRoot 为 true,则调用 addView 将 temp 加入到 root 中,这样整个 XML 对应的布局就设置了根布局是 root,具体 523 行。
  • root 不为 null 且 attachToRoot 为 false,则在 502 行得到包含当前 XML 文件外层的 ViewGroup(root)的 layout 属性然后设置给 temp(就是当前 XML 的根布局)。

在第 515 行,调用到了 rInflateChildren 方法,该方法最终调用到了 rInflate 方法。


在 rInflate 方法中,我们需要关注这里:

其中第 857 行指的是 include 标签不能够作为 XML 的根标签存在,第 861 行指的是 merge 标签必须作为 XML 的跟标签存在,关于这两个标签的意义,简单概括就是:
include:如果一个 XML 布局回被多次引用到其它布局中,为了避免反复复制粘贴多次相同的 XML 代码,你可以使用这个标签来复用同一份 XML 代码。
merge:为了减少使用 include 的时候带来的多一层级,你可以使用这个标签作为你要引入的 XML 布局的根标签。就像下图所示一样,蓝色部分的 FrameLayout 可以被 merge 替代然后消去,这样可以减少 ViewTree 的高度,达到布局优化的目的。

回到第 863 行,这个是最关键的函数调用,正是 createViewFromTag 完成了 View 的创建。
第 866 行是对 ViewGroup 的子标签进行处理,完成子 View 的创建,然后在 867 行,将子 View 添加到 ViewGroup 中,最后 rInflate 方法会回调 parent 的 onFinishInflate 方法,以此完成这个 XML 文件中的 View 的创建过程。


我们再来看到 createViewFromTag:

我们需要关注的是两部分,771-774 行是通过 Factory 来创建 View,后面会分析。
如果 Factory 为 null,在 787 行,判断的是 name 中如果包含点符号,则表示是自定义控件,否则就是系统控件,为什么会做这一步?其中 onCreateView 最终还是调用到了 createView,如下:
createView(name, “android.view.”, attrs);

而在 createView 中,是如何创建 View 的呢?
答案是反射:

clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
...
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);

如上,无法通过 new 关键字来创建 View 对象,就只能通过反射了,而反射需要知道 Class 的全路径名,就是 packageName.xxx 这样的形式,而系统控件的 packageName 就是 android.view。这就是为何可以通过点符号来判断是不是自定义控件的原因,因为系统控件在调用 createView 的时候补全了名称。
通过反射,获取到 View 的 Constructor,然后将其 put 到一个 Map 中保存,然后在后面调用 newInstance 方法完成 View 的创建。

args[1] = attrs;
final View view = constructor.newInstance(args);

最后返回该 View 对象,这样就完成了 View 的创建。


至此,我们对通过 inflate 方法创建 View 的过程做一个总结:
XML 中保存了 ViewTree 的结构和 View 的相关标签信息(包括 View 的类型和一些属性值),然后这些信息会在后面通过反射的方式(如果没有 Factory2 和 Factory 的话)创建实例对象,如果创建的是 ViewGroup,则会对它的子 View 遍历重复创建步骤,创建完 View 对象后,会 add 到对应的 ViewGroup 中。其中相关方法的调用流程是:inflate->rInflate->createViewFromTag->createView。


Factory

在之前已经分析过了 createViewFromTag 中会先判断有没有 Factory 或者 Factory2 的对象,如果有,则调用 Factory 的 onCreateView 方法。这两个类都是借口,其中 Factory2 是 Factory 的子接口,都只有唯一一个 onCreateView 方法。不同之处在于 Factory2 的 onCreateView 方法传入了 parentView。
该方法的作用就是你可以借助它来改造 XML 中已经存在了的 Tag 的值。所以 Factory2 可以达到改造 parentView 的目的。


先从 LayoutInflate 的 setFactory2 方法说起:

setFactory 方法同这个也是一样的逻辑,这里需要注意的是第 314 行的异常,从这个异常来看,一个 LayoutInflater 是只能允许一个 Factory 存在的。即 set 方法只允许调用一次,但是我们日常写代码中并没有调用过该方法。
那么 Factory2 是被谁、在哪里被设置的呢?
在开始的时候提到过,获取 LayoutInflate 需要 Context 参数,如果我们没有在代码中显式调用 setFactory(2),那么一定是在 Context(Activity)中被设置了,看到 AppCompatActivity 中的 onCreate 方法中有这样一行:
delegate.installViewFactory();

delegate 是 AppCompatDelegate 对象,最终这个方法调用到了 AppCompatDelegateImplV9 的 installViewFactory 方法,在该方法中:
LayoutInflaterCompat.setFactory2(layoutInflater, this);

LayoutInflaterCompat 其实是一个帮助类,通过它的 set 方法完成了 Factory2 的设置。


设置完成之后,就是 mFactory2 的 onCreateView 方法了,最终会调用到 AppCompatViewInflater 的 createView 方法:
在该方法中,像 TextView,ImageView 等是通过 new AppCompatXXX 创建的,这 是为了将一些 控件变成兼容性控件(例如将 TextView 变成 AppCompatTextView)以便于向下兼容新版本中的效果,在高版本中的一些控件新特性可以在老版本中也能展示。

在 147 行,对于一般控件,走 createViewFromTag 方法,调用到了 createView 方法:

第 214 行先对 map 中的构造方法检索,看是否 map 中已经保存了对应 View 的构造方法,如果没有则通过反射获取到构造方法然后保存,225 行设置构造方法的访问权限符,最后在 226 行通过 newInstance 创建 View 实例。


setFactory2 的使用方式如下:

最终效果如下图所示
原本字体为红色,我们修改成了蓝色。


最后总结一下 Factory 相关知识:
通过 LayoutInflate 的 setFactory,我们可以为当前的 LayoutInflate 设置一个自定义的 Factory 来实现改造 View 的目的,但是要注意,该方法必须在 super.onCreate 之前调用,否则会抛出异常,而 AppCompatActivity 要 setFactory 的原因是为了兼容 UI,就像前面看到过的,通过 new AppCompatTextView 可以在早期版本系统中展示新的 TextView 的效果。

退出移动版