注解分为运行时注解和编译时注解
区别是运行时注解只需自定义注解处理器即可,不会产生其他文件,但自定义注解处理器是通过运行时反射来工作的,所以损耗性能还是有的。
而编译时注解在编译程序时可以通过“-processor”选项在编译时指定一个Annotation处理器,该处理器实现Processor接口,通过该接口的方法来检查获取类中的注解类,你可以看一下Processor的process方法,对注解的处理就可以在这个方法中实现,而注解类就可以从该方法传入的属性中获取。
注解说明
@Retention:表示注解保留到哪个阶段。
元注解@Retention 按生命周期来划分可分为3类
- SOURCR:只在*.java源文件的时候有效;
- CLASS:注解在编译的时候会存在,被保留到class文件中,jvm加载class文件时就会擦除;
- RUNTIME:包含以上两种,并且运行时也会存在
这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码
使用示例:@Retention(RetentionPolicy.RUNTIME)
@Target:表示该注解可以用于什么地方。
- TYPE:作用于接口、类、枚举、注解;
- FIELD:作用于成员变量(字段、枚举的常量);
- METHOD:作用于方法;
- PARAMETER:作用于方法的参数;
- CONSTRUCTOR:作用于构造函数;
- LOCAL_VARIABLE:作用于局部变量;
- ANNOTATION_TYPE:作用于Annotation。
- PACKAGE:作用于包名;
- TYPE_PARAMETER:java8新增,但无法访问到;
- TYPE_USE:java8新增,但无法访问到;
使用示例:@Target(ElementType.FIELD)
@Documented:表示将注解包含在 Javadoc 中。
@Inherite:表示允许子类继承父类中的注解。
运行时注解
运行时注解比较简单,下面准备了一个findViewById的例子;
使用
在Activity的成员位置:@FindViewById(R.id.tv) public TextView tv;
在setContentView后面调用:AnnotationParse.parse(this);方法,上面写的tv就不在是null了
代码实现:定义注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FindViewById {
// 使用value命名,则使用的时候可以忽略,否则使用时就得把参数名加上
int value();
}
定义注解解析器
public class AnnotationParse {
/**
* 解析
* @param target 解析目标
*/
public static void parse(final Activity target) {
try {
Class<?> clazz = target.getClass();
Field[] fields = clazz.getDeclaredFields(); // 获取所有的字段
FindViewById byId;
SetOnClickListener clkListener;
View view;
String name;
String methodName;
int id;
for (Field field : fields) {
Annotation[] annotations = field.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof FindViewById) {
byId = field.getAnnotation(FindViewById.class); // 获取FindViewById对象
field.setAccessible(true); // 反射访问私有成员,必须进行此操作
name = field.getName(); // 字段名
id = byId.value();
// 查找对象
view = target.findViewById(id);
if (view != null) field.set(target, view);
else throw new Exception("Cannot find.View name is " + name + ".");
} else if (annotation instanceof SetOnClickListener) { // 设置点击方法
clkListener = field.getAnnotation(SetOnClickListener.class);
field.setAccessible(true);
// 获取变量
id = clkListener.id();
methodName = clkListener.methodName();
name = field.getName();
view = (View) field.get(target);
if (view == null) { // 如果对象为空,则重新查找对象
view = target.findViewById(id);
if (view != null) field.set(target, view);
else throw new Exception("Cannot find.View name is " + name + ".");
}
// 设置执行方法
Method[] methods = clazz.getDeclaredMethods();
boolean isFind = false;
for (final Method method : methods)
if (method.getName().equals(methodName)) {
isFind = true;
view.setOnClickListener(v -> {
try {
method.invoke(target);
} catch (Exception e) {
e.printStackTrace();
}
});
break;
}
if (!isFind) // 没有找到
throw new Exception("Cannot find.Method name is " + methodName + ".");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
编译时注解
apt+javaPoet
编译时注解的工作过程是,通过注解定义,使用注解解析器(AbstractProcessor或者SymbolProcessor)解析注解,明白你要干什么,然后使用JavaPoet或KotlinPoet生成相应的代码
介绍几个关键字
- APT(Annotation Processor Tool)
- KAPT(Kotlin Annotation Processing Tool )
- KSP(Kotlin Symbol Processing)
- AbstractProcessor:java的注解解析器,在JavaPoet的包里
- SymbolProcesso:ksp,2021出的kotlin解析器,在KotlinPoet的包里
APT(Annotation Processor Tool),即注解处理器。APT是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解,一个注解的注解处理器,以java代码(或者编译过的字节码)作为输入,生成.java文件作为输出,核心是交给自己定义的处理器去处理。注解处理器,即 annotation processor tool,简称apt
KAPT(Kotlin Annotation Processing Tool )kotlin注解处理工具,就是使用KotlinPoet生成kotlin代码,还是使用的Java的AbstractProcessor注解解析器
KSP(Kotlin Symbol Processing)在KAPT的基础上添加了SymbolProcesso解析器,功能上讲跟apt和kapt完全相同,都是代码生成的玩意,据说ksp比kapt的效率提升了百分之好几十,我没测试,但既然是新出的解析器,效率提升应该是没有问题的,但是这玩意没法断点调试,反正我是没弄明白,因为不能调试,英文文档看起来又好累,并且最近没有相关需求,所以下面偷懒只写了Java的apt
继续使用FindViewById作为例子:
一:定义Java Model,名字为annotation,注解的部分
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
@IdRes int value();
}
这里定义的注解,Activity和JavaPoet生产类时都会用,所以单独定义了一个Java model,因为生产类涉及到类名,避免字符串写错,所以定义一个名字的字段,我demo中放到了一个接口里
public interface BindingSuffix {
String GENERATED_CLASS_SUFFIX = "$Binding";
}
二:定义Java Model,名字为compiler, javapoet的部分
单独定义创建一个Java model
1:导包:
- implementation 'com.squareup:javapoet:1.11.1’//引入javapoet
- implementation project(path: ':annotation’)//因为类名在这里定义的,所以引入
2:META-INF注册:
在编译时,java编译器(javac)会去META-INF中查找实现了的AbstractProcessor的子类,并且调用该类的process方法,最终生成.java文件。其实就像activity需要注册一样,就是要到META-INF注册 ,javac才知道要给你调用哪个类来处理注解
按照下方图片红框中的样子创建文件,文件名字按照图片中写,别写错了,文件内容是com.javapoet.compiler.Processor,就是Java文件夹下Processor类的路径
3:对应注解的处理器需要继承AbstractProcessor类
- init方法,会被注解处理工具调用
- process方法,这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
- getSupportedSourceVersion方法,指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6。
- getSupportedAnnotationTypes方法,这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称,即注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
public class Processor extends AbstractProcessor {
private Filer filer;
private Messager messager;
private Elements elementUtils;
//每个存在注解的类整理出来,key:package_classname value:被注解的类型元素
private Map<String, List<Element>> annotationClassMap = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
buildAnnotatedElement(roundEnv, BindView.class);
} else {
for (Map.Entry<String, List<Element>> entry : annotationClassMap.entrySet()) {
String packageName = entry.getKey().split("_")[0];//调用处的路径
String typeName = entry.getKey().split("_")[1];//调用处名字
ClassName className = ClassName.get(packageName, typeName);//根据路径和名字获取类名
ClassName classNameSuffix = ClassName.get(packageName,//获取带生产类的名字
typeName + BindingSuffix.GENERATED_CLASS_SUFFIX);
TypeSpec.Builder classBuilder = createClass(classNameSuffix);//创建类
createConstructor(classBuilder, className);//添加构造函数
MethodSpec.Builder bindViewsMethodBuilder =//添加bindViews方法
createBindViewsMethod(className);
createMethodBody(entry, bindViewsMethodBuilder);//增加方法体
classBuilder.addMethod(bindViewsMethodBuilder.build());//将构建出来的方法添加到类里面
try {//将类写入文件中
JavaFile.builder(packageName,classBuilder.build())
.build().writeTo(filer);
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.ERROR, e.toString());
}
}
}
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return new TreeSet<>(Arrays.asList(BindView.class.getCanonicalName()));
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
private void buildAnnotatedElement(RoundEnvironment roundEnv, Class<? extends Annotation> clazz) {
for (Element element : roundEnv.getElementsAnnotatedWith(clazz)) {
String className = getFullClassName(element);
List<Element> cacheElements = annotationClassMap.get(className);
if (cacheElements == null) {
cacheElements = new ArrayList<>();
annotationClassMap.put(className, cacheElements);
}
cacheElements.add(element);
}
}
private String getFullClassName(Element element) {
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
String packageName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
return packageName + "_" + typeElement.getSimpleName().toString();
}
/* 创建要生成的类,如下所示
public class MainActivity$Binding {}
*/
private TypeSpec.Builder createClass(ClassName className) {
return TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC);
}
/* 创建构造函数构造函数
public MainActivity$Binding(MainActivity activity) {
bindViews(activity);
} */
private void createConstructor(TypeSpec.Builder classBuilder, ClassName className) {
classBuilder.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(className, "activity")
.addStatement("$N($N)", "bindViews", "activity")
.build());
}
/* 创建方法bindViews(MainActivity activity)
private void bindViews(MainActivity activity) {}
*/
private MethodSpec.Builder createBindViewsMethod(ClassName className) {
return MethodSpec.methodBuilder("bindViews")
.addModifiers(Modifier.PRIVATE)
.returns(void.class)
.addParameter(className, "activity");
}
/* 增加方法体
activity.tvHello = (TextView)activity.findViewById(2131165326);
*/
private void createMethodBody(Map.Entry<String, List<Element>> entry, MethodSpec.Builder bindViewsMethodBuilder) {
for (VariableElement variableElement : ElementFilter.fieldsIn(entry.getValue())) {
BindView bindView = variableElement.getAnnotation(BindView.class);
if (bindView != null) {
bindViewsMethodBuilder.addStatement("$N.$N = ($T)$N.findViewById($L)",
"activity",
variableElement.getSimpleName(),
variableElement,
"activity",
bindView.value());
}
}
}
}
三:创建Binding类,供activity使用时调用
public class Binding {
private Binding() { }
private static <T extends Activity> void instantiateBinder(T target, String suffix) {
Class<?> targetClass = target.getClass();
String className = targetClass.getName();
try {
Class<?> bindingClass = targetClass
.getClassLoader()
.loadClass(className + suffix);
Constructor<?> classConstructor = bindingClass.getConstructor(targetClass);
try {
classConstructor.newInstance(target);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + classConstructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + classConstructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create instance.", cause);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unable to find Class for " + className + suffix, e);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find constructor for " + className + suffix, e);
}
}
public static <T extends Activity> void bind(T activity) {
instantiateBinder(activity, BindingSuffix.GENERATED_CLASS_SUFFIX);
}
}
四:使用
Activity的成员位置写@BindView(R.id.tv_hello)TextView tvHello;
setContentView方法后面写Binding.bind(this);,就完成了调用,tvHello就不在是空值了
五:调试
1:创建调试的Remote
点击加号,创建Remote JVM Debug
3下面的use module classpath的选项别忘了改,你要调试哪个module就选那个
在项目中的gradle.properties文件中添加以下代码
org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=30025
org.gradle.jvmargs=后面的值就是图片中第三步复制的内容
2:设置gradle和Java
在Android studio的Terminal使用命令gradle --daemon开启守护线程,关闭守护线程的命令:gradle --stop(需要环境变量,没配的话配一下吧)
因为当时电脑使用的Java版本是1.8,而Android Studio使用的是11,所以开启失败了,Android studio中使用的gradle版本在7.0以后就必须使用11,1.8就会失败,所以我把as中的gradle降级到7.0以下了,as使用的Java也改成了1.8,再次使用命令gradle --daemon开启守护线程成功
开启成功后,按照图片中样式把model切换到前面新建立的Remote JVM Debug的aaa下,点击红框的调试,成功后,在把model这里直接切换回应用的model,run即可调试了,别忘了打断点,因为是类生产,不能修改已经存在的Java类(即不能向已有的类中添加方法),所以每次还是clean一下,否则没有新代码的话,不会从新生成类,就不会进入点断
生成的类的位置,可能会因为版本和配置不同,位置不同,可以通过类搜索搜索一下
public class MainActivity$Binding {
public MainActivity$Binding(MainActivity activity) {
bindViews(activity);
}
private void bindViews(MainActivity activity) {
activity.tvHello = (TextView) activity.findViewById(2131231128);
}
}