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

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

作者: EasonDev | 来源:发表于2017-09-22 11:29 被阅读397次

    之前写过一篇是在运行期间使用注解帮助我们减少代码量的文章,如果你对注解不太熟建议先看下之前文章:http://www.jianshu.com/p/47cc73f6b966

    因为在运行期间的注解使用反射性能是有降低的,所以很多框架使用的是编译期生成代码的方式比如还是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
    

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/xtezsxtx.html