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? = null


    override 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<String> getSupportedAnnotationTypes() {
        LinkedHashSet<String> types = new LinkedHashSet<>();
        types.add(GenerateGS.class.getCanonicalName());
        return types;
    }

    /**
     * 返回Java的版本号
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 真正处理注解的方法
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        HashMap<String, HashSet<Element>> nameMap = new HashMap<>();
        Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(GenerateGS.class);
        //遍历处理带注解的Element,把他们分类保存在Map中,key=包裹类 value=类中所有使用注解的Element
        for (Element element : annotatedElements) {
            Element parent = element.getEnclosingElement();
            String parentName = parent.getSimpleName().toString();
            HashSet<Element> 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<String, HashSet<Element>> map) {
        System.out.println("开始生成代码");
        Set<Map.Entry<String, HashSet<Element>>> nameSet = map.entrySet();
        for(Map.Entry<String, HashSet<Element>> entry : nameSet) {
            String className = entry.getKey();
            Set<Element> 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<Fly> serviceLoader = ServiceLoader.load(Fly.class, Fly.class.getClassLoader());
Iterator<Fly> 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目录


    image.png
  • 在该目录下新建javax.annotation.processing.Processor文件


    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使用注解

首先,添加项目依赖

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

总结

经过上面的介绍,相信大家对注解有了比较全面的认识。各位同学可以尝试在项目开发过程中去使用注解,它可以大大提升我们的开发效率,减少不必要的重复代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,837评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,704评论 2 59
  • 夯实 Java 基础 - 注解 不知道大家有没有一种感觉,当你想要了解某个知识点的时候,就会发现好多技术类 APP...
    醒着的码者阅读 1,052评论 4 7
  • 我在江南那年, 种下, 一颗花籽。 等待北方的你, 去欣赏花开。
    折玫人阅读 240评论 0 3
  • 很多系统方法都有版本支持的说明,所以对于支持多个系统版本,需要判断系统版本来执行方法。比如: 第一个方法只支持7-...
    罗淞阅读 1,560评论 0 0