说起 ButterKnife 相信大多人都知道这么一个框架, 它是一个专注于 Android
系统的 View
注入框架, 简化了我们的 findViewById, OnClick, getString()
以及加载动画等操作, 给平时开发带来了很大的便利. 只是现在这个框架的作者已经不再更新了, 只会修复一些关键性的 BUG
, 同时建议使用 Google
的 view binding 了. 但是作为曾经最流行的框架之一, 还是很有必要学习和研究一下的.
众所周知 ButterKnife
的便利来自于注解. 那么既然存在注解, 注解处理器技术的使用是必然的. 现在的框架不同于以往.
以前的框架类似 XUtils
的注解很大程度上是使用反射来解析的, 反射带来性能消耗还是有的.
但是现在, 大多数的注解框架都是基于 AnnotationProcessor
的编译时解析实现的.
试想一下, 在程序编译时就完成了注解解析的工作, 又会给性能带来什么影响呢?答案当然是没影响。
(APT
已不再被作者所维护, 并且 Google
推出了AnnotationProcessor
来替代它,更是集成到了 API
中)
- 那么什么是
AnnotationProcessor
呢 ?
是一个JavaC
的工具,也就是Java
编译源码到字节码的一个预编译工具, 会在代码编译的时候调用到. 它有一个抽象类AbstractProcessor
, 只需实现该抽象类, 就可以在预编译的时候被编译器调用, 就可以在预编译的时候完成一下你想完成工作. 比如代码注入!!
那么简单来说 ButterKnife
在编译时解析注解, 通过使用 AnnotationProcessor
代码注入. 这是最基本最核心的思想. 那么今天我们也来按照这个核心思想来写一个山寨版的 ButterKnife
.
先来看一下, 我们最终需要自动生成的文件内容是什么样的.
public final class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
MainActivity_ViewBinding(MainActivity target) {
this.target = target;
target.tv_name = Utils.findViewById(target, 2131165425);
}
@Override
@CallSuper
public final void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared. ");;
target.tv_name = null;
}
}
先来分析一波:
首先自动生成的类实现了 Unbinder
接口, 并且实现了 unbind
方法.
有一个有参的构造函数,参数为 Activity
, 在构造函数内对传入的 Activity
内的控件 ID , 进行 findById
操作. 有一个 Activity
类型的变量 target
.
OK, 现在就根据上面的这些代码, 开始愉快的山寨吧.
先创建如下 Module
- 创建
App
名字为butterknife-app
, - 创建
android Module
名字为butterknife
. 为 APP 提供butterknife
绑定操作. - 创建
java Module
名字为butterknife-annotation
. 存放我们声明的注解 - 创建
java Module
名字为butterknife-compiler
. 作为我们的注解处理器.
工程目录截图如下
目录
为 APP
添加依赖
implementation project(path: ':butterknife-annotations')
implementation project(path: ':butterknife'
annotationProcessor project(path: ':butterknife-compiler')
为 butterknife-compiler
添加依赖
implementation 'com.squareup:javapoet:1.13.0'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
implementation 'com.google.auto.service:auto-service:1.0-rc7'
implementation project(path: ':butterknife-annotations')
-
javapoet
是square
推出的开源java
代码生成框架, 提供Java Api
生成.java
源文件. 这个框架功能非常有用, 我们可以很方便的使用它根据注解, 数据库模式, 协议格式等来对应生成代码. 通过这种自动化生成代码的方式, 可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作 -
auto-service 是
Google
为我们提供用于java.util.ServiceLoader
样式的服务提供者的配置/元数据生成器. 简单来说就是会为了加了@AutoService
注解的类, 自动装载和实例化,并完成模块的注入.
OK, 现在开始撸代码.
1. 编写注解
先到 butterknife-annotation module
中新建一个注解. em...既然山寨了, 那就连注解名字也一起山寨吧.
package com.butterknife_annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
原版中定义了很多注解, 什么
BindView, BindFont, BindInt, BindString, BindColor ...
扩展了很多很多, 这里我们就先写一个简单又山寨的BindView
好了.
2. 编写接口与工具类
到 butterknife module
中新建
-
Unbinder
接口, 声明一个unbind
解绑方法. 待会要让我们自动生成的java
文件实现这个接口. -
Utils
工具类, 实现我们真正的findViewById
.在注解处理器中调用. -
ButterKnife
先空着. 最后再写.
public interface Unbinder {
@UiThread
void unbind();
Unbinder EMPTY = new Unbinder() {
@Override
public void unbind() {
}
};
}
//在注解处理器重调用
public class Utils {
public static <T extends View> T findViewById(Activity activity, int id) {
return activity.findViewById(id);
}
}
3. 编写注解处理器
接下来开始到 butterknife-compiler moudle
中写我们的注解处理器.
新建一个 java
类 ButterKnifeProcessor
, 继承自 AbstractProcessor
, 并重写如下方法
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
}
-
init
方法主要做一些初始化的事情, 其中参数processingEnv
会为我们提供很多有用的工具类- 例如等下需要用到的
Filer
, 它用来生成java
类文件. -
Elements
注解处理器运行扫描源文件时, 以获取元素 (Element
)相关的信息.Element
有以下几个子类:
包 (PackageElement
), 类 (TypeElement
), 成员变量 (VariableElement
), 方法 (ExecutableElement
)
- 例如等下需要用到的
-
getSupportedSourceVersion
方法 返回当前系统支持的java
版本 -
getSupportedAnnotationTypes
该方法返回一个Set<String>
, 代表ButterKnifeProcessor
要处理的注解类的名称集合,即ButterKnife
支持的注解 -
process
敲黑板, 划重点, 这个就是最重要的方法. 在这里完成了目标类信息的收集并生成对应java
类
接着开始写下面几个简单的.
//创建文件的时候需要用到
private Filer mFiler;
private Elements mElementUtils;
//打印输出
private Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
}
//指定处理的版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annnotation : getSupportedAnnotation()) {
types.add(annnotation.getCanonicalName());
}
return types;
}
//添加所有我们需要处理的注解.
public Set<Class<? extends Annotation>> getSupportedAnnotation() {
Set<Class<? extends Annotation>> annotation = new LinkedHashSet<>();
annotation.add(BindView.class);
//原版本中会添加 N 多注解类到这里
//例如 annotation.add(BindString.class)
return annotation;
}
接下来就是最关键的 process
方法了.
OK, 现在开始生成.
/**
* 获取每个 Activity 内所有加了需要解析的注解的元素
* @param elements 我们所需要的注解集合, 因为目前我们就一个注解, 所以这里长度为 1.
* @return key 是包含我们需要解析的注解所属的 Activity, value 为当前 Activit 内所有加了要解析的注解的元素.
*/
private Map<Element, List<Element>> getAllElements(Set<? extends Element> elements){
Map<Element, List<Element>> elementMap = new LinkedHashMap<>();
for (Element element : elements) {
//来自那个 Activity=
Element enclosingElement = element.getEnclosingElement();
//以 Activity 为 Key, 先取一次,看 Map 中是否已存在
List<Element> viewBindElement = elementMap.get(enclosingElement);
if (viewBindElement == null) {
//没有存在就重新创建
viewBindElement = new ArrayList<>();
//存入到 Map 中. key 为 Activity 名字, value 为 集合
elementMap.put(enclosingElement, viewBindElement);
}
//存到集合, 同时也会更新 Map 中对应的集合
viewBindElement.add(element);
}
return elementMap;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//获取所有 Activity 中加了 bindView 注解的 元素, 需要整理为一个 Activity 对应一个自己内部的元素集合
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
Map<Element, List<Element>> elementMap = getAllElements(elements);
return false;
}
通过 roundEnv.getElementsAnnotatedWith(BindView.class)
可以拿到所有加了 @BindView
注解的控件名字, 来自哪个 Activity
. 但是一个 Activity
中可能有很多很多加了注解的控件, 那么我们需要整理成为一个 Map
, 对每个 Activity
进行归类. 因为后面,我们需要为每个 Activity
都生成一个文件.
那么接下来就需要开始遍历这个 Map
, 开始为每个 Activity
都生成一个 java
文件. 有多少个 key
就生成多少个.
for (Map.Entry<Element, List<Element>> entry : elementMap.entrySet()) {
Element enclosingElement = entry.getKey();
List<Element> viewBindElements = entry.getValue();
//获得类名
String activityClassNameStr = enclosingElement.getSimpleName().toString();
mMessager.printMessage(Diagnostic.Kind.NOTE, "------->" + activityClassNameStr);
//获取我们要实现的接口.
ClassName unbinderClassName = ClassName.get("com.butterknife", "Unbinder");
//获得对应类名的对象
ClassName activityClassName = ClassName.bestGuess(activityClassNameStr);
//开始生成类名, 继承接口对象, 以及字段
//public final class MainActivity_ViewBinding implements Unbinder, 生成出来的样子是这样的
TypeSpec.Builder classBuilder = buildClass(activityClassNameStr, unbinderClassName, activityClassName);
//生成要实现的 unbinder 方法
MethodSpec.Builder unbinderMethodBuilder = buildMethod(activityClassName);
//生成构造函数
MethodSpec.Builder constructorMethodBuilder = buildConstructor(activityClassName,viewBindElements,unbinderMethodBuilder);
//将方法添加到类中
classBuilder.addMethod(unbinderMethodBuilder.build());
//将构造函数添加到类中
classBuilder.addMethod(constructorMethodBuilder.build());
//开始生成类文件
try {
String packageName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
mMessager.printMessage(Diagnostic.Kind.NOTE, "------->" + packageName);
JavaFile.builder(packageName, classBuilder.build())
.addFileComment("自动生成")
.build().writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
过程比较简单, 就是依次生成类, 方法, 如果需要构造函数的话, 也需要生成. 最后都添加到类的构造器中. 最后生成.
下面是三个生成的方法 buildClass , buildMethod, buildConstructor
.
private TypeSpec.Builder buildClass(String activityClassNameStr, ClassName unbinderClassName, ClassName activityClassName) {
return TypeSpec.classBuilder(activityClassNameStr + "_ViewBinding")
//生成类的访问修饰符为 public final
.addModifiers(Modifier.FINAL, Modifier.PUBLIC)
//生成的类实现接口
.addSuperinterface(unbinderClassName)
//添加字段
.addField(activityClassName, "target", Modifier.PRIVATE);
}
private MethodSpec.Builder buildMethod(ClassName activityClassName) {
//生成类实现类 unbinder 方法
ClassName callSuper = ClassName.get("androidx.annotation", "CallSuper");
return MethodSpec.methodBuilder("unbind")
.addModifiers(Modifier.FINAL, Modifier.PUBLIC)
.addAnnotation(Override.class)
.addAnnotation(callSuper)
.addStatement("$T target = this.target", activityClassName)
.addStatement("if (target == null) throw new IllegalStateException(\"Bindings already cleared. \");");
}
private MethodSpec.Builder buildConstructor(ClassName activityClassName, List<Element> viewBindElements, MethodSpec.Builder unbinderMethodBuilder) {
MethodSpec.Builder constructorMethodBuilder = MethodSpec.constructorBuilder()
.addParameter(activityClassName, "target")
.addStatement("this.target = target");
for (Element viewBindElement : viewBindElements) {
//获得在 Activity 中声明的控件名字
String filedName = viewBindElement.getSimpleName().toString();
//拿到工具类的对象
ClassName utilsClassName = ClassName.get("com.butterknife", "Utils");
//拿到在 Activity 中注解中传入的参数 ID .
int resId = viewBindElement.getAnnotation(BindView.class).value();
constructorMethodBuilder.addStatement("target.$N = $T.findViewById(target, $L)", filedName, utilsClassName, resId);
//最终生成的结果如下
//target.tv_name = Utils.findViewByid(mainactivity, R.id.tv_name);
//在 unbind 方法中,将控件全赋值为 Null
unbinderMethodBuilder.addStatement("target.$N = null", filedName);
}
return constructorMethodBuilder;
}
最后一步, Activity
在使用的时候需要进行绑定. 需要传入当前 Activity
对象. 绑定的目的是什么呢? 就是根据传入的当前 Activity
然后调用生成文件的构造方法.
OK, 我们继续到 butterknife module
中的 ButterKnife.java
添加方法 bind(Activity activity)
public class ButterKnife {
public static Unbinder bind(Activity activity) {
try {
//唯一需要的反射, 反射自动生成类的构造函数
Class<? extends Unbinder> bindClassName = (Class<? extends Unbinder>) Class.forName(activity.getClass().getName() + "_ViewBinding");
//调用自动生成类的构造函数
Constructor<? extends Unbinder> bindConstructor = bindClassName.getDeclaredConstructor(activity.getClass());
Unbinder unbinder = bindConstructor.newInstance(activity);
return unbinder;
} catch (Exception e) {
e.printStackTrace();
}
return Unbinder.EMPTY;
}
}
最后在项目中 Build --> Clean Project --> Make Project
, 在我们 APP
工程的目录下就能看到自动生成的代码文件了.
迫不及待的来使用一把
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_name)
TextView mTvName;
private Unbinder mUnbinder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUnbinder = ButterKnife.bind(this);
mTvName.setText("666");
}
@Override
protected void onDestroy() {
super.onDestroy();
mUnbinder.unbind();
}
}
我这边运行成功了, 你们呢?
OK, 到这里, 是不是对整个流程有个大致的印象了呢.
- 我们在我们声明的控件上添加注释.
- 在
MainActivity
中调用ButterKnife.bind(this)
传入当前Activity
. - 编译的时候自动生成
MainActivity_ViewBinding
文件. - 运行的时候,执行
ButterKnife.bind(this)
, 在ButterKnife.bind()
方法中, 反射获取到自动生成的MainActivity_ViewBinding
文件实例, 调用自动生成文件的构造方法. 在构造方法内执行findViewById
. 这样就获取到啦.
好了, 就先山寨到这里吧. 其中还有一些没弄的, 比如加注解的控件不能声明为 private
, 比如我们拿到的 ID 不是 R.id.xxx
, 而是一堆数字的样子. 还有很多很多. 但是也算是基本完成了最核心最基础的功能. 并且也算是弄清楚了基本流程.
网友评论