为什么是注解
作为Android开发人员,先看一些熟悉的代码:
setContentView(R.layout.activity_main); Toolbar toolbar = findViewById(R.id.toolbar); new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.xxx, parent, false))); 像这样的不可省略代码,大量出现在各个地方,一遍一遍的被复制,复制过程中改变很少,或者根本没有改变。在计算机编程中称之为样板代码。经常使用类似的样板,直接后果导致程序员编写更多代码,做更少的工作。这显然是不可以忍受的。
从《重构改善既有代码的设计》章节三:代码的坏味道中,我们学到,多个相同的程序结构,将他们合二为一,程序会变得更好。最直观体现就是使用工具类抽取公共方法各处调用。这样就可以更少的代码,更高的效率了。类似的,使用注解解决Android中的样板代码问题。
上面的引子的目的是要直观地说明,为什么需要注解?术语话一些如下:
> 每当你创建描述符性质的类和接口的时,一旦其中包含重复性工作,就可以考虑简化与自动化该过程。
> 如果你想为应用设置很多的常量或参数,xml是一个很好的选择,因为它不会同特定的代码相连。如果你想把某个方法声明为服务,那么使用Annotation会更好一些,因为这种情况下需要注解和方法紧密耦合起来,开发人员也必须意识到这一点。
注解如何工作
示例:
package java.lang; @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { } 这是JDK的源码,可以说里面几乎啥也没写,我们平时在重写父类或接口方法时,使用@Override,编译器就知道调用我们重写后的方法。这说明编译器在某些地方根据这个注解,对程序做了对应的处理。
我们来看下注解的定义:
> 源代码的元数据,即描述数据的数据。
可以看到注解是描述数据用的,根据我们在@Override上的推断,还应该有相对应的代码,根据这个描述(注解)来处理注解标记了的类,方法,变量,参数。
所以,当自定义注解时候,要定义注解,还要根据注解实现对应的业务逻辑。
注解类型
看java.lang.Override这个注解类的实现。看到自定义注解上还有注解,这称之为元注解。
元注解
Documented
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.ANNOTATION_TYPE}) public @interface Documented { } 没有值,简单的标记注解,标识是否将注解信息包含在java文档中
Retention
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.ANNOTATION_TYPE}) public @interface Retention { RetentionPolicy value(); } public enum RetentionPolicy { SOURCE, CLASS, RUNTIME; private RetentionPolicy() { } } 标识在什么时候使用该注解,注解的生命周期。有三个值:SOURCE,CLASS,RUNTIME
RetentionPolicy.SOURCE:有效期在源码阶段,在编译阶段丢弃,这些注解在编译结束后不会有任何意义,也不会写入字节码中。RetentionPolicy.CLASS:有效期至字节码文件。在类加载的时候丢弃,注解默认使用这种方式。RetentionPolicy.RUNTIME:始终有效,在运行时也会保留。因此可以使用反射读取该注解的信息,自定义注解通常使用这种方式。
Target
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.ANNOTATION_TYPE}) public @interface Target { ElementType[] value(); } public enum ElementType { TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE; private ElementType() { } } 表示在什么地方使用该注解。如果没有指定,则该注解可以放在任意地方。
ElementType.TYPE:用于描述类,接口(包括注解类型),EnumElementType.FIELD:用于描述实例变量(包括枚举的常量)ElementType.METHOD:用于描述方法ElementType.PARAMETER:用于描述参数ElementType.CONSTRUCTOR:用于构造方法ElementType.LOCAL_VARIABLE:用于描述局部变量ElementType.ANNOTATION_TYPE:用于描述注解ElementType.PACKAGE:用于描述包ElementType.TYPE_PARAMETER:since 1.8 表示该注解能写在类型变量的声明语句中ElementType.TYPE_USE:since 1.8 表示该注解能写在使用类型的任何语句中
Inherited
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Inherited { } 表示该注解类型被自动继承,如果用户在当前类中查询这个元注解类型,但是当前类的声明中不包含这个元注解类型,那么将自动查询其父类,直至查到该注解或到达顶层类。
内建注解
@Override
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) public @interface Override { } - 适用于方法
- 仅保留在源码阶段
- 用于表示重写超类方法,如果带有此标记的方法没有在超类出现,那么编译器报错
@Deprecated
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE}) public @interface Deprecated { } - 注解信息包含在
java文档中 - 适用于所有地方
- 始终有效
- 表示过时了的api
@SuppressWarnings
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); } - 适用于所有地方
- 仅保留在源码阶段
- 表示抑制警告
自定义注解
以butterknife 为例
annotations
@Target(METHOD) @Retention(RUNTIME) @ListenerClass( targetType = "android.view.View", setter = "setOnClickListener", type = "butterknife.internal.DebouncingOnClickListener", method = @ListenerMethod( name = "doClick", parameters = "android.view.View" ) ) public @interface OnClick { /** View IDs to which the method will be bound. */ @IdRes int[] value() default { View.NO_ID }; } - 适用于方法
- 始终有效
- 自定义元注解,标识该注解使用的api范围
自定义APT
Annotation Processor Tool这里我们可以自己使用反射实现,也可以继承javax.annotation.processing.AbstractProcessor
public class MyProcessor extends AbstractProcessor { @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); } @Override public Set<string> getSupportedAnnotationTypes() { return null; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public boolean process(Set<!--? extends TypeElement--> annotations, RoundEnvironment roundEnv) { return true; } } getSupportedAnnotationTypes:该处理器是处理哪些注解的。
butterknife实现:
@Override public Set<string> getSupportedAnnotationTypes() { Set<string> types = new LinkedHashSet<>(); for (Class<!--? extends Annotation--> annotation : getSupportedAnnotations()) { types.add(annotation.getCanonicalName()); } return types; } private Set<class<? extends annotation>> getSupportedAnnotations() { Set<class<? extends annotation>> annotations = new LinkedHashSet<>(); annotations.add(BindAnim.class); annotations.add(BindArray.class); annotations.add(BindBitmap.class); annotations.add(BindBool.class); annotations.add(BindColor.class); annotations.add(BindDimen.class); annotations.add(BindDrawable.class); annotations.add(BindFloat.class); annotations.add(BindFont.class); annotations.add(BindInt.class); annotations.add(BindString.class); annotations.add(BindView.class); annotations.add(BindViews.class); annotations.addAll(LISTENERS); return annotations; } getSupportedSourceVersion:支持的java版本,一般不需要重写。
process(Set<!--? extends TypeElement--> annotations, RoundEnvironment roundEnv):扫描和处理注解并生成java代码。代码生成java源码比较痛苦,使用javapoet
butterknife的BindView实现:
// Process each @BindView element. for (Element element : env.getElementsAnnotatedWith(BindView.class)) { // we don't SuperficialValidation.validateElement(element) // so that an unresolved View type can be generated by later processing rounds try { parseBindView(element, builderMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindView.class, e); } } 注册到javac
@AutoService(Processor.class) public class MyProcessor extends AbstractProcessor { ... } implementation 'com.google.auto.service:auto-service:1.0-rc5' 向javac注册自定义的注解处理器,然后javac在编译时才会调用我们自定义的处理方法。
调用
普通方法:
submit.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO ... } }) lamda:
submit.setOnClickListener(v -> { // TODO ... }) butterknife:
@OnClick(R.id.submit) void submit() { // TODO ... } 脑图
实战
网上有很多关于AnnotationProcessorTool的示例,尤其以仿写butterknife的为多。要手敲一个例子,加深一下印象。
前提要点:
- 只有注解而没有对应处理器,那注解将毫无作用
- 注解处理器生成
java源码,而不是字节码 - 如果读了《深入理解java虚拟机 第四部分:程序编译与代码优化》就更好了,没读过也不影响
- 使用
JavaPoet和google的AutoService库
可能出现的问题
Android Studio 启用Annotation Process Tool
在我的Adnroid 3.4版本上,需要设置启用一下,步骤如下:
File -> Close Project 进入:
在Configure中找到Project Defaults层级里的Settings,如图:
勾选Enable annotation processing。
@AutoService(Processor.class)没有生成源码
我们自己编写也好,运行网上的源码也好,如果构建顺利,目录.\app\build\generated\source\apt\debug下会出现在注解处理器中指定生成的.java文件。也有可能构建没有出错,但是什么也没有生成。
网上通用教程,一般有两种方式把注解处理器注册到javac。手写javax.annotation.processing.Processor文件和使用 com.google.auto.service:auto-service:1.0-rc5库。
没有生成源码,就是javac没有执行我们的注解处理器中的逻辑。
本人的实践,确定已经启用了注解处理器的前提下,使用 com.google.auto.service:auto-service:1.0-rc5库没有生成源码,手写文件则成功生成了,这花费了我一上午去找原因,感觉很不值得,中断了连贯的学习,打击也挺大的。
Javapoet
这是个好用的库,方便生成Java源文件,建议跟着网上教程,花费一个小时左右时间,敲一下例子,提高直观感觉,然后再实际使用会比较顺畅。
调试APT
这个自行搜索吧,远程调试配合Android Studio的快速构建,总是产生Connection refused让人用得好难受,每次调试都要或Clean或改代码或Invalidate Caches,把我给虐惨了,所以 本人实践中是直接查看产生的java文件的。 </https:></class<?></class<?></string></string></string></https:></https:>