编译期注解生成java文件及kotlin使用注意点

之前写过一篇是在运行期间使用注解帮助我们减少代码量的文章,如果你对注解不太熟建议先看下之前文章:链接地址
因为在运行期间的注解使用反射性能是有降低的,所以很多框架使用的是编译期生成代码的方式比如还是Butterknife(怎么老是它,谁让它最经典用的人最多呢)在后期的版本中使用的就是编译期生成代码的方式。
我会尽量用最少的文字更多的代码来表达。最主要的是梳理逻辑(因为看网上很多说这样那样看的一头污水)。其实逻辑想清楚了就那么回事了。不逼逼 了。先来说下流程吧。因为编译期的方式在流程上稍复杂些但也别怕,我来给你先捊捊一共就三步(不懂无所谓有个印象)咱们再开始。 github:https://github.com/EasonHolmes/AnnotationDemo

  • 创建注解并标示@Target及@Retention

  • 创建继承自AbstractProcessor的类用来收集注解的值及生成java源代码

  • 使用反射实例化生成的源代码文件

    #第一步创建注解类

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
  int value();
}
复制代码

注解就不解释了这一步就这么简单

#第二步创建类继承自AbstractProcessor

@AutoService(Processor.class)//生成 META-INF 信息;
@SupportedSourceVersion(SourceVersion.RELEASE_7)//声明支持的源码版本
@SupportedAnnotationTypes({"com.cui.libannotation.BindView"})//指定要处理的注解路径
//声明 Processor 处理的注解,注意这是一个数组,表示可以处理多个注解;
public class ViewInjectProcessor extends AbstractProcessor {
    // 存放同一个Class下的所有注解
    Map<String, List<VariableInfo>> classMap = new HashMap<>();
    // 存放Class对应的TypeElement
    Map<String, TypeElement> classTypeElement = new HashMap<>();

    private Filer filer;
    Elements elementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnv.getFiler();
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        collectInfo(roundEnvironment);
        writeToFile();
        return true;
    }

    void collectInfo(RoundEnvironment roundEnvironment) {
        classMap.clear();
        classTypeElement.clear();

        //获取所有使用到bindView的类
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for (Element element : elements) {
            //获取bindvView注解的值
            int viewId = element.getAnnotation(BindView.class).value();

            //代表被注解的元素 variableElemet是element的子类
            VariableElement variableElement = (VariableElement) element;

            //被注解元素所在的class
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            //class的完整路径
            String classFullName = typeElement.getQualifiedName().toString();

            // 收集Class中所有被注解的元素
            List<VariableInfo> variablist = classMap.get(classFullName);
            if (variablist == null) {
                variablist = new ArrayList<>();

                VariableInfo variableInfo = new VariableInfo();
                variableInfo.setVariableElement(variableElement);
                variableInfo.setViewId(viewId);
                variablist.add(variableInfo);

                classMap.put(classFullName, variablist);

                //保存class对应要素(完整路径,typeElement)
                classTypeElement.put(classFullName, typeElement);
            }
        }
    }

    /**
     * http://blog.csdn.net/crazy1235/article/details/51876192 javapoet使用 用来生成java文件源代码的
     * 底下创建java源代码的可以看这个链接
     */
    void writeToFile() {
        try {
            for (String classFullName : classMap.keySet()) {
                TypeElement typeElement = classTypeElement.get(classFullName);

                //使用构造函数绑定数据 创建一个构造函数public类型添加参数(参数类型全路径如Bundle android.os.Bundle,"参数名")
                MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(ParameterSpec.builder(TypeName.get(typeElement.asType()), "activity").build());

                List<VariableInfo> variableList = classMap.get(classFullName);
                for (VariableInfo variableInfo : variableList) {
                    VariableElement variableElement = variableInfo.getVariableElement();
                    // 变量名称(比如:TextView tv 的 tv)
                    String variableName = variableElement.getSimpleName().toString();
                    // 变量类型的完整类路径(比如:android.widget.TextView)
                    String variableFullName = variableElement.asType().toString();
                    // 在构造方法中增加赋值语句,例如:activity.tv = (android.widget.TextView)activity.findViewById(215334);
                    constructor.addStatement("activity.$L=($L)activity.findViewById($L)", variableName, variableFullName, variableInfo.getViewId());
                }

                //创建class
                TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName() + "$$ViewInjector")
                        .addModifiers(Modifier.PUBLIC)
                        .addMethod(constructor.build())
                        .build();

                //与目标class放在同一个包下,解决class属性的可访问性
                String packageFullname = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
                JavaFile javaFile = JavaFile.builder(packageFullname, typeSpec).build();
                //生成 class文件
                javaFile.writeTo(filer);

            }
        } catch (Exception e) {

        }
    }
}
复制代码

其中VariableInfo就是一个简单的实体类。而@AutoService(Processor.class) 注解是用来生成 META-INF 信息 MethodSpec TypeSpec JavaFile就是我们生成java源代码文件需要用的相关类 这两个需要依赖两个库

 implementation 'com.google.auto.service:auto-service:1.0-rc3'//用于自动为 JAVA Processor 生成 META-INF 信息。
 implementation 'com.squareup:javapoet:1.8.0'//封装了一套生成 .java 源文件的 API
复制代码
public class VariableInfo {
    int viewId;
    VariableElement variableElement;

    public VariableElement getVariableElement() {
        return variableElement;
    }

    public void setVariableElement(VariableElement variableElement) {
        this.variableElement = variableElement;
    }

    public int getViewId() {
        return viewId;
    }

    public void setViewId(int viewId) {
        this.viewId = viewId;
    }
}
复制代码

collectInfo方法就是用来收集使用了BindView注解的相关信息,writeToFile方法就是用来创建java文件的。类中的每一步注释都写的很清楚有不懂的可以@我。

#第三步创建通过反射实例化生成的java类

public class InjectHelper {
    public static void inject(Activity host) {
        //获得 View 所在 Activity 的类路径,然后拼接一个字符串“$$ViewInjector”。
        // 这个是编译时动态生成的 Class 的完整路径,也就是我们需要实现的,同时也是最关键的部分;
        String classFullName = host.getClass().getName() + "$$ViewInjector";
        try {
//            根据 Class 路径,使用 Class.forName(classFullName) 生成 Class 对象;
            Class proxy = Class.forName(classFullName);
//            得到 Class 的构造函数 constructor 对象
            Constructor constructor = proxy.getConstructor(host.getClass());
//            使用 constructor.newInstance(host) new 出一个对象,这会执行对象的构造方法,方法内部是我们为 MainActivity 的 tv 赋值的地方。
            constructor.newInstance(host);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码

第三步完成rebuild一下你就可以在build/generated/source/kapt/debug 下看到java源文件了

到重点了kotlin在使用上是和java有一些差别

依赖上

关键字annotationProcessor改为kapt这是在官方文档上也有说明

修饰上

比如我们这里的@Target(ElementType.FIELD)在使用时要加lateinit否则在生成的java源代码类中被注解的textview会被认定为private无法获取

@BindView(R.id.tv)
lateinit var textview: TextView
复制代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值