Android:Java 注解解析与 APT
什么是 APT
APT:Annotation Process Tools。注解处理工具。
APT 特点
- 与 Javac 一样,apt 是拿来操作 Java 源文件的,而不是编译后的类
- apt 会处理完源文件后编译它们
- apt 处理源文件过程中如果产生新的源文件,再新一轮检查中新新的源文件的注解也会被处理,直到没有新的源文件产生之后,再编译所有源文件
- apt 能将多个注解处理器组合在一起
- 还能添加监听器,方便一轮处理结束时收到通知
发展
- APT 最早提及是在 JDK 1.5,注解也是这个时候支持的,APT 作为补充性 API 存在:Mirror API
- JDK 1.6 开始,写入编译器 javac 中,相关包:javax.annotation.processing
应用
非常出门的应用框架就是 Butterknife 黄油刀,使用注解绑定控件 id 和控件,省略了 findViewById 的绑定控件的重复代码的编写,提供效率
模仿 Butterknife 写个例子
虽然没有读过 ButterKnife 的源码,但是根据使用加上 APT 的知识应该可以大致模仿写个简单的工具:
1. 模仿 Butterknife 的依赖创建两个 Java Lib
两个Java Library:只有 Java Library 可以使用 JDK 的 javax.annotation.processing
- buildCompiler:注解解析器
- buildLib:注解,工具类
2. 把 Java Lib 添加到 app 模块的依赖上
// build.gradle (app)
annotationProcessor project(':bindCompiler')
implementation project(':bindLib')
同时,把 bindLib 添加到 bindCompiler 的依赖上:
// build.gradle (bindCompiler)
implementation project(':bindLib')
3. bindLib 添加注解,添加工具类
注解:作用到 View 的成员变量,并且生成代码后就不需要了,因此:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
int value();
}
工具类:
- 所有 Activity 的总入口,每个 Activity 都调用此类,传入 Activity 实例,有了实例,就能执行 Activity#findViewById 的操作
- 根据之前 Butterknife 使用 apt 生成的代码,实际上是为每个 Activity 和 Fragment 都生成了一个工具类,例如 MainActivity_ViewBinding,来做 Activity#findViewById 的操作
- 因此,工具类里面要无差别调用所有使用 apt 生成的工具类代码。那如何无差别?让所有工具类继承统一接口,工具类调用接口的实例
// bindLib
public class BindLib {
public static void bind(Object activity) {
String canonicalName = activity.getClass().getCanonicalName();
Class<?> utilClass = null;
try {
utilClass = Class.forName(canonicalName + "_Binder");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if (utilClass != null) {
try {
Object o = utilClass.newInstance();
if (o instanceof IBinder) {
((IBinder) o).bind(activity);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
}
这里,每个 Activity 的生成的工具类为 Activity 全称 + “_Binder”,例如 MainActvity_Binder,并且继承了 IBinder 接口:
public interface IBinder {
void bind(Object o);
}
4. bindCompiler 添加 annotationProcessor 生成工具代码
思路:
- 获取所有注解的元素
- 获取所有元素所在的 Activity 类元素
- 基于 “Activity 类元素:带注解的 View 元素列表” 的键值对形式储存
- 遍历 “Activity 类元素” 创建 “类名_Binder” 的工具类,要求实现 IBinder 接口,并在实现方法里面做 findViewById 操作
public class BindViewProcessor extends AbstractProcessor {
private static class ViewInfo {
public ViewInfo(int id, String viewName) {
this.id = id;
this.viewName = viewName;
}
public int id;
public String viewName;
@Override
public boolean equals(Object o) {
return this.viewName.equals(((ViewInfo) o).viewName);
}
}
private Map<TypeElement, Set<ViewInfo>> mMap = new HashMap<>();
private Filer mFiler;
private Elements mElementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mElementUtils = processingEnvironment.getElementUtils();
mFiler = processingEnvironment.getFiler();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new HashSet<>();
set.add(BindView.class.getCanonicalName());
return set;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
for (Element element : elements) {
if (element instanceof VariableElement) {
// get className which contains element annotated by @BindView
Element enclosingElement = element.getEnclosingElement();
if (enclosingElement instanceof TypeElement) {
Set<ViewInfo> viewInfoSet = mMap.get(enclosingElement);
if (viewInfoSet == null) {
viewInfoSet = new HashSet<>();
}
BindView annotation = element.getAnnotation(BindView.class);
String viewName = element.getSimpleName().toString();
viewInfoSet.add(new ViewInfo(annotation.value(), viewName));
mMap.put((TypeElement) enclosingElement, viewInfoSet);
}
}
}
Set<TypeElement> typeElementSet = mMap.keySet();
for (TypeElement element : typeElementSet) {
Set<ViewInfo> viewInfoSet = mMap.get(element);
String code = generateUtilClassCode(element, viewInfoSet);
String utilClassName = element.getSimpleName().toString() + "_Binder";
try {
JavaFileObject sourceFile = mFiler.createSourceFile(utilClassName, element);
Writer writer = sourceFile.openWriter();
writer.write(code);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private String generateUtilClassCode(TypeElement element, Set<ViewInfo> viewInfoSet) {
String className = element.getSimpleName().toString();
PackageElement packageOf = mElementUtils.getPackageOf(element);
String packageName = packageOf.getQualifiedName().toString();
String utilClassName = className + "_Binder";
StringBuilder builder = new StringBuilder();
builder.append("package ").append(packageName).append(";").append("\n")
.append("import com.john.bindlib.IBinder;").append("\n")
.append("public class ").append(utilClassName).append(" implements IBinder ").append("{")
.append("\n")
.append("\t").append("@Override").append("\n")
.append("\t").append("public void bind(Object o) {").append("\n")
.append("\t\t").append(className).append(" act = (").append(className).append(") o;")
.append("\n");
for (ViewInfo viewInfo : viewInfoSet) {
builder.append("\t\t").append("act.").append(viewInfo.viewName).append(" = ")
.append("act.findViewById(").append(viewInfo.id).append(");").append("\n");
}
builder.append("\t}").append("\n")
.append("}");
return builder.toString();
}
}
其中 process() & generateUtilClassCode() 为核心代码
补充 Elements 对应关系:
- ExecutableElement:成员方法
- PackageElement:包
- TypeElement:类,接口
- TypeParameterElement:带泛型参数的类和接口
- VariableElement:成员变量
5. 注册 annotationProcessor
注册 annotationProcessor 是固定步骤,Google 提供了 AutoService 框架帮处理这个工作,但是由于简单固定,这里手工添加也是可以的:
buildCompiler 模块:新建路径和文件
// 建路径和文件
src/main/resources/META-INF/services/javax.annotation.processing.Processor
// 内容为完整处理器名称
com.john.bindcompiler.BindViewProcessor
6. 运行查看生成的源文件
新生成的源代码路径:app/build/generated/ap_generated_sources/debug/out
package com.john.newtest;
import com.john.bindlib.IBinder;
public class MainActivity_Binder implements IBinder {
@Override
public void bind(Object o) {
MainActivity act = (MainActivity) o;
act.mTextView2 = act.findViewById(2131165354);
act.mTextView = act.findViewById(2131165353);
}
}
// Activity 源文件:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.textView)
TextView mTextView;
@BindView(R.id.textView2)
TextView mTextView2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindLib.bind(this);
mTextView.setText("Hello Text");
mTextView2.setText("Hello Text 2");
}
}
调试
- 打开 Android Studio 内置的 Terminal 输入:
./gradlew --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac
- 创建远程调试配置:
- Edit Configurations..
- 点击 ‘+’ 创建 Remote,输入名字,确认下端口是不被占用的,一般默认 5005 就 ok
- Apply + OK
- 代码上设置好断点位置,点击 Debug 按钮,等待一会儿
补充
Update in 2020-04-09