Android注解&APT技术

Android注解&APT技术
文章图片
image.png 序言
注解是Java程序和Android程序中常见的语法,之前虽然知道有这么个东西,但并没有深入了解注解。写EventBus源码解析和ButterKnife源码解析的时候,发现注解在其中起到很大作用,就决定专门写一篇文章介绍注解。
下面将会从这几个方面展开介绍:

  1. 注解的概念和语法
  2. 运行时注解
  3. 编译时注解(APT技术)
  4. 对比运行时和编译时注解
  5. 总结
注解的概念和语法
1. 注解的概念 定义:注解用于为Java提供元数据,作为元数据,注解不影响代码执行,但某些类型注解也可以用于这一目的,注解从Java5开始引入
2. 注解的语法 注解通过@interface关键字来定义。
@Retention(RetentionPolicy.CLASS) @Target({ElementType.FIELD, ElementType.TYPE}) public @interface MyAnnotation {}

3. 元注解 在上面的定义中,RetentionTarget是什么东西?它们为什么能够修饰注解。
实际上,它们是元注解:元注解是可以注解到注解上的注解,简单来说就是一种基本注解,可以作用到其他注解上。
Java中总共有5中元注解:@Retention,@Documented,@Target,@Inherited,@Repeatable。下面分别介绍它们:
@Retention 用来说明注解的存活时间,有三种取值:
  • RetentionPolicy.SOURCE:注解只在源码阶段保留,编译器开始编译时它将被丢弃忽视
  • RetentionPolicy.CLASS:注解会保留到编译期,但运行时不会把它加载到JVM中
  • RetentionPolicy.RUNTIME:注解可以保留到程序运行时,它会被加载到JVM中,所以程序运行过程中可以获取到它们
编译期注解和运行时注解使用得比较多,下面会有两个主题专门介绍。
@Target 指定注解可作用的目标,取值如下:
  • ElementType.PACKAGE:可作用在包上
  • ElementType.TYPE:可作用在类、接口、枚举上
  • ElementType.ANNOTATION_TYPE:可以作用在注解上
  • ElementType.FIELD:可作用在属性上
  • ElementType.CONSTRUCTOR:可作用在构造方法上
  • ElementType.METHOD:可作用在方法上
  • ElementType.PARAMETER:可作用在方法参数上
  • ElementType.LOCAL_VARIABLE:可作用在局部变量上,例如方法中定义的变量
它接收一个数组作为参数,即可以指定多个作用对象,就像上面的Demo:
@Target({ElementType.FIELD, ElementType.TYPE})

@Documented 从名字可知,这个注解跟文档相关,它的作用是能够将注解中的元素包含到Javadoc中去。
@Inherited Inherited是继承的意思,但并不是注解本身可被继承,而是指一个父类SuperClass被该类注解修饰,那么它的子类SubClass如果没有任何注解修饰,就会继承父类的这个注解。
举个栗子:
@Inherited @Target(ElementType.Type) @Retention(RetentionPolicy.RUNTIME) public @interface Test {}@Test public class A {}public class B extens A {}

解释:注解Test被@Inherited修饰,A被Test修饰,B继承A(B上又无其他注解),那么B就会拥有Test这个注解。
@Repeatable 这个词是可重复的意思,它是java1.8引入的,算一个新特性。
什么样的注解可以多次应用来呢,通常是注解可以取多个值,举个栗子:
public @Interface Persons { Person[] value(); }@Repeatable(Persons.class) public @Interface Person { String role() default "" }@Person("artist") @Person("developer") @Person("superman") public class Me {}

解释:@Person被@Repeatable修饰,所以Person可以多次作用在同一个对象Me上,而Repeatable接收一个参数,这个参数是个容器注解,用来存放多个@Person。
4. 注解的属性 注解中可以定义属性,也可以叫成员变量,不能定义方法。
就如上面的例子:
  • @Person中定义了一个属性role,在使用的过程中就可以传一个字符串
  • 又给role设置了默认值为空字符串,以就算不传可以直接使用
  • 如果有多个属性,就必须以key=value的形式指定属性值
注解中的属性支持8种基本类型外加字符串、类、接口、注解及以上类型的数组
5. Java预置注解 Java中提供了很多注解,如:
  • @Override:表示覆写父类中的方法
  • @Depracated:标记过时的类、方法、成员变量
  • @FunctionalInterface:Java1.8引入的新特性,表示函数式接口(只有一个方法的普通接口),主要用于lambda表达式。
  • ......
运行时注解
上面介绍过,用Retention(RetentionPolicy.RUNTIME)修饰的就是运行时注解。使用这种注解,多数情况是为了在运行时做一些事情。至于具体做什么事?就看各位同学自己的意愿了。
这里,我通过一个例子来介绍怎么使用运行时注解。
现在,我打算通过运行时注解实现一个功能,跟ButterKnife类似,即自动注入功能,不需要我手动调用findViewById。
下面是实现的步骤:
1. 定义注解
/** * author : user_zf * date : 2018/11/6 * desc : 运行时通过反射自动注入View,不再需要写findViewById */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface InjectView { @IdRes int id() default -1; }

这个注解中有一个属性id,表示待注入控件的id
2. 定义注解解析工具
/** * author : user_zf * date : 2018/11/6 * desc : 用来解析注解InjectView */ public class AnnotationUtil {/** * 解析注解InjectView * * @param activity 使用InjectView的目标对象 */ public static void inject(Activity activity) { Field[] fields = activity.getClass().getDeclaredFields(); //通过该方法设置所有的字段都可访问,否则即使是反射,也不能访问private修饰的字段 AccessibleObject.setAccessible(fields, true); for (Field field : fields) { boolean needInject = field.isAnnotationPresent(InjectView.class); if (needInject) { InjectView anno = field.getAnnotation(InjectView.class); int id = anno.id(); if (id == -1) continue; View view = activity.findViewById(id); Class fieldType = field.getType(); try { //把View转换成field声明的类型 field.set(activity, fieldType.cast(view)); } catch (Exception e) { Log.e(InjectView.class.getSimpleName(), e.getMessage()); } } } } }

主要是通过反射,找到Activity中使用了@InjectView的字段,然后通过findViewById来初始化控件。
3. 使用注解
class MainActivity : AppCompatActivity() {@InjectView(id = R.id.tvHello) private var tvHello: TextView? = null @InjectView(id = R.id.btnHello) private var btnHello: Button? = null @InjectView(id = R.id.rlRoot) private var rlRoot: RelativeLayout? = nulloverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //通过注解初始化控件 AnnotationUtil.inject(this@MainActivity)//设置控件 tvHello?.text = "Hello World!" btnHello?.text = "Hello Button" btnHello?.setOnClickListener { Toast.makeText(this@MainActivity, "点击按钮了", Toast.LENGTH_SHORT).show() } rlRoot?.setBackgroundColor(resources.getColor(R.color.colorAccent, null)) } }

在控件上使用注解,就不需要我们手动初始化,注解解析工具会自动帮我们初始化。大大减少重复代码。
原理:运行时注解主要通过反射进行解析,代码运行过程中,通过反射我们可以知道哪些属性、方法使用了该注解,并且可以获取注解中的参数,做一些我们想做的事情
编译时注解(APT技术)
使用Retention(RetentionPolicy.CLASS)修饰的注解就是编译时注解。
说到编译时注解,就需要引出我们今天的主角:APT(编译时解析技术)。
APT技术主要是通过编译期解析注解,并且生成java代码的一种技术,一般会结合Javapoet技术来生成代码。
下面,我们还是通过一个栗子来介绍APT技术。
在写Bean的时候经常需要写Getter和Setter方法,我们想通过一个注解,在编译的过程中自动帮我们生成Getter和Setter方法,这里会生成一个新的类,而不是修改原来的类。
1. 编写注解
/** * author : user_zf * date : 2018/11/7 * desc : 编译期给bean生成getter和setter方法的注解(限java类使用) */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface GenerateGS {}

2. 编写注解解析器Processor 这里的注解解析器和运行时注解解析器不一样,这里需要继承AbstractProcessor类。
在Android Module和Android Library Module中是不能使用AbstractProcessor类的,需要新建一个Java Library Module,把注解解析器放在这个Java Module中,然后用Android Module依赖这个Java Module。
接下来,看一下我们的GenerateGSProcessor的实现:
/** * author : user_zf * date : 2018/11/7 * desc : generateGS编译时注解解析器 */ //@AutoService(Processor.class) //@SupportedAnnotationTypes("study.com.aptlib.GenerateGS") //@SupportedSourceVersion(SourceVersion.RELEASE_7) public class GenerateGSProcessor extends AbstractProcessor {private Filer mFiler; /** * 初始化Processor和一些工具类 */ @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); mFiler = processingEnv.getFiler(); }/** * 返回该Processor能够处理的注解 */ @Override public Set getSupportedAnnotationTypes() { LinkedHashSet types = new LinkedHashSet<>(); types.add(GenerateGS.class.getCanonicalName()); return types; }/** * 返回Java的版本号 */ @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }/** * 真正处理注解的方法 */ @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { HashMap> nameMap = new HashMap<>(); Set annotatedElements = roundEnv.getElementsAnnotatedWith(GenerateGS.class); //遍历处理带注解的Element,把他们分类保存在Map中,key=包裹类 value=https://www.it610.com/article/类中所有使用注解的Element for (Element element : annotatedElements) { Element parent = element.getEnclosingElement(); String parentName = parent.getSimpleName().toString(); HashSet set = nameMap.get(parentName); if (set == null) { set = new HashSet<>(); } set.add(element); nameMap.put(parentName, set); }generateJavaFile(nameMap); return true; }/** * 根据Map生成Java文件 */ private void generateJavaFile(Map> map) { System.out.println("开始生成代码"); Set> nameSet = map.entrySet(); for(Map.Entry> entry : nameSet) { String className = entry.getKey(); Set fields = entry.getValue(); TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(className + "$Bean") .addModifiers(Modifier.PUBLIC); //遍历添加属性和对应的getter/setter方法 for (Element element : fields) { //只处理field if (element.getKind().isField()) { //获取字段名称 String fieldName = element.getSimpleName().toString(); //字段名称首字母变成大写 char[] cs = fieldName.toCharArray(); cs[0] -= 32; String firstUpperName = String.valueOf(cs); //获取字段类型 TypeName type = TypeName.get(element.asType()); //生成字段 FieldSpec fieldSpec = FieldSpec.builder(type, fieldName, Modifier.PRIVATE).build(); //生成getter/setter方法 MethodSpec getterMethod = MethodSpec.methodBuilder("get" + firstUpperName) .addModifiers(Modifier.PUBLIC) .returns(type) .addStatement("return " + fieldName) .build(); MethodSpec setterMethod = MethodSpec.methodBuilder("set" + firstUpperName) .addModifiers(Modifier.PUBLIC) .returns(TypeName.VOID) .addParameter(type, fieldName) .addStatement("this." + fieldName + " = " + fieldName) .build(); //给$Bean添加字段及对应的getter和setter方法 typeSpecBuilder.addField(fieldSpec) .addMethod(getterMethod) .addMethod(setterMethod); } } TypeSpec typeSpec = typeSpecBuilder.build(); JavaFile javaFile = JavaFile.builder("study.com.aptlib", typeSpec).build(); try { javaFile.writeTo(mFiler); System.out.println("生成" + className + "$Bean" + "类"); } catch (Exception e) { e.printStackTrace(); } } System.out.println("代码生成完毕"); } }

GenerateGSProcessor中主要有四个方法:init方法,getSupportedAnnotationTypes方法,getSupportedSourceVersion方法和process方法。代码注释中给出了这四种方法的作用,其中最主要的方法就是process方法, 这个方法就是用来解析注解的。而getSupportedAnnotationTypes和getSupportedSourceVersion两个方法可以用注解来替代,分别是@SupportedAnnotationTypes@SupportedAnnotationTypes,在GenerateGsProcessor的注释中可以看到他们。
有人会问,除了那两个注解之外,还有一个@AutoService注解,这个是干嘛的呢?
别着急,下面我们会介绍的。
3. 添加SPI配置文件 我们先来简单介绍一下SPI(服务提供接口Service Provider Interface)机制,主要做作用是为接口寻找服务实现。
举个栗子,我们现在有三个模块:common、A、B,并且A和B都依赖与common。现在,common模块中有个接口Fly(有一个fly方法)而A中定义Fly的实现类Bird,B中定义Fly的实现类Butterfly。
在A和B中都添加配置文件,A的配置文件中写上Bird的带包全名,B的配置文件中写上Butterfly带包全名,接着在需要使用的地方,A和B都可以使用下面一段代码:
ServiceLoader serviceLoader = ServiceLoader.load(Fly.class, Fly.class.getClassLoader()); Iterator it = serviceLoader.iterator(); if (it.hasNext()) { it.next().fly(); }

这样,在A中的效果就是Bird在飞,B中的效果是Butterfly在飞。有点类似于策略模式,可以通过配置文件动态加载。
接下来,总结一下配置方法:
1、定义接口和接口实现类 2、创建resources/META-INF/services目录 3、在该目录下创建一个文件,文件名为接口名(带包全名),内容为接口实现类的带包全名 4、在代码中通过ServiceLoader动态加载并且调用实现类的内部方法。

好,现在让我们回到APT技术来,APT技术中的Processor
就使用了SPI机制,接口是Process,实现类是GenerateGSProcessor,所以我们需要做下面几件事:
  • 在main目录下创建resources/META-INF/services目录

    Android注解&APT技术
    文章图片
    image.png
  • 在该目录下新建javax.annotation.processing.Processor文件

    Android注解&APT技术
    文章图片
    image.png
  • 在文件中添加内容study.com.aptlib.GenerateGSProcessor
study.com.aptlib.GenerateGSProcessor

到这里SPI配置完毕。
可能大家会觉得这种配置方式比较麻烦,对,确实比较麻烦。我们可以使用Google提供的auto-service库来简化这些操作:
compile 'com.google.auto.service:auto-service:1.0-rc4' compile 'com.google.auto:auto-common:0.10'

然后在GenerateGSProcessor类上添加注解:
@AutoService(Processor.class)

这就是上面提到的AutoService,用这个注解可以替代SPI的配置文件。
4. 在Android Module使用注解 【Android注解&APT技术】首先,添加项目依赖
annotationProcessor project(':aptlib') api project(':aptlib')

这里为什么要添加两次呢?
  • annotationProcessor:指定专门的注解解析库
  • api:表示添加注解依赖,因为我们的GenerateGs写在aptlib库,所以需要单独添加这个依赖,如果注解和解析器放在不同的module,就不需要这么写
接下来,在代码中使用注解:
public class Person { @GenerateGS private String name; @GenerateGS private int gender; @GenerateGS private String hobby; }

通过rebuild来编译我们的项目,就会生成Person$Bean类:
package study.com.aptlib; import java.lang.String; public class Person$Bean { private String hobby; private String name; private int gender; public String getHobby() { return hobby; }public void setHobby(String hobby) { this.hobby = hobby; }public String getName() { return name; }public void setName(String name) { this.name = name; }public int getGender() { return gender; }public void setGender(int gender) { this.gender = gender; } }

有没有很神奇。
对比运行时和编译时注解
在很多情况下,运行时注解和编译时注解可以实现相同的功能,比如依赖注入框架,我们既可以在运行时通过反射来初始化控件,也可以再编译时就生成控件初始化代码。那么,这两者有什么区别呢?
答:编译时注解性能比运行时注解好,运行时注解需要使用到反射技术,对程序的性能有一定影响,而编译时注解直接生成了源代码,运行过程中直接执行代码,没有反射这个过程。
很多框架的实现都是用到了编译时注解,如ButterKnife、EventBus、Dagger2等等。
项目中使用这些库的时候,会有一个比较让人疑惑的地方。就拿ButterKnife举例。
我们使用ButterKnife时,会添加依赖:
annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'

但我们在项目结构中怎么也找不到ButterKnifeProcessor和编译库的代码。这个是为什么呢?
经过一番研究,总算知道原因了,一些插件库、注解解析库并不会放在项目结构中,而是会放在gradle的缓存目录中:
/Users/user_zf/.gradle/caches/modules-2/files-2.1/com.jakewharton/butterknife-compiler/8.6.0/d3defb48a63aa0591117d0cec09f47a13fffda19,在这个路径中,总算找到了butterknife-compiler-8.6.0.jar
总结
经过上面的介绍,相信大家对注解有了比较全面的认识。各位同学可以尝试在项目开发过程中去使用注解,它可以大大提升我们的开发效率,减少不必要的重复代码。

    推荐阅读