# 概述
* 编写代码时对目标元素进行注解
* 编译时注解处理器会扫描注解,并通过 JavaPoet 自动生成 Java 文件
* 调用 ButterKnife.bind()方法时,会通过反射拿到之前为目标类生成的 TargetClassName_ViewBinding 类,并调用其构造方法,完成绑定
# 具体分析
## 源码组成
ButterKnife 的源码主要的组成部分包括三个:butterknife、butterknife-annotations、butterknife-compiler。其中 butterknife-annotations 声明了所有的注解;butterknife-compiler 为注解处理器,在编译阶段对注解进行处理;butterknife 则是我们调用的接口。接下来我们一一来看。
## butterknife-annotations
ButterKnife 提供了如下注解:
* 绑定类型的注解:BindAnim、BindArray、BindBitmap、BindBool、BindColor、BindDimen、BindDrawable、BindFloat、BindFont、BindInt、BindString、BindView、BindViews
* 监听类型注解:OnCheckedChanged、OnClick、OnEditorAction、OnFocusChange、OnItemClick、OnItemLongClick、OnItemSelected、OnLongClick、OnPageChange、OnTextChanged、OnTouch、Optional
## butterknife-compiler
复习下在 EventBus 源码分析时用到的注解处理器:
注解处理器用来在编译时扫描和处理注解,会自动生成 .java 文件。
每一个注解处理器都继承于 AbstractProcessor,
```java
package com.example;
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}
```
其中必需的几个方法如下:
* init(ProcessingEnvironment env):会被外部的工具调用,传入 ProcessingEnvironment 参数,我们可以从其中获取到一些工具类
* process():在这里扫描、处理注解以及生成代码
* getSupportedAnnotationsTypes():在这里定义本注解处理器会注册到哪些注解上
* getSupportedSourceVersion():指定 Java 版本
那么首先来看看在 butterknife-compiler 中的关键类:ButterknifeProcessor。
```java
public final class ButterKnifeProcessor extends AbstractProcessor {
// 初始化工具
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
...
elementUtils = env.getElementUtils();
typeUtils = env.getTypeUtils();
filer = env.getFiler();
}
// 定义所有注解
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
// 在这里看到了前面声明的所有注解
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindColor.class);
annotations.add(BindView.class);
...
annotations.addAll(LISTENERS);
return annotations;
}
// 对注解进行处理
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {}
}
```
### process 方法
接下来我们重点看看 process 方法,也就是注解处理器是如何对注解进行操作的。
```java
// 对注解进行处理
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
// 扫描注解并解析,得到 Map
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
// 生成 Java 文件
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
```
通过参数 RoundEmviroment 我们可以拿到所有包含特定注解的被注解元素,生成一个 Key 为 TypeElement,Value 为 BindingSet 的 Map;接着遍历 Map 生成相应的 Java 文件。
先来看看扫描注解并解析的源码:
```java
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
...
// Process each @BindAnim element.
// 遍历所有被注解的元素
for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
try {
parseResourceAnimation(element, builderMap, erasedTargetNames);
} catch (Exception e) {
...
}
}
...
// Process each annotation that corresponds to a listener.
for (Class<? extends Annotation> listener : LISTENERS) {
findAndParseListener(env, listener, builderMap, erasedTargetNames);
}
// 把之前的 builderMap 转换为 bindingMap
...
while (!entries.isEmpty()) {
Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();
TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();
TypeElement parentType = findParentType(type, erasedTargetNames);
if (parentType == null) {
bindingMap.put(type, builder.build());
} else {
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
builder.setParent(parentBinding);
bindingMap.put(type, builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
}
return bindingMap;
}
```
中间省略了一些注解的处理,每种注解处理方式基本一致,以 BindAnim 为例,其中的关键方法为 parseResourceAnimation,其他注解的该方法分别对应着(parseBindView、parseResourceString 等)
```java
private void parseResourceAnimation(Element element,
...
// Assemble information on the field.
BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
builder.addResource(new FieldAnimationBinding(getId(qualifiedId), name));
erasedTargetNames.add(enclosingElement);
}
```
parseResourceAnimation 的作用为生成被注解元素所对应类的 Builder,并得到 builderMap。
接下来看看生成 Java 文件的代码:
```java
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
```
这里使用 JavaPoet 生成了代码。
源码分析未完待续!
来看看 JavaPoet 为我们生成的 Java 文件(示例中 MainActivity 中有两个点击事件绑定):
```java
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view2131230756;
private View view2131230758;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
view = Utils.findRequiredView(source, R.id.btn_alipay, "method 'onViewClicked'");
view2131230756 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onViewClicked(p0);
}
});
view = Utils.findRequiredView(source, R.id.btn_custom_view, "method 'onViewClicked'");
view2131230758 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onViewClicked(p0);
}
});
}
@Override
@CallSuper
public void unbind() {
if (target == null) throw new IllegalStateException("Bindings already cleared.");
target = null;
view2131230756.setOnClickListener(null);
view2131230756 = null;
view2131230758.setOnClickListener(null);
view2131230758 = null;
}
}
```
在构造方法中,可以看到我们熟悉的点击事件,View 的绑定同理。
## butterknife
在 ButterKnife 的标准用法中,会在 Activity 或 Fragment 的 onCreate 方法中调用 `ButterKnife.bind(this);` 进行绑定,我们就首先从 bind 方法入手。
```java
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
...
}
}
```
可以看到,通过反射首先拿到 target 的类名,然后找到其对应的构造器,接着创建相应的对象。其中的 findBindingConstructorForClass 方法源码如下:
```java
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
return bindingCtor;
}
String clsName = cls.getName();
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
} catch (ClassNotFoundException e) {
} catch (NoSuchMethodException e) {
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
```
可以看到,根据类名拿到 targetClassName_ViewBinding 类的构造器,并调用其构造方法,进行绑定
# 参考文档
[ButterKnife 源码分析](https://www.jianshu.com/p/1c449c1b0fa2)
- 导读
- Java知识
- Java基本程序设计结构
- 【基础知识】Java基础
- 【源码分析】Okio
- 【源码分析】深入理解i++和++i
- 【专题分析】JVM与GC
- 【面试清单】Java基本程序设计结构
- 对象与类
- 【基础知识】对象与类
- 【专题分析】Java类加载过程
- 【面试清单】对象与类
- 泛型
- 【基础知识】泛型
- 【面试清单】泛型
- 集合
- 【基础知识】集合
- 【源码分析】SparseArray
- 【面试清单】集合
- 多线程
- 【基础知识】多线程
- 【源码分析】ThreadPoolExecutor源码分析
- 【专题分析】volatile关键字
- 【面试清单】多线程
- Java新特性
- 【专题分析】Lambda表达式
- 【专题分析】注解
- 【面试清单】Java新特性
- Effective Java笔记
- Android知识
- Activity
- 【基础知识】Activity
- 【专题分析】运行时权限
- 【专题分析】使用Intent打开三方应用
- 【源码分析】Activity的工作过程
- 【面试清单】Activity
- 架构组件
- 【专题分析】MVC、MVP与MVVM
- 【专题分析】数据绑定
- 【面试清单】架构组件
- 界面
- 【专题分析】自定义View
- 【专题分析】ImageView的ScaleType属性
- 【专题分析】ConstraintLayout 使用
- 【专题分析】搞懂点九图
- 【专题分析】Adapter
- 【源码分析】LayoutInflater
- 【源码分析】ViewStub
- 【源码分析】View三大流程
- 【源码分析】触摸事件分发机制
- 【源码分析】按键事件分发机制
- 【源码分析】Android窗口机制
- 【面试清单】界面
- 动画和过渡
- 【基础知识】动画和过渡
- 【面试清单】动画和过渡
- 图片和图形
- 【专题分析】图片加载
- 【面试清单】图片和图形
- 后台任务
- 应用数据和文件
- 基于网络的内容
- 多线程与多进程
- 【基础知识】多线程与多进程
- 【源码分析】Handler
- 【源码分析】AsyncTask
- 【专题分析】Service
- 【源码分析】Parcelable
- 【专题分析】Binder
- 【源码分析】Messenger
- 【面试清单】多线程与多进程
- 应用优化
- 【专题分析】布局优化
- 【专题分析】绘制优化
- 【专题分析】内存优化
- 【专题分析】启动优化
- 【专题分析】电池优化
- 【专题分析】包大小优化
- 【面试清单】应用优化
- Android新特性
- 【专题分析】状态栏、ActionBar和导航栏
- 【专题分析】应用图标、通知栏适配
- 【专题分析】Android新版本重要变更
- 【专题分析】唯一标识符的最佳做法
- 开源库源码分析
- 【源码分析】BaseRecyclerViewAdapterHelper
- 【源码分析】ButterKnife
- 【源码分析】Dagger2
- 【源码分析】EventBus3(一)
- 【源码分析】EventBus3(二)
- 【源码分析】Glide
- 【源码分析】OkHttp
- 【源码分析】Retrofit
- 其他知识
- Flutter
- 原生开发与跨平台开发
- 整体归纳
- 状态及状态管理
- 零碎知识点
- 添加Flutter到现有应用
- Git知识
- Git命令
- .gitignore文件
- 设计模式
- 创建型模式
- 结构型模式
- 行为型模式
- RxJava
- 基础
- Linux知识
- 环境变量
- Linux命令
- ADB命令
- 算法
- 常见数据结构及实现
- 数组
- 排序算法
- 链表
- 二叉树
- 栈和队列
- 算法时间复杂度
- 常见算法思想
- 其他技术
- 正则表达式
- 编码格式
- HTTP与HTTPS
- 【面试清单】其他知识
- 开发归纳
- Android零碎问题
- 其他零碎问题
- 开发思路