android layoutinflater 高度,从LayoutInflater.inflate看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文件来解析。

bVbsjlz?w=553&h=505

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

bVbsjlx?w=553&h=161

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

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

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

bVbsjlv?w=315&h=469

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

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

我们再来看到createViewFromTag:

bVbsjlq?w=527&h=366

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

bVbsjlm?w=553&h=167

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

bVbsjlk?w=553&h=221

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

bVbsjlj?w=553&h=275

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

setFactory2的使用方式如下:

bVbsjld?w=553&h=248

最终效果如下图所示

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

bVbsjlc?w=199&h=127

bVbsjlb?w=199&h=128

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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
java.lang.IllegalStateException: ScrollView can host only one direct child at androidx.core.widget.NestedScrollView.addView(NestedScrollView.java:507) at android.view.LayoutInflater.rInflate_Original(LayoutInflater.java:1131) at android.view.LayoutInflater_Delegate.rInflate(LayoutInflater_Delegate.java:72) at android.view.LayoutInflater.rInflate(LayoutInflater.java:1101) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1088) at android.view.LayoutInflater.rInflate_Original(LayoutInflater.java:1130) at android.view.LayoutInflater_Delegate.rInflate(LayoutInflater_Delegate.java:72) at android.view.LayoutInflater.rInflate(LayoutInflater.java:1101) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1088) at android.view.LayoutInflater.inflate(LayoutInflater.java:686) at android.view.LayoutInflater.inflate(LayoutInflater.java:505) at com.android.layoutlib.bridge.impl.RenderSessionImpl.inflate(RenderSessionImpl.java:360) at com.android.layoutlib.bridge.Bridge.createSession(Bridge.java:443) at com.android.tools.idea.layoutlib.LayoutLibrary.createSession(LayoutLibrary.java:121) at com.android.tools.idea.rendering.RenderTask.createRenderSession(RenderTask.java:722) at com.android.tools.idea.rendering.RenderTask.lambda$inflate$9(RenderTask.java:879) at com.android.tools.idea.rendering.RenderExecutor$runAsyncActionWithTimeout$3.run(RenderExecutor.kt:194) at com.android.tools.idea.rendering.RenderExecutor$PriorityRunnable.run(RenderExecutor.kt:292) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:829)
最新发布
06-06
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值