Java和Kotlin的注解

注解分为运行时注解和编译时注解

区别是运行时注解只需自定义注解处理器即可,不会产生其他文件,但自定义注解处理器是通过运行时反射来工作的,所以损耗性能还是有的。

而编译时注解在编译程序时可以通过“-processor”选项在编译时指定一个Annotation处理器,该处理器实现Processor接口,通过该接口的方法来检查获取类中的注解类,你可以看一下Processor的process方法,对注解的处理就可以在这个方法中实现,而注解类就可以从该方法传入的属性中获取。

注解说明

@Retention:表示注解保留到哪个阶段。

元注解@Retention 按生命周期来划分可分为3类

  1. SOURCR:只在*.java源文件的时候有效;
  2. CLASS:注解在编译的时候会存在,被保留到class文件中,jvm加载class文件时就会擦除;
  3. RUNTIME:包含以上两种,并且运行时也会存在

这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码

使用示例:@Retention(RetentionPolicy.RUNTIME)

@Target:表示该注解可以用于什么地方。

  1. TYPE:作用于接口、类、枚举、注解;
  2. FIELD:作用于成员变量(字段、枚举的常量);
  3. METHOD:作用于方法;
  4. PARAMETER:作用于方法的参数;
  5. CONSTRUCTOR:作用于构造函数;
  6. LOCAL_VARIABLE:作用于局部变量;
  7. ANNOTATION_TYPE:作用于Annotation。
  8. PACKAGE:作用于包名;
  9. TYPE_PARAMETER:java8新增,但无法访问到;
  10. 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生成相应的代码

介绍几个关键字

  1. APT(Annotation Processor Tool)
  2. KAPT(Kotlin Annotation Processing Tool )
  3. KSP(Kotlin Symbol Processing)
  4. AbstractProcessor:java的注解解析器,在JavaPoet的包里

  5. 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

ksp的话项目中的build.gradle要改成后缀为kts的kotlin文件,build.gradle.kts,如何修改参阅https://developer.android.com/studio/build/configure-app-module?hl=zh-cn#groovy

继续使用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);
    }
}

kapt和ksp的代码没有贴,因为ksp的findViewById没有弄明白,kapt可以正常运行,完整代码入口

参考了下面的博客,多谢:

apt and kapt:编译时注解Kapt 实现基础版butterKnife - 掘金

kapt:KotlinPoet - KotlinPoet

这个在GitHub上的代码可以直接在as中的Terminal使用命令 ./gradlew build 运行,博客地址:使用 KAPT 生成 Kotlin Data Class 转换器 | The Coding Notes

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值