一步步带你实现简版 ButterKnife
一、项目工程介绍
- lib-annotation 是一个 Java Library 模块,主要用于自定义注解;
- lib-compiler 是一个 Java Library 模块,需要依赖 lib-annotation 模块,主要用于解析自定义注解与生成源文件。lib-compiler 还需要依赖 3 个开源库来帮助开发;
- auto-common/auto-service:为注解处理器自动生成 metadata 文件并将注解处理器 jar 文件加入构建路径,不再需要我们手动创建并更新 META-INF/services/javax.annotation.processing.Processor 文件;
- javapoet:一款 Java 代码生成框架,可以令我们省去繁琐冗杂的拼接代码的重复工作。
- lib-inject 是一个 Android Library 模块,需要依赖 lib-annotation 模块,主要用于提供 Api 给 app 模块调用;
- app 为应用模块,依赖 lib-compiler 与 lib-inject;
二、lib-annotation-自定义注解模块
创建一个自定义注解类BindView
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
-
@Target(ElementType.FIELD)
表示该注解修饰的是成员变量; -
@Retention(RetentionPolicy.CLASS)
表示该注解只会在编译时使用; -
int value()
为注解的值,这里应该传入的是一个控件 id;
三、lib-compiler-注解处理器模块
首先在build.gradle
里添加依赖
dependencies {
api project(':lib-annotation')
implementation 'com.google.auto:auto-common:0.8'
implementation 'com.google.auto.service:auto-service:1.0-rc3'
implementation 'com.squareup:javapoet:1.9.0'
}
然后创建一个类 BindViewProcessor,通过继承 AbstractProcessor 来自定义注解处理器,继承 AbstractProcessor 要实现一个抽象方法process()
public class BindViewProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
这里我们先不理会这个方法,先做一些准备工作
第一步,我们需要注册 BindViewProcessor,之前我们已经添加了 auto-service 这个库,那么注册就是一个注解的事,使用@AutoService(Processor.class)
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor { }
第二步,我们需要声明支持的 Java 版本,这里有两种方式,一种是重写getSupportedSourceVersion()
,一种是使用注解@SupportedSourceVersion()
// 重写方法
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
// 使用注解
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BindViewProcessor extends AbstractProcessor { }
SourceVersion 是一个枚举类,可以使用SourceVersion.RELEASE_0
至SourceVersion.RELEASE_8
表示各个 Java 版本,也可以直接使用SourceVersion.latestSupported()
表示最新的版本
第三步,我们需要声明自定义注解处理器要处理哪些注解,同样的,这里也有两种方式,一种是重写getSupportedAnnotationTypes()
,一种是使用注解@SupportedAnnotationTypes()
// 重写方法
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new LinkedHashSet<>();
set.add(BindView.class.getCanonicalName());
return set;
}
// 使用注解-传入注解的全类名
@SupportedAnnotationTypes({"com.fancyluo.lib_annotation.BindView"})
public class BindViewProcessor extends AbstractProcessor { }
第四步,我们需要重写init()
方法来获取一些辅助类
// 解析 Elementm 的工具类,主要用于获取包名
private Elements mElementUtils;
// 主要用于输出 Java 源文件
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mElementUtils = processingEnvironment.getElementUtils();
mFiler = processingEnvironment.getFiler();
}
第五步,这里要重新拿起之前忽略的process()
方法,这个方法是重中之重,我们要在这里面解析自定义注解和生成 Java 源文件。
先来看看我们要生成什么样的代码
public class MainActivity$$ViewBinder<T extends MainActivity>
implements ViewBinder<MainActivity> {
@Override
public void bind(final MainActivity target) {
target.btnAction=(Button)target.findViewById(2131165218);
}
}
当我们使用BindView
修饰程序元素的时候,我们的自定义注解处理器就可以拿到相应的程序元素的节点,通过解析节点,拿到相应的数据,然后自动的为这个程序元素所在的类生成一个辅助类,在里面为程序元素赋值。
也可以这么理解,我们会为使用BindView
修饰的控件所在的 Activity 自动的生成一个辅助类,在里面进行控件的findViewById
接下来的代码都是在process()
方法里,只是我将其分拆出来讲解
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
...//代码下面讲解
return false;
}
首先,我们通过 roundEnvironment 拿到所有的被BindView
修饰的节点
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
这里可以理解为一个控件,只是被封装转换成了 Element
@BindView(R.id.btnAction)
Button btnAction;
转换成 -> Element
然后遍历 elements 集合,解析数据,将我们需要的数据封装成一个类,并按照 TypeElement 来进行分组。TypeElement 可以理解为类节点,而 Element 是成员节点,再具体来说,TypeElement 就是 MainActivity,而 Element 就是其中的 btnAction;那么,按照 TypeElement 分组也就是将控件按照其所在的 Activity 进行分组。
首先创建我们需要的数据封装类BindViewInfo
public class FieldBinding {
// 可以理解为:Button 这个类型
private TypeMirror typeMirror;
// 可以理解为:成员变量名-btnAction
private String name;
// 可以理解为:Button 的 id-R.id.btnAction
private int resId;
...
}
开始遍历集合,并且将节点数据封装到 BindViewInfo,并将其分组保存到 Map 集合
// Key 为类型节点,可以理解为 MainActivity
// Value 可以理解为 MainActivity 里面所有被 BindView 注解的成员变量信息
Map<TypeElement, List<BindViewInfo>> cacheMap = new HashMap<>();
// 遍历所有被 BindView 注解的成员变量,按照 Activity 进行分组
for (Element element : elements) {
// 得到类型节点,可以理解为得到MainActivity
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// 从缓存中获取数据,如果没有,则新建并添加到缓存
List<BindViewInfo> fieldList = cacheMap.get(enclosingElement);
if (fieldList == null) {
fieldList = new ArrayList<>();
cacheMap.put(enclosingElement, fieldList);
}
// 封装被 BindView 注解的成员变量的信息
// 成员变量的类类型,例如 Button
TypeMirror typeMirror = element.asType();
// 成员变量名 例如 btnAction
String fieldName = element.getSimpleName().toString();
// 控件资源Id 例如 R.id.btn
int resId = element.getAnnotation(BindView.class).value();
BindViewInfo bindViewInfo = new BindViewInfo(typeMirror, fieldName, resId);
fieldList.add(fieldBinding);
}
将数据分好组缓存后,我们就可以来构建我们需要的 Java 源文件的代码了,前面说过,TypeElement 代表着一个Activity,而 List<BindViewInfo>
就代表着里面使用 BindView 注解修饰的控件,我们要为 Activity 生成一个辅助类,在里面为这些控件生成 findViewById
代码
首先,我们遍历 cacheMap,并解析我们需要的数据
for (Map.Entry<TypeElement, List<FieldBinding>> entry : cacheMap.entrySet()) {
List<FieldBinding> bindingList = entry.getValue();
// 如果该Activity没有被BindView注解的成员变量,则执行下一个
if (bindingList == null || bindingList.size() == 0) {
continue;
}
// 获取类型节点 例如 MainActivity
TypeElement typeElement = entry.getKey();
// 获取包名 例如 com.fancyluo.k_butterknife
String packageName = getPackageName(typeElement);
// 获取类名 例如 MainActivity
String classNameStr = getClassName(packageName, typeElement);
ClassName classNamePackage = ClassName.bestGuess(classNameStr);
// 获取ViewBinder
ClassName viewBinder = ClassName.get("com.fancyluo.lib_inject", "ViewBinder");
...//代码下面讲解
}
getPackageName(typeElement)
private String getPackageName(TypeElement enClosingElement) {
// 获取包节点
PackageElement packageElement = mElementUtils.getPackageOf(typeElement);
//返回的是 com.fancyluo.k_butterknife
return packageElement.getQualifiedName().toString();
}
getClassName(packageName, typeElement)
// 例如 com.fancyluo.k_butterknife.MainActivity
String qualifiedName = typeElement.getQualifiedName().toString();
// 例如 com.fancyluo.k_butterknife.
int length = packageName.length() + 1;
// 如果当前的TypeElement是内部类的话,裁剪掉包名和后面的点号,并将之后的点号替换为$
return qualifiedName.substring(length).replace(".", "$");
ViewBinder
是在lib_inject
模块里定义的一个接口,我们生成的辅助类需要实现这个接口并且实现接口的bind()
方法进行控件的findViewById
拿到我们需要的数据以后,就可以开始使用 javapoet 提供的 api 来构建 Java 源代码,下面,我们再来贴一下我们要生成的代码,然后我们会一步一步来构建这些代码。
public class MainActivity$$ViewBinder<T extends MainActivity>
implements ViewBinder<MainActivity> {
@Override
public void bind(final MainActivity target) {
target.btnAction=(Button)target.findViewById(2131165218);
}
}
首先,我们要构建类
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(classNameStr + "$$ViewBinder")
.addModifiers(Modifier.PUBLIC)
.addTypeVariable(TypeVariableName.get("T", classNamePackage))
.addSuperinterface(ParameterizedTypeName.get(viewBinder, classNamePackage));
- classBuilder 里传入的是类名
- addModifiers 是设置类的访问属性
- addTypeVariable 是设置类的泛型参数,传入一个 TypeVariableName,TypeVariableName 第一个参数为泛型参数名,第二个参数为 ClassName,例如
T extends MainActivity
- addSuperinterface 是设置当前类实现的接口,传入一个 ParameterizedTypeName,ParameterizedTypeName 第一个参数为父接口的 ClassName,第二个参数 ClassName,例如
ViewBinder<MainActivity>
这里就相当于构建了
public class MainActivity$$ViewBinder<T extends MainActivity>
implements ViewBinder<MainActivity> {
}
第二,我们要构建方法
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")//方法名
.addAnnotation(Override.class)//添加注解
.addModifiers(Modifier.PUBLIC)//访问属性
.returns(TypeName.VOID)// 返回值
// 添加参数:1-ClassName 2-参数名 3-参数的访问权限
.addParameter(classNamePackage, "target", Modifier.FINAL);
构建完方法的基本元素后,现在的代码结构为
public class MainActivity$$ViewBinder<T extends MainActivity>
implements ViewBinder<MainActivity> {
@Override
public void bind(final MainActivity target) {
...
}
}
最后我们来构建方法里面的具体代码,也就是相应控件的 findViewById
for (BindViewInfo bindViewInfo : bindingList) {
// 获取类型名称,例如 Button
String packageNameStr = fieldBinding.getTypeMirror().toString();
ClassName className = ClassName.bestGuess(packageNameStr);
// $L/$T代表占位符,$L为基本类型 $T为类类型
// 这里相当于生成了 target.btnAction=(Button)target.findViewById(2131165218);
methodBuilder.addStatement("target.$L=($T)target.findViewById($L)",
fieldBinding.getName(),
className,
fieldBinding.getResId());
}
方法完全构建完成后,我们将其添加到类里面
typeBuilder.addMethod(methodBuilder.build());
最后,我们通过 Filer 类来生成 Java 源文件
try {
//生成Java文件,最终写是通过filer类写出的
JavaFile.builder(packageName,result.build())
.addFileComment("auto create make")
.build()
.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
四、lib-inject-核心 Api 模块
定义一个ViewBinder
接口,之前说过,这个接口是给注解处理器自动生成的类来实现的,然后在其bind()
方法里面实现 findViewById 代码
public interface ViewBinder<T> {
void bind(T target);
}
接下来,定义一个核心类,其中的静态方法bind()
会传入要绑定的 Activity,通过这个 Activity 的类名在运行时反射获取到注解处理器生成的对应的辅助类,然后调用辅助类的bind
方法完成控件的 findViewById
public class KButterKnife {
public static void bind(Activity activity) {
String className = activity.getClass().getName();
try {
Class<?> clazz = Class.forName(className+"$$ViewBinder");
ViewBinder viewBinder = (ViewBinder) clazz.newInstance();
viewBinder.bind(activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
五、App-应用层
最后来测试使用一下,首先, 要依赖 lib-compiler 模块与 lib-inject 模块
implementation project(':lib-inject')
// lib-compiler 为注解处理器
annotationProcessor project(':lib-compiler')
然后在 Activity 里面使用
public class MainActivity extends AppCompatActivity {
@BindView(R.id.btnAction)
Button btnAction;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
KButterKnife.bind(this);
btnAction.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "注入成功,哈哈哈", Toast.LENGTH_SHORT).show();
}
});
}
}
查看生成的源文件