为什么是注解
作为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:>