仿写ButterKnife框架核心功能 掌握编译时注解+注解处理器APT生成Java代码的技术

编译时注解

  • 前言
  • 编译时注解
  • 实现
    • 创建编译时注解
    • 创建注解处理器
    • 编译期生成Java文件
    • 使用
  • 总结

在这里插入图片描述

前言

ButterKnife框架想必大家都使用过,他是出自JakeWharton的一个开源库,其Github地址ButterKnife;它通过注解的方式来替代Android中繁杂的View操作,比如findViewById,setOnClickListener等,因为是编译时注解的使用,所以对性能的影响很小,本篇文章就通过仿写ButterKnife框架的一些主要功能来加深对编译时注解及注解处理器的使用

编译时注解

在上篇文章Android使用运行时注解+反射仿写EventBus组件通信框架 掌握事件总线通信核心原理中就介绍过注解的概念,这里再挪过来:

注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的上面,用来对这些元素进行标记说明,它本身不会在运行时起什么作用,需要我们编写注解处理器处理这些注解(编译时注解),或者在程序运行时通过反射得到这些注解做出相应的处理(运行时注解)

每个注解都必须使用注解接口@interface进行声明,这实际上会创建一个Java接口,也会编译成一个class文件,注解接口内部的元素声明实际上是方法声明,方法没有参数,没有throws语句,也不能使用泛型

注解又分为标准注解、编译时注解和运行时注解:

  • 标准注解:Java API中默认定义的注解我们称为标准注解,它们定义在java.lang、java.lang.annotation、javax.annotation中;按照使用场景不同又可以分为三类:

    • 编译相关注解:编译相关的注解是供编译期使用的,如@Override:编译器会检查被注解的方法是否真的重写了父类的方法,没有的话编译器会提示错误;@Deprecated:用来修饰任何不再鼓励使用或被弃用的方法

    • 资源相关注解:这个一般用在JavaEE领域,在Android开发中没有用到,比如@Resource:用于Web容器的资源注入,表示单个资源

    • 元注解:这个一般用来定义和实现注解的注解,也就是用来修饰注解的,总共有如下5种:

      • @Target:用来指定注解所适用的对象范围,这个注解的取值是一个ElementType类型的数组,总共有如下几种不同类型:
      1. ANNOTATION_TYPE:注解类型声明,表明该注解只能作用在注解上
      2. CONSTRUCTOR:构造方法,表明该注解作用于构造方法上
      3. FIELD:变量,表明该注解只能作用在变量上
      4. LOCAL_VARLABLE:局部变量,表明该注解作用在局部变量
      5. PARAMETER:参数,表明该注解只能作用在参数上
      6. METHOD:方法,表明该注解只能作用在方法上
      7. TYPE:类和接口,表明该注解只能作用在类和接口上
      • @Retention:用来指明注解的访问范围,也就是在什么级别保留注解,有如下三种选择:
      1. 源码级注解:在定义注解接口时,使用@Retention(RetentionPolicy.SOURCE)修饰的注解,该类型的注解信息只会保留在.java源码里,源码经过编译后,注解信息会被丢弃,不会保留在class文件中
      2. 编译时注解:在定义注解接口时,使用@Retention(RetentionPolicy.CLASS)修饰的注解,该类型的注解信息会保留在.java源码里和class文件里,在执行的时候,会被Java虚拟机丢弃,不会加载到内存中
      3. 运行时注解: 在定义注解接口时,使用@Retention(RetentionPolicy.RUNTIME)修饰的注解,该类型的注解信息不光会保留在.java源码里和class文件里,在运行时也会被虚拟机保留,可以通过反射机制读取注解信息
      • @Documented:表示被修饰的注解应该被包含在被注解项的文档中,比如用JavaDoc生成的文档
      • @Inherited:表示该注解可以被子类继承
      • @Repeatable:表示该注解可以在同一个项上面应用多次,不过这个是在Java8引入的,其它四个都是Java5就存在了
  • 编译时注解:要定义编译时注解只需在定义注解时使用@Retention(RetentionPolicy.SOURCE)修饰即可,编译时注解结合注解处理器(APT)在编译期完成操作,对性能影响很小;像@Nullable@NonNull这类的注解就是编译时注解;一些开源框架如BufferKnife, ARout、Dagger、Retrofit等都有使用编译时注解

  • 运行时注解:只需在定义注解时使用@Retention(RetentionPolicy.RUNTIME)修饰即可;运行时注解一般和反射配合使用,相比编译时注解,性能较低,但是灵活,实现方便;像@Subscribe@Autowired等都是通过反射API进行操作,otto、EventBus等框架会使用运行时注解

实现

要想实现类似的功能,主要有两步,第一就是创建编译时注解,第二步就是创建注解处理器

编译时注解能够自动处理Java源文件并生成更多源码、配置文件、脚本或其它可能想要生成的东西,这些操作都是由注解处理器完成的

创建编译时注解

首先新建一个library,将所有的注解定义在这个库里,但是要注意这里是新建Java Library,不是Android Library;因为后面创建的注解处理器Library要用到javax里的api,而且要依赖这个注解Library,所以两者都需要是Java Library
仿写ButterKnife框架核心功能 掌握编译时注解+注解处理器APT生成Java代码的技术_第1张图片
然后新建注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {

    @IdRes  int value();
}

在build.gradle中添加依赖

api 'com.android.support:support-annotations:27.0.2'

@IdRes 是 android support library 中的编译时检查注解,表明注解的值必须是资源 ID;当然还可以定义其它的注解,比如BindString、BindColor等

创建注解处理器

继续新建一个Java Library,定义注解处理器;这里解释下注解处理器,英文Annotation Processing Tool,也就是我们平时说的APT:它是一种处理注解的工具,对源代码文件进行检测找出其中的注解,然后根据注解生成代码;如果想要自定义的注解处理器能够正常运行,必须通过APT来进行处理,或者说只有通过声明APT后,程序在编译期其自定义注解处理器才能执行

自定义注解处理器实现步骤:

  1. 添加依赖 implementation ‘com.google.auto.service:auto-service:1.0-rc3’,加入谷歌注解处理器服务,这是注册注解处理器的第一步

  2. 定义自定义注解处理器类并继承AbstractProcessor类 ,重写一系列方法

  3. 使用@AutoService注解修饰,完成注册注解处理器,这样编译器工作时才会执行这个注解处理器;其原理是利用了 Google 的 AutoService 为注解处理器自动生成 metadata 文件并将注解处理器jar文件加入构建路径,这样也就不需要再手动创建并更新 META-INF/services/javax.annotation.processing.Processor 文件了

/**
 * @Description TODO(注解处理器 帮我们生成文件并且写文件)
 * @author Mangoer
 * @Date 2019/5/22 22:37
 * 自定义注解处理器:
 *      1. 继承AbstractProcessor类 并重写process方法
 *      2. 使用@AutoService注解修饰 注册注解处理器
 *  在编译期间,编译器会定位到Java源文件中的注解(因为有RetentionPolicy.CLASS修饰),注解处理器会对其感兴趣的注解进行处理
 *  一个注解处理器只能产生新的源文件,不能修改一个已经存在的源文件
 */
@AutoService(Processor.class)
public class AnotationComplie extends AbstractProcessor {

    //创建Java源文件、Class文件以及其它辅助文件的对象
    Filer mFiler;
    //包含了一些用于操作Element的方法
    Elements mElementUtils;

    /**
     * 初始化方法被注解处理工具调用,并传入参数,这个参数包含了很多有用的工具类
     * 比如Elements、Types、Filer等
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
    }

    /**
     * 这个方法很重要,指定这个注解处理器能够处理的注解
     * 返回一个Set集合,里面保存我们希望它处理的注解
     * @return
     */
    @Override
    public Set getSupportedAnnotationTypes() {
        Set type = new LinkedHashSet<>();
        type.add(BindView.class.getCanonicalName());
        return type;
    }

    /**
     * 指定注解处理器使用的Java版本,通常返回SourceVersion.latestSupported()即可
     * 也可以指定支持某个版本的Java,比如SourceVersion。RELEASE_6
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 实现注解处理器的具体业务逻辑
     * 这里我们就需要编写findViewById等代码
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        return false;
    }
}

同时在build.gradle文件添加一句

//注册我们的注解处理器,告诉虚拟机我们这个module里面的自定义注解处理器
implementation 'com.google.auto.service:auto-service:1.0-rc3'

这里面最重要的方法就是process,我们的逻辑就在这里面编写,如下

@Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {

        /**
         * 要知道我们的Java文件都是结构化的数据,最外层就是类声明,里面是全局变量,然后就是方法
         * 这样每一个结点都对应一个标签,比如
         *          类标签---------TypeElement
         *          成员变量标签---VariableElement
         *          方法标签-------ExecutableElement
         *
         *  所以这里拿到的Set集合就是程序中所有使用到BindView注解的结点对应的标签
         */
        Set elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        /**
         * 把所有使用到BindView的数据结构化
         * key-----类名
         * value---每个类里使用到BindView的成员变量的集合
         */
        Map> eleMap = new HashMap<>();
        /**
         * 遍历所有使用到BindView注解的结点,也就是成员变量
         * 将其与所在类一一对应
         */
        for (Element element : elements) {

            VariableElement variableElement = (VariableElement) element;
            //通过成员变量结点获取上一个结点,也就是拿到类结点,再拿到类名
            String className = variableElement.getEnclosingElement().getSimpleName().toString();
            List elementList = eleMap.get(className);
            if (elementList == null) {
                elementList = new ArrayList<>();
                eleMap.put(className,elementList);
            }
            elementList.add(variableElement);

        }

        /**
         * 接下来就是重点了,开始写Java文件了
         * Java文件是结构化数据
         */
        Iterator iterator = eleMap.keySet().iterator();
        //写文件对象
        Writer writer = null;
        while (iterator.hasNext()) {
            //拿到每个类的所有成员变量
            String className = iterator.next();
            List elementList = eleMap.get(className);
            //拿到包名,主要是通过成员变量结点的上一个结点,也就是类结点,获取它的包信息
            String packageName = mElementUtils.getPackageOf(elementList.get(0).getEnclosingElement()).toString();
            /**
             * 创建Java文件
             * createSourceFile---创建Java文件
             * createClassFile----创建Class文件
             * createResource-----创建资源文件
             */
            try {
                JavaFileObject fileObject = mFiler.createSourceFile(packageName+"."+className+"$ViewBinder");
                writer = fileObject.openWriter();
                //写类的包名
                writer.write("package " + packageName + ";\n");

                writer.write("\n");
                // 写导入的类
                writer.write("import " + packageName + ".ViewBinder" + ";\n");

                writer.write("\n");
                //定义类
                writer.write("public class " + className + "$ViewBinder implements ViewBinder<" + packageName + "." + className + ">{" + "\n");
                //定义bind方法 接收参数
                writer.write("    public void bind(" + packageName + "." + className + " target){" + "\n");
                //写findViewById
                for (VariableElement variableElement : elementList) {
                    //获取变量名
                    String name = variableElement.getSimpleName().toString();
                    //获取变量类型
                    TypeMirror typeMirror = variableElement.asType();
                    //获取控件id
                    BindView annotation = variableElement.getAnnotation(BindView.class);
                    int id = annotation.value();
                    writer.write("        target." + name + " = (" + typeMirror + ")target.findViewById(" + id + ");"+"\n");
                }
                writer.write("    }\n");
                writer.write("}");
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                if (writer != null) {
                    try {
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return false;
    }

通过这个方法,你应该知道两个东西

  • 结构体语言:Java就是一种结构体语言,Java文件都是结构化的数据,由一个个结点Element组成的,比如类声明结点,成员变量声明结点,方法声明结点;如果你写过Html,就很容易理解结构体语言了;这里需要获取整个工程所有使用到我们定义的BindView注解的结点,然后将所有结点按类分开保存;接下来就是写每个Java文件了,把属于同一个类的结点写到同一个Java文件中;最后就是写findViewById了,当然了还可以写其它的

  • Element:这个类在注解处理器中是非常重要的一个api,就是上面说的结点对象,经常用到的方法如下:
    仿写ButterKnife框架核心功能 掌握编译时注解+注解处理器APT生成Java代码的技术_第2张图片

其实注解处理器重点就是帮我们在编译期写Java文件,当然了还可以写其它文件了;这里其实就是相当于我们自己写个Java类,然后在里面把findViewById等操作写在里面,到运行时直接用这里面的代码,就不需要我们在Activity里写;所以我们通过编译时注解和注解处理器来写Java文件,就不需要我们写这些代码

编译期生成Java文件

都说了是在编译期就帮我们把代码写好了,那是不是真的呢?先在几个Activity使用注解BindView

仿写ButterKnife框架核心功能 掌握编译时注解+注解处理器APT生成Java代码的技术_第3张图片
仿写ButterKnife框架核心功能 掌握编译时注解+注解处理器APT生成Java代码的技术_第4张图片

然后Build一下工程,在如下图目录就会生成两个文件

仿写ButterKnife框架核心功能 掌握编译时注解+注解处理器APT生成Java代码的技术_第5张图片

打开它们看
仿写ButterKnife框架核心功能 掌握编译时注解+注解处理器APT生成Java代码的技术_第6张图片
仿写ButterKnife框架核心功能 掌握编译时注解+注解处理器APT生成Java代码的技术_第7张图片

可以看到编译完成后,生成了这么两个Java文件,里面帮我们写好了findViewById操作,这样我们只要调用这个bind方法就可以了

使用

接下来再新建一个Module,这是一个Android Library,因为它要用到Android里的api

新建一个ViewBinder接口

public interface ViewBinder{

    void bind(T target);
}

这个就接口就是上面用到的,它的目的就是方便给Activity调用的

再新建一个工具类

public class MangoKnife {

    public static void bind(Activity activity){

        String complieName = activity.getClass().getName()+"$ViewBinder";

        try {
            ViewBinder viewBinder = (ViewBinder) Class.forName(complieName).newInstance();
            viewBinder.bind(activity);

        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

这个方法就是给Activity或者Fragment使用的,它的逻辑是通过反射实例化出刚才生成的Java类,因为它们实现了ViewBinder接口,所以直接将其强转成这个类型,调用其bind方法

总结

使用编译时注解技术可以帮我们省却很多工作,极大的提高开发效率,由于不涉及到反射就不会影响运行时性能,同时让代码更加简洁优美,值得我们去掌握

有疑问的朋友欢迎下方留言

觉得不错的朋友就点个赞吧
在这里插入图片描述

本文代码可从MangoBus下载

你可能感兴趣的:(【Android常用开发】)