引言
在日常开发中,我们经常会碰到这样一个问题:在需要实现一些圆角或者多个状态视图切换等功能时,一般的做法都是将之前写的shape或者selector文件复制一份再修改。这样就导致类似重复的xml文件越来越多,无形中增大了apk包的大小。其实这也没什么,关键不能忍的是每次还都要为起一个新名字想半天…想想真的是繁琐之极!那么有没有一种什么办法来解决呢?
当然是有的。有大佬已经写了个开源库,可以供大家使用。本篇文章是对该库的原理全解。
原理
想要搞懂该库,首先得要搞懂LayoutInflater的内部接口Factory、Factory2。(本篇源码解析基于API26)
Factory知识
1 | public interface Factory { |
从注释中,就能看出这是一个钩子函数,可以通过该函数获取布局中的View名称及属性,从而对View做一些定制操作。
再来看看Factory2是个什么东东。
1 | public interface Factory2 extends Factory { |
Factory2是从API11之后引入到,从上面可以看出Factory2继承自Factory,并且声明了一个重载函数,参数中多了一个当前控件的父控件。
下面来看下LayoutInflater.Factory是如何被调用的。
setContentView流程
首先,从我们最常见的Activity的setContentView开始。
1 | public void setContentView(@LayoutRes int layoutResID) { |
调用到Window的setContentView方法,Window是个抽象类,所以这里是到它的实现类PhoneWindow中看。
1 |
|
再往下看就到了LayoutInflater中的inflate函数
1 | public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { |
继续
1 | public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { |
继续
1 | public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { |
这段代码比较长,首先看如果根节点是merge标签,会调用rInflate函数
1 | void rInflate(XmlPullParser parser, View parent, Context context, |
从上面的代码可以看出,createViewFromTag是来负责创建View对象!!!
再回过头来来看第二部分,也就i是inflate函数中不是merge标签的情况,也就是根节点是个View
1 | final View temp = createViewFromTag(root, name, inflaterContext, attrs); |
从这里再次认证了上面的结论:createViewFromTag是创建View对象的最核心函数!!!
1 | private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) { |
继续
1 | View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, |
从这段代码中,我们找到了Factory的用处,可以看出这里存在Factory、Factory2、mPrivateFactory,这些Factory可以通过LayoutInflater来设置。
1 | public void setFactory(Factory factory) { |
其中mPrivateFactory是为了框架层用的。Factory和Factory2只能设置一次,不然就报IllegalStateException
也就是说,如果设置了Factory,就可以提前通过Factory解析name创建View,如果Factory不存在或者Factory创建的View为空,就会使用默认的解析方式创建。
其实,对于对于android.app.Activity来说,mFactory2、mFactory、mPrivateFactory这三个对象为 null或者空实现,所以在解析创建View时会使用默认的方式创建。
下面看看默认的解析方式。
1 | //原生控件的解析方式 根据name中是否有· 如果没有则是原生 |
可以看到是调用了onCreateView,而在LayoutInflater的实现类 PhoneLayoutInflater中覆写了该函数。
1 | * |
可以看出,这里对于系统内置的 View,会依次在 View 的标签前面加上android.widget.、android.webkit.,android.app. 、android.view. 然后通过createView()方法创建 View。
自定义控件的解析方式,也是通过调用createView来创建的。
1 | if (-1 == name.indexOf('.')) { |
下面来看看createView方法
1 | public final View createView(String name, String prefix, AttributeSet attrs) |
从这里可以看出,View默认创建方式是通过反射实现的!
总结:通过梳理了从xml到View的创建流程后,我们发现原来Factory接口是系统给开发者留下了后门,通过它可以自己提前对标签解析,进而做一些额外的操作。那么具体怎么用呢?首先看下AppCompat是如何做到向下兼容的。明白这个后,再看该库就会全部明白了。
AppCompat的向下兼容
平时的开发中,我们的BaseActivity一般都会继承AppCompatActivity,然后你会发现当我们在布局文件中明明声明的是TextView,在运行时却变成了AppCompatTextView。
这其实就是用到了Factory实现的。
1 |
|
看下getDelegate
1 |
|
调用AppCompatDelegate的静态方法创建mDelegate对象
往下追
1 | public static AppCompatDelegate create(Dialog dialog, AppCompatCallback callback) { |
可以看到这里根据不同的API Level创建不同的AppCompatDelegate对象,最低支持到API 9

通过该图,我们发现类似这种 AppCompatDelegateImplVxx 的类,都是高版本的继承低版本的。
接着看下AppCompatDelegate中的installViewFactory()方法,点进去发现是个抽象方法,继续追,看到AppCompatDelegateImplV9中实现了该方法。
1 |
|
往下追LayoutInflaterCompat.setFactory2
1 | public static void setFactory2( |
可以看到这里最终调用到了LayoutInflater中的setFactory2方法。也就是说AppCompatDelegateImplV9自己实现了Factory2接口,并设置到了与当前Context相关联的LayoutInflater中,通过前面setContentView流程中,我们知道如果页面设置了Factory,则布局文件中的标签会交由该Factory解析。所以,在解析过程中,一定会走到AppCompatDelegateImplV9的onCreateView方法!!!
1 |
|
这里有两个,我们知道一个是兼容Factory接口的。
继续往下追。
1 |
|
再看AppCompatViewInflater的createView方法
1 | public final View createView(View parent, final String name, @NonNull Context c |
看到这里,我们才发现原来是在这里将TextView替换成了AppCompatTextView!
结论:AppCompat组件实现兼容的原理就是通过Factory2接口实现的。另外,如果我们要自己再次添加额外的操作时,必须要调用到AppCompatDelegate的createView方法,否则V7库将不能发挥作用!
BackgroundLibrary库解析
前面啰嗦了那么多,终于到正题啦!
先来看下该库的用法,在BaseActivity中的super.onCreate之前调用一行代码就可以实现了。
1 | BackgroundLibrary.inject(this); |
往下追,看看里面做了什么?
1 | public static LayoutInflater inject(Context context) { |
首先是根据不同的Context类型获取到LayoutInflater,然后创建了BackgroundFactory实例。
看下BackgroundFactory是什么。
1 | public class BackgroundFactory implements LayoutInflater.Factory2 { |
原来是个实现了Factory2接口的类,内部有两个成员变量mViewCreateFactory,mViewCreateFactory2,分别对应Factory接口和Factory2接口,并且提供了外部传值的方法。
再回头看上面,下一步判断如果当前传入的Context是AppCompatActivity,就调用BackgroundFactory的setInterceptFactory2方法,并回调AppCompatDelegate的createView方法,最后将BackgroundFactory设置到了inflater中。
由此可知,原来该库同V7库向下兼容的方法是一致的,都是通过Factory2接口做文章的。另外由前文知道,V7库想要发挥作用最终是通过AppCompatDelegate的createView方法,并且Factory2接口只能设置一次!所以此处就必须要回调该方法!
看下BackgroundFactory实现的两个方法。
1 |
|
可以看到Factory2接口声明的方法调用了Factory接口声明的方法,所以最终核心是在
1 | public View onCreateView(String name, Context context, AttributeSet attrs) { |
该方法中。
首先是回调了一开始设置的mViewCreateFactory接口里的方法,继而会回调到V7库中的方法,让V7库先做向下兼容。
接着获取了一系列自定义的属性,包括shape、selector、ripple等
1 | <declare-styleable name="background"> |
如果没有获取的我们自己声明的属性,就返回由V7创建的View。如果获取到了,再次判断V7创建的View是否为空,如果为空,将会由库自己重新去创建View。
看下createViewFromTag方法
1 | private View createViewFromTag(Context context, String name, AttributeSet attrs) { |
是不是感到很熟悉呢?
没错,这就是前文讲到从xml到View流程中系统默认创建View的源码。由前文所知,在反射创建View之前,首先会交由Factory、Factory2这两个后门去创建。当它们两个没有创建出来时,再交由系统默认去反射创建。
所以,库已经给Context设置了Factory2接口,那么在xml解析的流程中,会先交由库的BackgroundFactory去解析,接着由于要兼容V7,又交由V7库去创建。但V7也只是对上面我们提到的某些需要兼容的View创建,所以必然有些View是会为空的。但我们这时又必须要对View添加额外的属性,所以必须要提提前创建View。
再往下看,创建完View后,就开始解析属性,根据属性创建对应的Drawable,这里都是对SDK API的应用,没什么可说的。
到这里,库的最核心原理已经全部剖析完毕!
最后,我们再看下库还提供了使用方法。
1 | public static LayoutInflater inject2(Context context) { |
可以看到,这里是为了兼容其它根据Factory原理实现某些特定功能的库(如换肤)。由前文所知,Factory只能设置一次,会将mFactorySet设置为true,所以这里通过反射的方式解决。