引言
在日常开发中,我们经常会碰到这样一个问题:在需要实现一些圆角或者多个状态视图切换等功能时,一般的做法都是将之前写的shape或者selector文件复制一份再修改。这样就导致类似重复的xml
文件越来越多,无形中增大了apk
包的大小。其实这也没什么,关键不能忍的是每次还都要为起一个新名字想半天…想想真的是繁琐之极!那么有没有一种什么办法来解决呢?
当然是有的。有大佬已经写了个开源库,可以供大家使用。本篇文章是对该库的原理全解。
原理
想要搞懂该库,首先得要搞懂LayoutInflater
的内部接口Factory
、Factory2
。(本篇源码解析基于API
26)
Factory
知识
1 | public interface Factory { |
从注释中,就能看出这是一个钩子函数,可以通过该函数获取布局中的View
名称及属性,从而对View
做一些定制操作。
再来看看Factory2
是个什么东东。
1 | public interface Factory2 extends Factory { |
Factory2
是从API
11之后引入到,从上面可以看出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,所以这里通过反射的方式解决。