Android Annotation扫盲笔记

写在前面

今年大家都在搞组件化,组件化开发不可避免的需要用到路由(Router)来完成组件之间数据的交互,这就促进了各种路由发展如:阿里的ARouter以及ActivityRouter等优秀的Router框架。为了方便大家的开发这些Router库以及像ButterKnife这类的库都用到了注解技术。本篇目的是进行一波扫盲。

本文导读

  • Android Annotation基础
  • 解析Annotation
  • 实战-自己做一个ButterKnife

1 Annotatin基础

1.1 基本Annotation

Java提供了三个基本的Annotation注解,使用时需要在Annotation前面增加@符号,并把Annotation当成一个修饰符来使用。注:Java提供的基本Annotation注解都在java.lang包下面

  1. @Override:限定重写父类方法:这个注解主要是命令编译器帮助我们检查代码(避免出现方法名写错这种低级错误),子类使用了Override注解之后,编译的时候IDE会检查父类中是否有这个函数,如果没有这个函数会编译失败。
public class Animal {
    public void run() {
        //TODO
    }
}

public class Monkey extends Animal {
    @Override
    public void run() {
        //使用了OVerride注解之后,必须重写父类方法
    }
}
  1. @Deprecated:标记该类或者方法已经过时。修改上面的Animal类,使用Deprecated修饰run方法后,子类在使用run方法是IDE会报警告。
public class Animal {
    @Deprecated
    public void run() {
        //TODO
    }
}

3. @SuppressWarnings:抑制编译器警告(用的比较少)。Java代码编译时IDE往往会给开发者很多警告信息,例如变量没有使用等,这种警告多了之后很大程度上影响我们debug效率。此注解就是来抑制这些警告。举个栗子:

 @SuppressWarning("unused")
 public void foo() {
  String s;
 }

如果不使用@SuppressWarning来抑制编译器警告,上面的代码会被警告变量s从未使用。出了"unused",该注解支持的抑制类型还有下图的内容(注该图摘自IBM Knowledge Center)。

1.2 JDK元Annotation

JDK出了在java.lang包中提供了1.1介绍的几种基本Annotation外还在java.lang.annotation包下面提供了四个Meta Annotation(元Annotation)。这四种元Annotation都是来修饰自定义注解的。(hold住节奏,看完这个小结咱们就可以自定义Annotation了)

  1. @Retention注解。该注解只能修饰一个Annotation定义,用于指定所修饰的Annotation可以保留多长"时间"(也可是说是保留的周期)。这里说的“时间”有三种类型

  • RetentionPolicy.SOURCE:没啥用,编译器会直接忽略这种策略的注释
  • RetentionPolicy.CLASS:自定义注解的默认值,编译器会把这种策略的注释保存在class文件中。像ButterKnife中的BindView注解就是用的这种方式。
  • RetentionPolicy.RUNTIME:编译器会把该策略的注释保存到class文件中,程序可以通过反射等方式来获取。

举个例子,自定义一个BindView注解(看不懂没关系,现有一个感性的认识,下一节开始做自定义Annotation讲解)。

//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
    int id() default 0;
}

当成员变量为value时,可以省略。也就是说上述代码可以换成 @Retention(RetentionPolicy.CLASS)

  1. @Target注解:这货也是用于修饰一个自定义的Annotation注解,用于指定自定义注解可以修饰哪些程序元素。该注解的成员变量有
  • ElementType.PACKAGE 注解作用于包
  • ElementType.TYPE 注解作用于类型(类,接口,注解,枚举)
  • ElementType.ANNOTATION_TYPE 注解作用于注解
  • ElementType.CONSTRUCTOR 注解作用于构造方法
  • ElementType.METHOD 注解作用于方法
  • ElementType.PARAMETER 注解作用于方法参数
  • ElementType.FIELD 注解作用于属性
  • ElementType.LOCAL_VARIABLE 注解作用于局部变量 同样的,成员变量名为value时可以省略。我们丰富一下上面用到的自定义的BindView注解:
//此注解修饰的是属性
@Target(ElementType.FIELD)
//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
    int id() default 0;
}
  1. @Documented注解,该注解修饰的自定义注解可以使用javac命令提取成API文档。
  2. @Inherited注解,该注解修饰的自定义具有继承性。举个例子Animal类使用了@Inherited修饰的自定义注解,则子类Monkey也具有该自定义注解描述的特性。

1.3 自定义注解

  1. 定义Annotation,以上面使用的自定义BindView注解为例。可以直接新建Annotation类型的java文件。

  1. 根据自己的需要,使用1.2的只是对自定义的注解进行修饰
/**
 * Created by will on 2018/2/4.
 */
@Documented
//此注解修饰的是属性
@Target(ElementType.FIELD)
//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
 
}

  1. 定义成员变量,自定义注解的成员变量以方法的形式来定义。丰富一下上面的BindView,由于这个自定义注解的功能是对Activity中的View进行绑定。所以我们定义一个id成员变量。
/**
 * Created by will on 2018/2/4.
 */
@Documented
//此注解修饰的是属性
@Target(ElementType.FIELD)
//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
    int id();
}
  1. 使用default关键字为成员变量指定默认值。继续丰富BindView的代码。注default关键字放到int id() 后面。
@Documented
//此注解修饰的是属性
@Target(ElementType.FIELD)
//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
    int id() default 0;
}

根据有没有成员变量,我们可以将Annotation划分成两种:

  • 没有成员变量的注解称为"标记Annotation",这种注解使用自身是否存在为我们提供信息,例如Override等注解
  • 有成员变量的称谓"元数据Annotation"。我们可以使用apt等工具对这种Annotation的成员进行二次加工。

注意:只定义了自定义注解没有任何效果,还需要对Annotation的信息进行提取与加工!!!

上面我们自定义了BindView注解,你是不是想直接拿到Activity中使用呢?例如:

然后你发现Crash了。。。这就要引入下一节的内容了,使用apt对被注解的代码进行二次加工。

2. 解析Annotation

完成自定义Annotation后,我们还需要知道,针对这些注解,我们要做哪些相关的处理,这就涉及到了Annotation的解析操作。 解析Annotation,通常分为:对运行时Annotation的解析、对编译时Annotation的解析; 解析Annotation,其实就是如何从代码中找到Annotation,通常我们的做法是:

  • 用反射的方式获取Annotation,运行时Annotation的解析方式
  • 借助apt工具获取Annotation,编译时Annotation的解析方式
  • 另外如果我们需要生成额外的代码、文件,则还需要借助JavaPoet API

2.1 利用反射解析Annotation

反射的解析方式,通常运用在运行时Annotation的解析。 反射是指:利用Class、Field、Method、Construct等reflect对象,获取Annotation

  • field.getAnnotation(Annotation.class):获取某个Annotation
  • field.getAnnotations():获取所有的Annotation
  • field.isAnnotationPresent(Annotation.class):是否存在该Annotation

通常使用Runtime修饰的注解需要使用反射来配合解析

@Retention(value = RetentionPolicy.RUNTIME)

  1. 新建一个test自定义注解
/**
 * Created by will on 2018/2/4.
 */
@Documented
//此注解修饰的是属性
@Target(ElementType.FIELD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface test {
    int id() default 0;
}
  1. 新建一个java类Animal,并添加test注解
public class Animal {
    @BindView(id = 1000)
    String a;

    @Deprecated
    public void run() {
        //TODO
    }
}
  1. 可以使用反射来获取a的注解成员属性值
 private void testMethod() {
        Class clazz = Animal.class;
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView != null) {
                int id = bindView.id();
                Log.e("------", String.valueOf(id));
            }
        }
    }

2.2 使用apt工具来解析Annotation

APT:是一个注解处理工具 Annotation Processing Tool 作用:利用apt,我们可以找到源代中的注解,并根据注解做相应的处理

  • 根据注解,生成额外的源文件或其他文件
  • 编译生成的源文件和原来的源文件,一起生成class文件

利用APT,在编译时生成额外的代码,不会影响性能,只是影响项目构建的速度

这里我们说一下Android中使用apt的步骤 Android中开发自定义的apt学会两个库及一个类基本就足够了

  • JavaPoet API 这个库的主要作用就是帮助我们通过类调用的形式来生成代码,简单理解就是利用这个库可以生成额外的Java代码。具体的API可以去github上看下,写的很详细。这里不贴代码了。
  • AutoService 这个库是Google开发的,主要的作用是注解 processor 类,并对其生成 META-INF 的配置信息。可以理解使用这个库之后编译的时候IDE会编译我们的Annotation处理器,只需要在自定义的Processor类上添加注释 @AutoService(Processor.class)下面会用到。
  • Processor类,我们自定义的Annotation处理器都需要实现该接口,Java为我们提供了一个抽象类实现了该接口的部分功能,我们自定义Annotation处理器的时候大部分只需要继承AbstractProcessor这个抽象类就行了。

JavaPoet的学习可以直接借鉴官方api,AutoService学习成本较低(只需要用里面一句代码而已,学习成本可以忽略),下面我们重点学习一下AbstractProcessor的使用。

AbstractProcessor介绍

  1. AbstractProcessor方法介绍:下面新建一个AbstractProcessor来看下这货的方法
/**
 * Created by will on 2018/2/5.
 */

public class CustomProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}
  • init(ProcessingEnvironment processingEnvironment): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。
  • process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。以前面提到的自定义注解BindView为例,这里可以查到所有注解了BindView的Activity。
  • getSupportedAnnotationTypes(): 这里必须由开发者指定,该方法返回一个Set,作用是这个注解的处理器支持处理哪些注解。
  • getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 7的话,你也可以返回SourceVersion.RELEASE_7。
  1. AbstractProcessor基础工具解析:从AbstractProcessor的init方法中可以获取一系列的工具来辅助我们解析源码
  • Elements工具类 在AbstractProcessor的init方法中可以获取到一个Elements工具类,具体代码为
 @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
       Elements elementUtils = processingEnv.getElementUtils();
    }

这个工具类是用来处理源代码的,在自定义注解处理器的领域里面,Java源代码每一个类型都属于一个Element,具体使用方法可以直接参考Java官方文档

package com.example;    // PackageElement

public class Test {        // TypeElement

    private int a;      // VariableElement
    private Test other;  // VariableElement

    public Test () {}    // ExecuteableElement

    public void setA (  // ExecuteableElement
                     int newA   // TypeElement
                     ) {}
}

例如,我有一个TypeElement,希望拿到这个class所在的包名就可以使用Elemnts这个工具

 private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }

再来一个栗子,有一个代表Test的TypeElement,希望获取所有的子元素可以这么写(注意,这个很有用)

TypeElement testClass = ... ;  
for (Element e : testClass.getEnclosedElements()){ // iterate over children  
    Element parent = e.getEnclosingElement();  // parent == testClass
}
  • Types:一个用来处理TypeMirror的工具类; TypeElement并不包含类本身的信息。你可以从TypeElement中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过TypeMirror获取。你可以通过调用elements.asType()获取元素的TypeMirror。
  • Filer:正如这个名字所示,使用Filer你可以创建文件。

好了枯燥的基础知识看完了之后我们一起写一个简单的ButterKnife

3. 自己写一个轻量级的ButterKnife

1. 新建一个Java项目,名字为annotations

  1. 这个项目用来定义所有自定义的注解,这部分用到了第一节的知识基础。

  1. 在这个项目包里面新建自定义的注解,我们模仿ButterKnife,这里增加一个BindView的注解

@Documented
//此注解修饰的是属性
@Target(ElementType.FIELD)
//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
    int id() default 0;
}

2 新建Java项目,名称为annotations_compiler

  1. 这个项目是用来处理自定义注解的,这里姑且叫这个项目为BindView的处理器,这里需要第二节的知识基础
  2. 在build.gradle文件中添加AutoService与JavaPoet的依赖
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.squareup:javapoet:1.7.0'
  1. 新建BindViewProcessor处理器类继承自AbstractProcessor,对源代码的注解进行处理(我尽可能的理解有歧义的地方都添加了注释)
/**
 * Created by will on 2018/2/4.
 */

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    /**
     * 工具类,可以从init方法的ProcessingEnvironment中获取
     */
    private Elements elementUtils;
    /**
     * 缓存所有子Element
     * key:父Element类名
     * value:子Element
     */
    private HashMap<String, List<Element>> cacheElements = null;
    /**
     * 缓存所有父Element
     * key:父Element类名
     * value:父Element
     */
    private HashMap<String, Element> cacheAllParentElements = null;

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 规定需要处理的注解类型
        return Collections.singleton(BindView.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations
            , RoundEnvironment roundEnv) {
        //扫描所有注解了BindView的Field,因为我们所有注解BindView的地方都是一个Activity的成员
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        for (Element element : elements) {
            //将所有子elements进行过滤
            addElementToCache(element);
        }

        if (cacheElements == null || cacheElements.size() == 0) {
            return true;
        }
        for (String parentElementName : cacheElements.keySet()) {
            //判断一下获取到的parent element是否是类
            try {
                //使用JavaPoet构造一个方法
                MethodSpec.Builder bindViewMethodSpec = MethodSpec.methodBuilder("bindView")
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .returns(void.class)
                        .addParameter(ClassName.get(cacheAllParentElements.get(parentElementName).asType())
                                , "targetActivity");

                List<Element> childElements = cacheElements.get(parentElementName);
                if (childElements != null && childElements.size() != 0) {
                    for (Element childElement : childElements) {
                        BindView bindView = childElement.getAnnotation(BindView.class);
                        //使用JavaPoet对方法内容进行添加
                        bindViewMethodSpec.addStatement(
                                String.format("targetActivity.%s = (%s) targetActivity.findViewById(%s)"
                                        , childElement.getSimpleName()
                                        , ClassName.get(childElement.asType()).toString()
                                        , bindView.id()));
                    }
                }

                //构造一个类,以Bind_开头
                TypeSpec typeElement = TypeSpec.classBuilder("Bind_"
                        + cacheAllParentElements.get(parentElementName).getSimpleName())
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addMethod(bindViewMethodSpec.build())
                        .build();

                //进行文件写入
                JavaFile javaFile = JavaFile.builder(
                        getPackageName((TypeElement) cacheAllParentElements.get(parentElementName))
                        , typeElement).build();
                javaFile.writeTo(processingEnv.getFiler());

            } catch (IOException e) {
                e.printStackTrace();
                return true;
            }


        }


        return true;
    }

    private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }

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

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 缓存父Element对应的所有子Element
     * 缓存父Element
     *
     * @param childElement
     */
    private void addElementToCache(Element childElement) {
        if (cacheElements == null) {
            cacheElements = new HashMap<>();
        }

        if (cacheAllParentElements == null) {
            cacheAllParentElements = new HashMap<>();
        }
        //父Element类名
        String parentElementName = null;
        parentElementName = ClassName.get(childElement.getEnclosingElement().asType()).toString();

        if (cacheElements.containsKey(parentElementName)) {
            List<Element> childElements = cacheElements.get(parentElementName);
            childElements.add(childElement);
        } else {
            ArrayList<Element> childElements = new ArrayList<>();
            childElements.add(childElement);
            cacheElements.put(parentElementName, childElements);
            cacheAllParentElements.put(parentElementName, childElement.getEnclosingElement());
        }
    }
}

3.新建Android项目,使用自定义的注解

  1. 添加对上述两个项目的引用

注意:Android Gradle插件2.2版本发布后,Android 官方提供了annotationProcessor来代替android-apt,annotationProcessor同时支持 javac 和 jack 编译方式,而android-apt只支持 javac 方式。同时android-apt作者宣布不在维护,这里我直接用了annotationProcessor

implementation project(':annotations')
annotationProcessor project(':annotations_compiler')
  1. 在Activity的View中添加@BindView注解,并设置id
public class MainActivity extends AppCompatActivity {

    @BindView(id = R.id.tv_test)
    TextView tv_test;
    @BindView(id = R.id.tv_test1)
    TextView tv_test1;
    @BindView(id = R.id.iv_image)
    ImageView iv_image;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Bind_MainActivity.bindView(this);
        tv_test.setText("test_1");
        tv_test1.setText("test_2");
        iv_image.setImageDrawable(getDrawable(R.mipmap.ic_launcher));
    }

}
  1. 此时你的IDE可能会报Bind_MainActivity找不到,没关系,重新Build一下就好了。Build一下后在app/build/generated/source/apt/debug/[你的包名]/annotation/路径下就回生成apt输出的文件了。

其他的问题

  1. 如果你发现build后没有apt文件输出,呵呵,因为你写的processor有Bug~~~。这时候你需要debug你的processor。关于如何debug,请移步这篇博客
  2. 关于android-apt切换为官方annotationProcessor的问题,请移步android-apt切换为官方annotationProcessor
  3. 待补充ing...

最后附上demo源码

参考文章

About Me

contact way value
mail weixinjie1993@gmail.com
wechat W2006292
github https://github.com/weixinjie
blog https://juejin.im/user/57673c83207703006bb92bf6
关注下面的标签,发现更多相似文章
评论
说说你的看法