从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的效果。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。