从LayoutInflater.inflate看View的创建过程

栏目: Android · 发布时间: 6年前

内容简介:在Activity中,我们通过在onCreate方法中调用setContentView传入一个Layout的资源id就可以完成布局的创建,该方法最终会调用到PhoneWindow的setContentView方法,这个方法里面有这样一行代码:而如果我们要通过引入Layout来创建View(比如在Fragment的OnCreateView中,或者RecyclerView的onCreateViewHolder中),我们可以看到这样一行代码:所以Android是如何将XML解析成View的实体对象的?我们便从in

从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文件来解析。

从LayoutInflater.inflate看View的创建过程

在第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方法中,我们需要关注这里:

从LayoutInflater.inflate看View的创建过程

其中第857行指的是include标签不能够作为XML的根标签存在,第861行指的是merge标签必须作为XML的跟标签存在,关于这两个标签的意义,简单概括就是:

include:如果一个XML布局回被多次引用到其它布局中,为了避免反复复制粘贴多次相同的XML代码,你可以使用这个标签来复用同一份XML代码。

merge:为了减少使用include的时候带来的多一层级,你可以使用这个标签作为你要引入的XML布局的根标签。就像下图所示一样,蓝色部分的FrameLayout可以被merge替代然后消去,这样可以减少ViewTree的高度,达到布局优化的目的。

从LayoutInflater.inflate看View的创建过程

回到第863行,这个是最关键的函数调用,正是createViewFromTag完成了View的创建。

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

我们再来看到createViewFromTag:

从LayoutInflater.inflate看View的创建过程

我们需要关注的是两部分,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方法说起:

从LayoutInflater.inflate看View的创建过程

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)以便于向下兼容新版本中的效果,在高版本中的一些控件新特性可以在老版本中也能展示。

从LayoutInflater.inflate看View的创建过程

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

从LayoutInflater.inflate看View的创建过程

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

setFactory2的使用方式如下:

从LayoutInflater.inflate看View的创建过程

最终效果如下图所示

原本字体为红色,我们修改成了蓝色。

从LayoutInflater.inflate看View的创建过程

从LayoutInflater.inflate看View的创建过程

最后总结一下Factory相关知识:

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

琢石成器

琢石成器

罗云彬 / 电子工业出版社 / 2009-6 / 89.00元

Windows环境下32位汇编语言是一种全新的编程语言。它使用与C++语言相同的API接口,不仅可以开发出大型的软件,而且是了解操作系统运行细节的最佳方式。 本书从编写应用程序的角度,从“Hello,World!”这个简单的例子开始到编写多线程、注册表和网络通信等复杂的程序,通过70多个实例逐步深入Win32汇编语言编程的方方面面。 本书作者罗云彬拥有十余年汇编语言编程经验,是汇编编程......一起来看看 《琢石成器》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具