Markdown版本笔记 | 我的GitHub首页 | 我的博客 | 我的微信 | 我的邮箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | [email protected] |
AS 注解处理器 APT ButterKnife MD
目录
实现一个轻量的 ButterKnife
APT(Annotation Processing Tool) 即注解处理器,是一种注解处理工具,用来在编译期扫描和处理注解,通过注解来生成 Java 文件
。即以注解作为桥梁,通过预先规定好的代码生成规则来自动生成 Java 文件。此类注解框架的代表有 ButterKnife、Dragger2、EventBus
等
Java API 已经提供了扫描源码并解析注解的框架,开发者可以通过继承 AbstractProcessor 类来实现自己的注解解析逻辑。APT 的原理就是在注解了某些代码元素(如字段、函数、类等)后,在编译时编译器会检查 AbstractProcessor 的子类,并且自动调用其 process() 方法,然后将添加了指定注解的所有代码元素作为参数传递给该方法,开发者再根据注解元素在编译期输出对应的 Java 代码
这里以 ButterKnife 为实现目标,在讲解 Android APT 的内容的同时,逐步实现一个轻量的控件绑定框架,即通过注解来自动生成如下所示的 findViewById() 代码
package hello.leavesc.apt;
public class MainActivityViewBinding {
public static void bind(MainActivity _mainActivity) {
_mainActivity.tvName = (android.widget.TextView) (_mainActivity.findViewById(2131165333));
_mainActivity.btnSend = (android.widget.Button) (_mainActivity.findViewById(2131165219));
_mainActivity.etName = (android.widget.EditText) (_mainActivity.findViewById(2131165246));
}
}
控件绑定的方式如下所示
@BindView(R.id.tv_name) TextView tvName;
@BindView(R.id.btn_send) Button btnSend;
@BindView(R.id.et_name) EditText etName;
建立 Module
首先在工程中新建一个 Java Library,命名为 apt_processor,用于存放 AbstractProcessor 的实现类。再新建一个 Java Library,命名为 apt_annotation ,用于存放各类注解
当中,apt_processor 需要导入如下依赖
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.auto.service:auto-service:1.0-rc2' //Google 开源的注解注册处理器
implementation 'com.squareup:javapoet:1.10.0' //square 开源的 Java 代码生成框架
implementation project(':apt_annotation') //需要依赖定义的注解
}
当中,JavaPoet 是 square 开源的 Java 代码生成框架,可以很方便地通过其提供的 API 来生成指定格式(修饰符、返回值、参数、函数体等)的代码。
auto-service 是由 Google 开源的注解注册处理器
实际上,上面两个依赖库并不是必须的,可以通过硬编码代码生成规则来替代,但还是建议使用这两个库,因为这样代码的可读性会更高,且能提高开发效率
app Module 需要依赖这两个 Java Library
implementation project(':apt_annotation') //需要依赖定义的注解
annotationProcessor project(':apt_processor') //使用apt
这样子,我们需要的所有基础依赖关系就搭建好了
编写代码生成规则
首先观察自动生成的代码,可以归纳出几点需要实现的地方:
- 1、文件和源 Activity 处在同个包名下
- 2、类名以 Activity名 + ViewBinding 组成
- 3、bind() 方法通过传入 Activity 对象来获取其声明的控件对象来对其进行实例化,这也是 ButterKnife 要求需要绑定的控件变量不能声明为 private 的原因
注解声明
BindView 注解的声明如下所示,放在 apt_annotation 中,注解值 value 用于声明 viewId
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
AbstractProcessor
在 apt_processor Module 中创建 BindViewProcessor 类并继承 AbstractProcessor 抽象类,该抽象类含有一个抽象方法 process() 以及一个非抽象方法 getSupportedAnnotationTypes() 需要由我们来实现
getSupportedAnnotationTypes() 方法用于指定该 AbstractProcessor 的目标注解对象
process() 方法则用于处理包含指定注解对象的代码元素
要自动生成 findViewById() 方法,则需要获取到控件变量的引用以及对应的 viewid,所以需要先遍历出每个 Activity 包含的所有注解对象
当中,Element 用于代表程序的一个元素,这个元素可以是:包、类、接口、变量、方法等多种概念。这里以 Activity 对象作为 Key ,通过 map 来存储不同 Activity 下的所有注解对象
获取到所有的注解对象后,就可以来构造 bind() 方法了
MethodSpec 是 JavaPoet 提供的一个概念,用于抽象出生成一个函数时需要的基础元素,直接看以下方法应该就可以很容易理解其含义了
通过 addCode() 方法把需要的参数元素填充进去,循环生成每一行 findView 方法
完整的代码声明如下所示
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> hashSet = new HashSet<>();
hashSet.add(BindView.class.getCanonicalName());
return hashSet;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment environment) {
Map<TypeElement, Map<Integer, VariableElement>> typeElementMap = getTypeElementMap(environment);
for (TypeElement key : typeElementMap.keySet()) {
Map<Integer, VariableElement> elementMap = typeElementMap.get(key);
TypeSpec typeSpec = generateCodeByPoet(key, elementMap);
String packageName = elementUtils.getPackageOf(key).getQualifiedName().toString();
JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
//以 Activity 对象作为 Key ,通过 map 来存储不同 Activity 下的所有注解对象
private Map<TypeElement, Map<Integer, VariableElement>> getTypeElementMap(RoundEnvironment environment) {
Map<TypeElement, Map<Integer, VariableElement>> typeElementMap = new HashMap<>();
Set<? extends Element> elementSet = environment.getElementsAnnotatedWith(BindView.class);
//Element 代表程序的一个元素,这个元素可以是:包、类、接口、变量、方法等,这里是获取所有包含指定注解的元素
//遍历所有包含 BindView 的注解对象
for (Element element : elementSet) {
VariableElement varElement = (VariableElement) element;//因为 BindView 的作用对象是 FIELD,因此可以直接强转
TypeElement typeElement = (TypeElement) varElement.getEnclosingElement();//返回封装此 Element 的最里层元素
//如果 Element 直接封装在另一个元素的声明中,则返回该封装元素,此处表示的即 Activity 类对象
Map<Integer, VariableElement> varElementMap = typeElementMap.get(typeElement);
if (varElementMap == null) {
varElementMap = new HashMap<>(); //看指定的key(即Activity)是否已经存在,如果不存在的话创建并添加到map中
typeElementMap.put(typeElement, varElementMap);
}
BindView bindAnnotation = varElement.getAnnotation(BindView.class); //获取注解
int viewId = bindAnnotation.value();//获取注解值
varElementMap.put(viewId, varElement);//将每个包含了 BindView 注解的字段对象以及其注解值保存起来
}
return typeElementMap;
}
/**
* 生成 Java 类,以 Activity名 + ViewBinding 进行命名
*
* @param typeElement 注解对象上层元素对象,即 Activity 对象
* @param varElementMap Activity 包含的注解对象以及注解的目标对象
*/
private TypeSpec generateCodeByPoet(TypeElement typeElement, Map<Integer, VariableElement> varElementMap) {
return TypeSpec.classBuilder(typeElement.getSimpleName().toString() + "ViewBinding")
.addModifiers(Modifier.PUBLIC)
.addMethod(generateMethodByPoet(typeElement, varElementMap))
.build();
}
/**
* 生成方法
*
* @param typeElement 注解对象上层元素对象,即 Activity 对象
* @param varElementMap Activity 包含的注解对象以及注解的目标对象
*/
private MethodSpec generateMethodByPoet(TypeElement typeElement, Map<Integer, VariableElement> varElementMap) {
ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString());
String parameter = "_" + toLowerCaseFirstChar(className.simpleName());//方法参数名
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(className, parameter);
for (int viewId : varElementMap.keySet()) {
VariableElement element = varElementMap.get(viewId);
String name = element.getSimpleName().toString();//被注解的字段名
String type = element.asType().toString();//被注解的字段的对象类型的全名称
String text = "{0}.{1}=({2})({3}.findViewById({4}));";
methodBuilder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId)));
}
return methodBuilder.build();
}
//将首字母转为小写
private static String toLowerCaseFirstChar(String text) {
if (text == null || text.length() == 0 || Character.isLowerCase(text.charAt(0))) return text;
else return String.valueOf(Character.toLowerCase(text.charAt(0))) + text.substring(1);
}
}
使用效果
首先在 MainActivity 中声明几个 BindView 注解,然后 Rebuild Project,使编译器根据 BindViewProcessor 生成我们需要的代码
Rebuild 结束后,就可以在 generatedJava 文件夹下看到 MainActivityViewBinding 类自动生成了
其源码为
package leavesc.hello.apt;
public class MainActivityViewBinding {
public static void bind(MainActivity _mainActivity) {
_mainActivity.btnSend = (android.widget.Button) (_mainActivity.findViewById(2131165218));
_mainActivity.tvName = (android.widget.TextView) (_mainActivity.findViewById(2131165327));
_mainActivity.etName = (android.widget.EditText) (_mainActivity.findViewById(2131165240));
}
}
此时有两种方式可以用来触发 bind() 方法
- 在 MainActivity 方法中直接调用 MainActivityViewBinding 的 bind() 方法
- 调用
ButterKnife.bind(this);
并通过反射来触发 MainActivityViewBinding 的 bind() 方法
public class ButterKnife {
public static void bind(Activity activity) {
Class clazz = activity.getClass();
try {
Class bindViewClass = Class.forName(clazz.getName() + "ViewBinding");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
两种方式各有优缺点。第一种方式在每次 build project 后才会生成代码,在这之前无法引用到对应的 ViewBinding 类。
第二种方式可以用固定的方法调用方式,但是相比方式一,反射会略微多消耗一些性能。
但这两种方式的运行结果是完全相同的。
2019-1-10