最近在研究注解,就看了下apt及butterknife的源码,还看了一些文章,见文末。自己手写了一遍,才算稍微搞明白了点东西。写下笔记,记录一下,免得忘记。
注解的介绍可参考之前的一篇文章 Java注解Annotation 简单总结及应用 - 掘金 (juejin.cn)
1. 环境
Android studio : Dolphin | 2021.3.1 Patch 1 Build #AI-213.7172.25.2113.9123335, built on September 30, 2022
gradle:gradle-7.0.2-bin
2. 反射实现
最早的butterknife是通过反射实现的,我也学着自己搞一下。
2.1 首先创建自定义注解类MyInjectView
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInjectView {
int value();
}
2.2 创建工具类InjectUtil
public class InjectUtil {
private final static String TAG = InjectUtil.class.getSimpleName();
public static void inject(Activity activity) {
Class<? extends Activity> aClass = activity.getClass();
Field[] declaredFields = aClass.getDeclaredFields();
for (Field field : declaredFields) {
if (field.isAnnotationPresent(MyInjectView.class)) {//是否有指定的注解
//获取注解对象
MyInjectView myInjectView = field.getAnnotation(MyInjectView.class);
//获取注解的值
int value = myInjectView.value();
//非public属性或方法需要设置否则无法访问
field.setAccessible(true);
try {
//通过反射调用
Method findViewById = aClass.getMethod("findViewById", int.class);
findViewById.setAccessible(true);
Object invoke = findViewById.invoke(activity, value);
//给filed变量设值
field.set(activity, invoke);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
其中通过反射调用findViewById
的三行代码,也可以替换为View view = activity.findViewById(id)
;然后field.set(activity, view)
。
2.3 在activity中加入注解
public class MainActivity extends Activity {
@MyInjectView(R.id.ip_tv)
public TextView ipTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
InjectUtil.inject(this);
ipTv.setText("你成功了");
}
}
到这里,就用反射技术实现了butterknife的功能,当然,这是丐版。
3. apt实现
需要创建注解、注解处理器和工具类三个模块。必须创建三个模块:一是因为注解处理器模块的接口只有java library才能引用的到;二是放一起时打包会出错。
3.1 注解模块myannotation
该模块是java library,即在使用Android studio创建module时,选择Java or kotlin library
,用来放自定义注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInjectView {
int value();
}
3.2 注解处理器模块myprocessor
该模块也是Java library,用来放注解处理器,并且需要添加依赖:
dependencies {
implementation project(':myannotation')
// java代码生成框架
implementation 'com.squareup:javapoet:1.13.0'
//引入autoservice,自动在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件,并在该文件中写入注解处理器的全称,包括包路径;
implementation 'com.google.auto.service:auto-service:1.0-rc4'
//因为gradle版本问题,需要使用annotationProcessor添加依赖,否则该文件无法生成
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
}
继承抽象类AbstractProcessor
,重写注解处理器:
// @SupportedAnnotationTypes({"com.aya.myannotation.MyInjectView"}) 注解表示哪些注解需要注解处理器处理,可以多个注解校验。也可通过重写父类方法实现
//@SupportedSourceVersion(SourceVersion.RELEASE_7) //注解 用于指定jdk使用版本。也可通过重写父类方法实现
@AutoService(Processor.class) //添加注解,自动在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件,并在该文件中写入注解处理器的全称,包括包路径
public class MyBindingProcessor extends AbstractProcessor {
private Filer mFiler;//过滤器
private Messager mMessager; //打印日志
private JavacTrees mTrees; //java语法树
private TreeMaker mTreeMaker; //创建或修改方法的AST变量
private Names mNames; //标识符,获取变量使用
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mFiler = processingEnvironment.getFiler();
mMessager = processingEnvironment.getMessager();
mTrees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
mTreeMaker = TreeMaker.instance(context);
mNames = Names.instance(context);
mMessager.printMessage(Diagnostic.Kind.NOTE, "*********init*******");
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "********process********");
//获取全部的类
for (Element element : roundEnvironment.getRootElements()) {
//获取类的包名
String pkgName = element.getEnclosingElement().toString();
//获取类的名字
String clsName = element.getSimpleName().toString();
//构建新的类的名字:原类名 + _MyViewBinding
ClassName className = ClassName.get(pkgName, clsName + "_MyViewBinding");
mMessager.printMessage(Diagnostic.Kind.NOTE, "pkgName:" + pkgName + ", clsName:" + clsName);
//构建新的类的构造方法
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(pkgName, clsName), "activity");
//判断是否要生成新的类,假如该类里面有 MyBindView 注解,那么就不需要新生成
boolean hasBuild = false;
//获取类的元素,例如类的成员变量、方法、内部类等
for (Element enclosedElement : element.getEnclosedElements()) {
//仅获取成员变量
if (enclosedElement.getKind() == ElementKind.FIELD) {
MyInjectView bindView = enclosedElement.getAnnotation(MyInjectView.class);
//判断是否被 MyInjectView 注解
if (bindView != null) {
hasBuild = true;
//在构造方法中加入代码
constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
enclosedElement.getSimpleName(), bindView.value());
}
}
}
//添加一个测试方法
MethodSpec testMethod = MethodSpec
.methodBuilder("test")//方法名
.addModifiers(Modifier.PUBLIC)//修饰符,public、private、static、final等
.returns(void.class)//返回值
.addParameter(String.class, "data")//添加参数
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//添加方法题,具体执行代码
.build();
//是否需要生成
if (hasBuild) {
try {
//构建新的类
TypeSpec builtClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addMethod(constructorBuilder.build())
.addMethod(testMethod)
.build();
//生成 Java 文件
JavaFile javaFile = JavaFile
.builder(pkgName, builtClass)
.addFileComment("This class is generated automatically by apt!")
.build();
//写文件
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 返回true:表示当前注解已经处理;返回false:可能需要后续的 processor 来处理
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(MyInjectView.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
3.3 注解工具类模块mybindview
该模块是Android模块,即在使用Android studio创建module时,选择Android library
。
- 首先添加依赖:
api project(':myannotation')
- 将
InjectUtil
工具类移至该模块下,并添加方法injectApt
public static void injectApt(Activity activity) {
try {
//获取"当前的activity类名+_MyViewBinding"的class对象
Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "_MyViewBinding");
//获取class对象的构造方法,该构造方法的参数为当前的activity对象
Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass());
//调用构造方法
constructor.newInstance(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
到这里,三个模块都已经创建好了,代码也都有注释,不再多做解释。
3.4 使用
在app模块里加入依赖,注意添加依赖的方式:
implementation project(':mybindview')
annotationProcessor project(':myprocessor')
在MainActivity中使用:
public class MainActivity extends Activity {
@MyInjectView(R.id.ip_tv)
public TextView ipTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
InjectUtil.injectApt(this);
ipTv.setText("你成功了");
}
}
为了验证,在as中可以选择Build
---Rebuild project
,然后在Build Output
中可以看到MyBindingProcessor
中的打印。
运行app,就可以看到效果啦。
3.4 Javapoet addStatement占位符介绍:
$T
是类型替换,一般用于("$T foo", List.class)
=> List foo
. $T
。好处是JavaPoet会自动添加文件开头的import
。如果直接写("List foo")
虽然也能生成List foo
,但是最终的java文件会缺少import java.util.List
。
$L
是字面量替换,比如("abc$L123", "FOO")
=> abcFOO123
。也就是直接替换。
$S
是字符串替换,比如:("$S.length()", "foo")
=> "foo".length()
。注意$S
是将参数替换为了一个带双引号的字符串。
$N
是名称替换,比如你之前定义了一个函数MethodSpec methodSpec = MethodSpec.methodBuilder("foo").build();
。现在你可以通过N", methodSpec) =>
foo`。
4. apt使用扩展
为了可以在多个activity中使用多个注解,我修改了MyBindingProcessor
的代码,同时定义了一个新的注解MyInjectView2
,可用于在编译器修改控件一些属性比如setText
等。代码如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInjectView2 {
int id();
String value();
}
MyBindingProcessor
的代码:
// @SupportedAnnotationTypes({"com.aya.myannotation.MyInjectView"}) 注解表示哪些注解需要注解处理器处理,可以多个注解校验。也可通过重写父类方法实现
//@SupportedSourceVersion(SourceVersion.RELEASE_7) //注解 用于指定jdk使用版本。也可通过重写父类方法实现
@AutoService(Processor.class)
//添加注解,自动在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件,并在该文件中写入注解处理器的全称,包括包路径
public class MyBindingProcessor extends AbstractProcessor {
private static HashMap<ClassName, MethodSpec.Builder> clsMap = new HashMap<>();
private static boolean hasMadeJavaFile = false;//process方法会被调用多次,如果重复创建Java会报错
private Filer mFiler;//过滤器
private Messager mMessager; //打印日志
private JavacTrees mTrees; //java语法树
private TreeMaker mTreeMaker; //创建或修改方法的AST变量
private Names mNames; //标识符,获取变量使用
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mFiler = processingEnvironment.getFiler();
mMessager = processingEnvironment.getMessager();
mTrees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
mTreeMaker = TreeMaker.instance(context);
mNames = Names.instance(context);
mMessager.printMessage(Diagnostic.Kind.NOTE, "*********init*******");
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
for (TypeElement typeElement : set) {
//这里可以获取所有支持的注解类型
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(typeElement);
mMessager.printMessage(Diagnostic.Kind.NOTE, "********: typeElement=" + typeElement + ", elements=" + elements);
}
handleMyInjectView(roundEnvironment.getElementsAnnotatedWith(MyInjectView.class));
handleMyInjectView2(roundEnvironment.getElementsAnnotatedWith(MyInjectView2.class));
makeJavaFile();
// 返回true:表示当前注解已经处理;返回false:可能需要后续的 processor 来处理
return false;
}
private void handleMyInjectView2(Set<? extends Element> elements) {
if (elements.isEmpty()) {
return;
}
// 遍历所有被注解的 Element
for (Element element : elements) {
VariableElement variableElement = (VariableElement) element;
// 获得它所在的类
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
//获取类的包名
String pkgName = classElement.getEnclosingElement().toString();
//获取类的名字
String oldClsNameStr = classElement.getSimpleName().toString();
mMessager.printMessage(Diagnostic.Kind.NOTE, "********process********: pkgName =" + pkgName + ", clsName=" + oldClsNameStr + ", element=" + element);
//构建新的类的名字:原类名 + _MyViewBinding
ClassName newClassName = ClassName.get(pkgName, oldClsNameStr + "_MyViewBinding");
mMessager.printMessage(Diagnostic.Kind.NOTE, "############ new class pkgName =" + newClassName.packageName() + ", clsName=" + newClassName.simpleName() + ", className=" + newClassName);
MyInjectView2 bindView2 = variableElement.getAnnotation(MyInjectView2.class);
if (bindView2 != null) {
MethodSpec.Builder constructorBuilder = null;
if (clsMap.get(newClassName) != null) {
//构建新的类的构造方法
constructorBuilder = clsMap.get(newClassName);
} else {
constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(pkgName, oldClsNameStr), "activity");
clsMap.put(newClassName, constructorBuilder);
}
//在构造方法中加入代码
constructorBuilder.addStatement("activity.$N = activity.findViewById($L)", variableElement.getSimpleName(), bindView2.id())
.addStatement("activity.$N.setText($S)", variableElement.getSimpleName(), bindView2.value());
}
}
}
private void handleMyInjectView(Set<? extends Element> elements) {
if (elements.isEmpty()) {
return;
}
// 遍历所有被注解的 Element
for (Element element : elements) {
VariableElement variableElement = (VariableElement) element;
// 获得它所在的类
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
//获取类的包名
String pkgName = classElement.getEnclosingElement().toString();
//获取类的名字
String oldClsNameStr = classElement.getSimpleName().toString();
mMessager.printMessage(Diagnostic.Kind.NOTE, "********process********: pkgName =" + pkgName + ", clsName=" + oldClsNameStr + ", element=" + element);
//构建新的类的名字:原类名 + _MyViewBinding
ClassName newClassName = ClassName.get(pkgName, oldClsNameStr + "_MyViewBinding");
mMessager.printMessage(Diagnostic.Kind.NOTE, "############ new class pkgName =" + newClassName.packageName() + ", clsName=" + newClassName.simpleName() + ", className=" + newClassName);
MyInjectView bindView = variableElement.getAnnotation(MyInjectView.class);
if (bindView != null) {
MethodSpec.Builder constructorBuilder = null;
if (clsMap.get(newClassName) != null) {
//构建新的类的构造方法
constructorBuilder = clsMap.get(newClassName);
} else {
constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(pkgName, oldClsNameStr), "activity");
clsMap.put(newClassName, constructorBuilder);
}
//在构造方法中加入代码
constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
variableElement.getSimpleName(), bindView.value());
}
}
}
private void makeJavaFile() {
if (!hasMadeJavaFile) {
hasMadeJavaFile = true;
//统一生成java类
for (ClassName className : clsMap.keySet()) {
try {
//构建新的类
TypeSpec builtClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addMethod(clsMap.get(className).build())
.build();
mMessager.printMessage(Diagnostic.Kind.NOTE, "*****start make java file***:" + className);
//生成 Java 文件
JavaFile javaFile = JavaFile
.builder(className.packageName(), builtClass)
.addFileComment("This class is generated automatically by apt!")
.build();
//写文件
javaFile.writeTo(mFiler);
mMessager.printMessage(Diagnostic.Kind.NOTE, "*****end make java file***");
} catch (IOException e) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "*****make java file fail ***:" + e);
e.printStackTrace();
}
}
}
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(MyInjectView.class.getCanonicalName());
annotations.add(MyInjectView2.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
5.小结
AnnotationProcessor技术,可以将运行期的部分工作提前到编译器,这样用户在使用app时就可以更加顺畅了。
注解的学习到这里就告一段落。
参考文章:
ButterKnife 原理解析 - 掘金 (juejin.cn)
注解处理器(Annotation Processor)简析 - 掘金 (juejin.cn)