0x01 前言
在程序开发的过程中,总会有一些场景需要去写重复冗余的代码。而程序员一般都是懒惰了(懒惰促使人进步 ^ο^ ),所以就出现了很多可以减少重复工作的框架或者工具。比如今天要分析的主角—— ButterKnife ,如果你做 Android 开发却没有听说过 ButterKnife 那就 Out 啦。ButterKnife 使用依赖注入的方式来减少程序员去编写一堆 findViewById
的代码,使用起来很方便。那么接下来就一步步地带你深入理解 ButterKnife 框架。PS:最近写的博客篇幅都有点长,请耐心阅读!Logo 图镇楼!
0x02 ButterKnife 的使用方法
我们先讲下 ButterKnife 的使用方法:
在
app/build.gradle
中添加依赖:dependencies { compile 'com.jakewharton:butterknife:8.4.0' annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0' }
在
Activity
中添加注解:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class ExampleActivity extends Activity {
@BindView(R.id.user)
EditText username;
@BindView(R.id.pass)
EditText password;
@OnClick(R.id.submit)
public void onClick(View v) {
// TODO onClick View...
}
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
使用方法非常简单,不得不赞叹 ButterKnife 实在是太方便了。彻底跟 findViewById
say goodbye 啦。但是我们也认识到,如果一个框架使用起来越简单,那么这个框架内部做的事情就越多。所以在 ButterKnife 内部一定做了很多事情。
今天我们主要分析下 ButterKnife 的三个部分:Annotation 、ButterKnifeProcessor 和 ButterKnife 。这三个部分就把整个 View 依赖注入的原理串联起来了。
准备好了吗?下面我们就一探究竟。(PS:本文分析的 ButterKnife 源码为 8.4.0 版本)
0x03 Annotation
我们先来看一下其中的注解部分。ButterKnife 的注解都在 butterknife-annotations 模块下:
发现我们平时常用的 @BindView
、@OnClick
和 @OnItemClick
都在里面。我们就挑 @BindView
(路径:butterknife-annotations/butterknife/BindView.java) 来看一下:
1 | @Retention(CLASS) |
注解都是用 @interface
来表示。在 BindView 注解的上面还有 @Retention
和 @Target
。
@Retention
:表示注解的保留时间,可选值 SOURCE(源码时),CLASS(编译时),RUNTIME(运行时),默认为 CLASS ;@Target
:表示可以用来修饰哪些程序元素,如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER 等,未标注则表示可修饰所有。
所以我们可知,@BindView
是用来修饰 field 的,并且保留至编译时刻。内部有一个默认属性 value
,用来表示 View 的 id ,即平时程序中的 R.id.xxx
。
0x04 ButterKnifeProcessor
如果只有 @BindView
是不行的,我们还需要去解析注解。如何去解析编译时的注解呢?我们可以创建一个继承自 AbstractProcessor
的注解处理器,然后实现相关方法。在 ButterKnife 中 ButterKnifeProcessor
(路径:butterknife-compiler/butterknife/compiler/ButterKnifeProcessor.java) 就是用来解析这些注解的注解处理器。
init(ProcessingEnvironment env)
我们先来看看 ButterKnifeProcessor
中的 init(ProcessingEnvironment env)
方法:
1 | @Override public synchronized void init(ProcessingEnvironment env) { |
在 init
中主要根据 env
得到一些工具类。其中的 filter
主要是用来生成 Java 代码,而 elementUtils
和 typeUtils
会在下面源码中用到。
getSupportedAnnotationTypes()
1 | private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(// |
getSupportedAnnotationTypes()
方法的作用就是返回该注解处理器所支持处理的注解集合。在 getSupportedAnnotations()
中我们可以看到一些熟悉的注解,比如 @BindView
、@OnClick
和 @OnItemClick
等。
process(Set<? extends TypeElement> elements, RoundEnvironment env)
接下来就是重头戏了,注解处理器中最重要的方法 process(Set<? extends TypeElement> elements, RoundEnvironment env)
。process(Set<? extends TypeElement> elements, RoundEnvironment env)
的代码看上去没几行,其实大部分都写在其他私有方法中了:
1 | @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) { |
总体来看 process
方法就干了两件事情:
- 扫描所有的注解,然后生成以
TypeElement
为 key ,BindingSet
为 value 的 Map ; - 根据生成的 Map ,遍历后通过 Filter 来生成对应的辅助类源码。PS:ButterKnife 使用了 JavaPoet 来生成 Java 源码。如果对 JavaPoet 不太熟悉,可以先阅读这篇文章 《javapoet——让你从重复无聊的代码中解放出来》 。
我们慢慢来看,先来分析一下 findAndParseTargets(env)
:
1 | // 扫描所有的ButterKnife注解,并且生成以TypeElement为键,BindingSet为值的HashMap |
先来看关于 BindView
的那个 for 循环,它会遍历所有被 @BindView
注解的属性,然后调用 parseBindView
方法。那么我们就先看到 findAndParseTargets
的前半段,一起跟进 parseBindView
的方法中去。
1 | private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap, |
在 parseBindView
方法中基本上都加了注释,在方法的开头会对该 element
去做校验。如果校验没通过的话,就没有下面代码的什么事了。若校验通过之后,生成该 element
所在的类元素对应的 builder ,builder 中添加相应的 Field 绑定信息,最后添加到待 unbind 的序列中去。
现在,我们回过头来看看 findAndParseTargets(env)
方法的后半段:
1 | private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) { |
在 findAndParseTargets(env)
方法的后半段中,主要就是把之前的 builderMap
转换为了 bindingMap
并返回。
到了这里,我们把 process(Set<? extends TypeElement> elements, RoundEnvironment env)
做的第一件事情搞清楚了,下面就接着来看第二件事情了。
1 | // 遍历 bindingMap 并且通过 Filer 生成 Java 代码 |
brewJava(int sdk)
从上面可以看到,遍历了之前得到的 bindingMap
,然后利用 binding
中的信息生成相应的 Java 源码。所以在 binding.brewJava(sdk)
这个方法是我们重点关注对象。那么就进入 BindingSet
(路径:butterknife-compiler/butterknife/compiler/BindingSet.java) 这个类中去看看吧:
1 | JavaFile brewJava(int sdk) { |
brewJava(int sdk)
方法的代码竟然这么短 O_o ,就是利用了 JavaFile.builder
生成了一个 JavaFile
对象而已。但是我们发现其中有一个 createType(int sdk)
方法,隐隐约约感觉一定是这个方法在搞大事情。继续跟进去看:
1 | private TypeSpec createType(int sdk) { |
在 createType(int sdk)
方法中,基本构建好了一个类的大概,其中对于构造器以及类似 findViewById
的操作都是在 createBindingConstructor(targetTypeName, sdk)
中实现:
1 | private MethodSpec createBindingConstructor(TypeName targetType, int sdk) { |
通过上面的代码就生成了构造器,但是我们还是没有看到具体 findViewById
操作的代码。别急,这些代码都在 addViewBinding(constructor, binding)
里会看到:
1 | private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) { |
至此,整个 ButterKnifeProcessor
解析注解、生成 Java 代码的流程就走完了。我们来看看生成的代码到底长成什么样子:
1 | public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder { |
不得不赞叹一句,JavaPoet 生成的代码跟我们手写的基本上没什么区别。JavaPoet 实在是太强大了 *^ο^* 。
0x05 ButterKnife
bind()
通过之前介绍 ButterKnife 的使用方法,我们知道 View 绑定是通过调用 ButterKnife.bind()
方法来实现的。下面我们来看看其内部原理 (路径:butterknife/butterknife/ButterKnife.java) :
1 | @NonNull @UiThread |
createBinding(@NonNull Object target, @NonNull View source)
发现 bind()
方法内都会去调用 createBinding(@NonNull Object target, @NonNull View source)
:
1 | private static Unbinder createBinding(@NonNull Object target, @NonNull View source) { |
其实 createBinding(@NonNull Object target, @NonNull View source)
方法做的事情就是根据 target
创建对应的 targetClassName_ViewBinding
。在 targetClassName_ViewBinding
的构造器中会把对应的 View 进行绑定(具体可以查看上面的 MainActivity_ViewBinding
)。而在 findBindingConstructorForClass(Class<?> cls)
方法中也使用了 Class.forName()
反射来查找 Class
,这也是无法避免的。但是仅限于一个类的第一次查找,之后都会从 BINDINGS
缓存中获取。
0x06 总结
总体来说,ButterKnife 是一款十分优秀的依赖注入框架,方便,高效,减少代码量。最重要的是解放程序员的双手,再也不用去写无聊乏味的 findViewById
了 \(╯-╰)/ 。与 ButterKnife 原理相似的,还有 androidannotations 框架。感兴趣的同学可以自己研究一下。那么,今天的 ButterKnife 解析到这里就结束了。如果对此有问题或疑惑的同学可以留言,欢迎探讨。
Goodbye !~~