zoukankan      html  css  js  c++  java
  • APT

    前言

    关于注解的基础知识,可以参考另一篇随笔——注解 ,这里不再复述。

    注解的保留时间分为三种:

    • SOURCE——只在源代码中保留,编译器将代码编译成字节码文件后就会丢掉
    • CLASS——保留到字节码文件中,但Java虚拟机将class文件加载到内存是不一定在内存中保留
    • RUNTIME——一直保留到运行时

    通常我们使用后两种,因为SOURCE主要起到标记方便理解的作用,无法对代码逻辑提供有效的信息。

     时间解析性能影响
    RUNTIME 运行时 反射
    CLASS 编译期 APT+JavaPoet

    如上图,对比两种解析方式:

    • 运行时注解比较简单易懂,可以运用反射技术在程序运行时获取指定的注解信息,因为用到反射,所以性能会收到一定影响。
    • 编译期注解可以使用APT(Annotation Processing Tool)技术,在编译期扫描和解析注解,并结合JavaPoet技术生成新的java文件,是一种更优雅的解析注解的方式,不会对程序性能产生太大影响。

    下面以BindView为例,介绍两种方式的不同使用方法。


    运行时注解

    运行时注解主要通过反射进行解析,代码运行过程中,通过反射我们可以知道哪些属性、方法使用了该注解,并且可以获取注解中的参数,做一些我们想做的事情。

    首先,新建一个注解

    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface BindViewTo {
        int value() default -1; //需要绑定的view id
    }

    然后,新建一个注解解析工具类AnnotationTools,和一般的反射用法并无不同:

    public class AnnotationTools {
    
        public static void bindAllAnnotationView(Activity activity) {
            //获得成员变量
            Field[] fields = activity.getClass().getDeclaredFields();
    
            for (Field field : fields) {
                try {
                    if (field.getAnnotations() != null) {
                        //判断BindViewTo注解是否存在
                        if (field.isAnnotationPresent(BindViewTo.class)) {
                            //获取访问权限
                            field.setAccessible(true);
                            BindViewTo getViewTo = field.getAnnotation(BindViewTo.class);
                            //获取View id
                            int id = getViewTo.value();
                            //通过id获取View,并赋值该成员变量
                            field.set(activity, activity.findViewById(id));
                        }
                    }
                } catch (Exception e) {
                }
            }
        }
    }

    在Activity中调用

    public class MainActivity extends AppCompatActivity {
    
        @BindViewTo(R.id.text)
        private TextView mText;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            //调用注解绑定,当前Activity中所有使用@BindViewTo注解的控件将自动绑定
            AnnotationTools.bindAllAnnotationView(this);
    
            //测试绑定是否成功
            mText.setTextColor(Color.RED);
        }
    
    }

    测试结果毫无意外,字体变成了红色,说明绑定成功。


    编译期注解(APT+JavaPoet)

    编译期注解解析需要用到APT(Annotation Processing Tool)技术,APT是javac中提供的一种编译时扫描和处理注解的工具,它会对源代码文件进行检查,并找出其中的注解,然后根据用户自定义的注解处理方法进行额外的处理。APT工具不仅能解析注解,还能结合JavaPoet技术根据注解生成新的的Java源文件,最终将生成的新文件与原来的Java文件共同编译。

    APT实现流程如下:

    1. 创建一个java lib作为注解解析库——如apt_processor
    2. 在创建一个java lib作为注解声明库——如apt_annotation
    3. 搭建两个lib和主项目的依赖关系
    4. 实现AbstractProcessor
    5. 编译和调用

    整个流程是固定的,我们的主要工作是继承AbstractProcessor,并且实现其中四个方法。下面一步一步详细介绍:

    (1)创建解析库apt_processor

    apply plugin: 'java-library'
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.squareup:javapoet:1.9.0' // square开源的 Java 代码生成框架
        compile 'com.google.auto.service:auto-service:1.0-rc2' //Google开源的用于注册自定义注解处理器的工具
        implementation project(':apt_annotation') //依赖自定义注解声明库
    }
    sourceCompatibility = "7"
    targetCompatibility = "7"

    (2)创建注解库apt_annotation

    声明一个注解BindViewTo,注意@Retention不再是RUNTIME,而是CLASS。

    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.CLASS)
    public @interface BindViewTo {
        int value() default -1;
    }

    (3)搭建主项目依赖关系

    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation project(':apt_annotation')  //依赖自定义注解声明库
        annotationProcessor project(':apt_processor')  //依赖自定义注解解析库(仅编译期)
    }

    这里需要解释一下,因为注解解析库只在程序编译期有用,没必要打包进APK。所以依赖解析库使用的关键字是annotationProcessor,这是google为gradle插件添加的特性,表示只在编译期依赖,不会打包进最终APK。这也是为什么前面要把注解声明和注解解析拆分成两个库的原因。因为注解声明是一定要编译到最终APK的,而注解解析不需要。

     (4)实现AbstractProcessor

    这是最复杂的一步,也是完成我们期望工作的重点。首先,我们在apt_processor中创建一个继承自AbstractProcessor的子类,重载其中四个方法:

    • init()——此处初始化一个工具类
    • getSupportedSourceVersion()——声明支持的Java版本,一般为最新版本
    • getSupportedAnnotationTypes()——声明支持的注解列表
    • process()——编译器回调方法,apt核心实现方法
    具体代码如下:
    //@SupportedSourceVersion(SourceVersion.RELEASE_7)
    //@SupportedAnnotationTypes("com.xibeixue.apt_annotation.BindViewTo")
    @AutoService(Processor.class)
    public class BindViewProcessor extends AbstractProcessor {
    
        private Elements mElementUtils;
        private HashMap<String, BinderClassCreator> mCreatorMap = new HashMap<>();
    
        /**
         * init方法一般用于初始化一些用到的工具类,主要有
         * processingEnvironment.getElementUtils(); 处理Element的工具类,用于获取程序的元素,例如包、类、方法。
         * processingEnvironment.getTypeUtils(); 处理TypeMirror的工具类,用于取类信息
         * processingEnvironment.getFiler(); 文件工具
         * processingEnvironment.getMessager(); 错误处理工具
         */
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            mElementUtils = processingEnv.getElementUtils();
        }
    
        /**
         * 获取Java版本,一般用最新版本
         * 也可以使用注解方式:@SupportedSourceVersion(SourceVersion.RELEASE_7)
         */
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        /**
         * 获取目标注解列表
         * 也可以使用注解方式:@SupportedAnnotationTypes("com.xibeixue.apt_annotation.BindViewTo")
         */
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            HashSet<String> supportTypes = new LinkedHashSet<>();
            supportTypes.add(BindViewTo.class.getCanonicalName());
            return supportTypes;
        }
    
        /**
         * 编译期回调方法,apt核心实现方法
         * 包含所有使用目标注解的元素(Element)
         */
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            //扫描整个工程, 找出所有使用BindViewTo注解的元素
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindViewTo.class);
            //遍历元素, 为每一个类元素创建一个Creator
            for (Element element : elements) {
                //BindViewTo限定了只能属性使用, 这里强转为变量元素VariableElement
                VariableElement variableElement = (VariableElement) element;
                //获取封装属性元素的类元素TypeElement
                TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
                //获取简单类名
                String fullClassName = classElement.getQualifiedName().toString();
                BinderClassCreator creator = mCreatorMap.get(fullClassName);
                //如果不存在, 则创建一个对应的Creator
                if (creator == null) {
                    creator = new BinderClassCreator(mElementUtils.getPackageOf(classElement), classElement);
                    mCreatorMap.put(fullClassName, creator);
    
                }
                //将需要绑定的变量和对应的view id存储到对应的Creator中
                BindViewTo bindAnnotation = variableElement.getAnnotation(BindViewTo.class);
                int id = bindAnnotation.value();
                creator.putElement(id, variableElement);
            }
    
            //每一个类将生成一个新的java文件,其中包含绑定代码
            for (String key : mCreatorMap.keySet()) {
                BinderClassCreator binderClassCreator = mCreatorMap.get(key);
                //通过javapoet构建生成Java类文件
                JavaFile javaFile = JavaFile.builder(binderClassCreator.getPackageName(),
                        binderClassCreator.generateJavaCode()).build();
                try {
                    javaFile.writeTo(processingEnv.getFiler());
                } catch (IOException e) {
                    e.printStackTrace();
                }
    
            }
            return false;
        }
    }

    其中,BinderClassCreator是代码生成相关方法,具体代码如下:

    public class BinderClassCreator {
    
        public static final String ParamName = "rootView";
    
        private TypeElement mTypeElement;
        private String mPackageName;
        private String mBinderClassName;
        private Map<Integer, VariableElement> mVariableElements = new HashMap<>();
    
        /**
         * @param packageElement 包元素
         * @param classElement   类元素
         */
        public BinderClassCreator(PackageElement packageElement, TypeElement classElement) {
            this.mTypeElement = classElement;
            mPackageName = packageElement.getQualifiedName().toString();
            mBinderClassName = classElement.getSimpleName().toString() + "_ViewBinding";
        }
    
        public void putElement(int id, VariableElement variableElement) {
            mVariableElements.put(id, variableElement);
        }
    
        public TypeSpec generateJavaCode() {
            return TypeSpec.classBuilder(mBinderClassName)
                    //public 修饰类
                    .addModifiers(Modifier.PUBLIC)
                    //添加类的方法
                    .addMethod(generateMethod())
                    //构建Java类
                    .build();
    
        }
    
        private MethodSpec generateMethod() {
            //获取全类名
            ClassName className = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
            //构建方法--方法名
            return MethodSpec.methodBuilder("bindView")
                    //public方法
                    .addModifiers(Modifier.PUBLIC)
                    //返回void
                    .returns(void.class)
                    //方法传参(参数全类名,参数名)
                    .addParameter(className, ParamName)
                    //方法代码
                    .addCode(generateMethodCode())
                    .build();
        }
    
        private String generateMethodCode() {
            StringBuilder code = new StringBuilder();
            for (int id : mVariableElements.keySet()) {
                VariableElement variableElement = mVariableElements.get(id);
                //变量名称
                String name = variableElement.getSimpleName().toString();
                //变量类型
                String type = variableElement.asType().toString();
                //rootView.name = (type)view.findViewById(id), 注意原类中变量声明不能为private,否则这里是获取不到的
                String findViewCode = ParamName + "." + name + "=(" + type + ")" + ParamName + ".findViewById(" + id + ");
    ";
                code.append(findViewCode);
    
            }
            return code.toString();
        }
    
        public String getPackageName() {
            return mPackageName;
        }
    }

    (5)编译和调用

    在MainActivity中调用,这里需要强调的是待绑定变量不能声明为private,原因在上面代码注释中已经解释了。

    public class MainActivity extends AppCompatActivity {
    
        @BindViewTo(R.id.text)
        public TextView mText;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);//这里的MainActivity需要先编译生成后才能调用
            new MainActivity_ViewBinding().bindView(this);
            //测试绑定是否成功
            mText.setTextColor(Color.RED);
        }
    }

    此时,build或rebuild工程(需要先注掉MainActivity的调用),会看到在generatedJava文件夹下生成了新的Java文件。

    上面的调用方式需要先编译一次才能使用,当有多个Activity时比较繁琐,而且无法做到统一。

    我们也可以选择另一种更简便的方法,即反射调用。新建工具类如下:

    public class MyButterKnife {
    
            public static void bind(Activity activity) {
                Class clazz = activity.getClass();
                try {
                    Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
                    Method method = bindViewClass.getMethod("bindView", activity.getClass());
                    method.invoke(bindViewClass.newInstance(), activity);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
    }

    调用方式改为:

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            //通过反射调用
            MyButterKnife.bind(this);
    
            //测试绑定是否成功
            mText.setTextColor(Color.RED);
        }

    此方式虽然也会稍微影响性能,但依然比直接使用运行时注解高效得多。


    总结

    说到底,APT是一个编译器工具,是一个非常好的从源码到编译期的过渡解析工具。虽然结合JavaPoet技术被各大框架使用,但是依然存在固有的缺陷,比如变量不能私有,依然要采用反射调用等,普通开发者可斟酌使用。

    个人认为APT有如下优点:

    1. 配置方式,替换文件配置方式,改为代码内配置,提高程序内聚性
    2. 代码精简,一劳永逸,省去繁琐复杂的格式化代码,适合团队内推广

    以上优点同时也是缺点,因为很多代码都在后台生成,会对新同学造成理解困难,影响其对整体架构的理解,增加学习成本。

    近期研究热修复和APT,发现从我们写完成代码,到代码真正执行,期间还真是有大把的“空子”可以钻啊,借图mark一下。

  • 相关阅读:
    Java基础--线程创建方式
    Java基础--static关键字
    Java基础--异常处理
    mybatis的#{}和${}的区别以及order by注入问题
    前后端分离结构中使用shiro进行权限控制
    Java FTP下载文件
    10个经典智力推理题!据说答对7道,智力在140!
    Java面试题总结之数据结构、算法和计算机基础(刘小牛和丝音的爱情故事1)
    Java面试题总结之JDBC 和Hibernate
    Java面试题总结之数据库与SQL语句
  • 原文地址:https://www.cnblogs.com/not2/p/11492806.html
Copyright © 2011-2022 走看看