通常的,注解在 Android 中有两种应用方式,一种方式是基于反射的,即在程序的运行期间获取类信息进行反射调用;另外一种是使用注解处理,在编译期间生成许多代码,而后在运行期间经过调用这些代码来实现目标功能。java
在本篇文章中,咱们会先重温一下 Java 的注解相关的知识,而后分别介绍一下上面两种方式的实际应用。android
Java 中的注解分红标准注解和元注解。标准注解是 Java 为咱们提供的预约义的注解,共有四种:@Override
、@Deprecated
、@SuppressWarnnings
和 @SafeVarags
。元注解是用来提供给用户自定义注解用的,共有五种(截止到Java8):@Target
、@Retention
、@Documented
、@Inherited
和 @Repeatable
,这里咱们重点介绍这五种元注解。git
不过,首先咱们仍是先看一下一个基本的注解的定义的规范。下面咱们自定义了一个名为UseCase
的注解,能够看出咱们用到了上面说起的几种元注解:github
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={METHOD, FIELD}) public @interface UseCase { public int id(); public String description() default "default value"; } 复制代码
这是一个普通的注解的定义。从上面咱们也能够总结出,在定义注解的时候,有如下几个地方须要注意:数据库
@interface
声明而且指定注解的名称;default
为指定的元素指定一个默认值,若是用户没有为其指定值,就使用默认值。好的,看完了一个基本的注解的定义,咱们来看一下上面用到的 Java 元注解的含义。api
@Target
用来指定注解可以修饰的对象的类型。由于 @Target
自己也是一个注解,因此你能够在源码中查看它的定义。该注解接收的参数是一个 ElementType
类型的数组,因此,就是说咱们自定义的注解能够应用到多种类型的对象,而对象的类型由 ElementType
定义。ElementType
是一个枚举,它的枚举值以下:数组
因此,好比根据上面的内容,咱们能够直到咱们的自定义注解 @UseCase
只能应用于方法和字段。缓存
用来指定注解的保留策略,好比有一些注解,当你在本身的代码中使用它们的时候你会把它写在方法上面,可是当你反编译以后却发现这些注解不在了;而有些注解反编译以后依然存在,发生这种状况的缘由就是在使用该注解的时候指定了不一样的参数。markdown
与 @Target
相同的是这个注解也使用枚举来指定值的类型,不一样的是它只能指定一个值,具体能够看源码。这里它使用的是 RetentionPolicy
枚举,它的几个值的含义以下:app
当咱们在 Android 中使用注解的时候,一种是在运行时使用的,因此咱们要用 RUNTIME
;另外一种是在编译时使用的,因此咱们用 CLASS
。
这三个元注解的功能比较简单和容易理解,这里咱们一块儿给出便可:
@Documented
表示此注解将包含在 javadoc 中;@Inherited
表示容许子类继承父类的注解;@Repeatable
是 Java8 中新增的注解,表示指定的注解能够重复应用到指定的对象上面。上文,咱们回顾了 Java 中注解相关的知识点,相信你已经对注解的内容有了一些了解,那么咱们接下来看一下注解在实际开发中的两种应用方式。
在我开始为个人开源项目 马克笔记 编写数据库的时候,我考虑了使用注解来为数据库对象指定字段的信息,并根据这心信息来拼接出建立数据库表的 SQL 语句。当时也想用反射来动态为每一个字段赋值的,可是考虑到反射的性能比较差,最终放弃了这个方案。可是,使用注解处理的方式能够完美的解决咱们的问题,即在编译的时候动态生成一堆代码,实际赋值的时候调用这些方法来完成。这先后两种方案就是咱们今天要讲的注解的两种使用方式。
这里为了演示基于反射的注解的使用方式,咱们写一个小的 Java 程序,要实现的目的是:定义两个个注解,一个应用于方法,一个应用于字段,而后咱们使用这两个注解来定义一个类。咱们想要在代码中动态地打印出使用了注解的方法和字段的信息和注解信息。
这里咱们先定义两个注解,应用于字段的 @Column
注解和应用于方法 @Important
注解:
@Target(value = {ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Column { String name(); } @Target(value = {ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface WrappedMethod { // empty } 复制代码
而后咱们定义了一个Person类,并使用注解为其中的部分方法和字段添加注解:
private static class Person { @Column(name = "id") private int id; @Column(name = "first_name") private String firstName; @Column(name = "last_name") private String lastName; private int temp; @WrappedMethod() public String getInfo() { return id + " :" + firstName + " " + lastName; } public String method() { return "Nothing"; } } 复制代码
而后,咱们使用Person类来获取该类的字段和方法的信息,并输出具备注解的部分:
public static void main(String...args) { Class<?> c = Person.class; Method[] methods = c.getDeclaredMethods(); for (Method method : methods) { if (method.getAnnotation(WrappedMethod.class) != null) { System.out.print(method.getName() + " "); } } System.out.println(); Field[] fields = c.getDeclaredFields(); for (Field field : fields) { Column column = field.getAnnotation(Column.class); if (column != null) { System.out.print(column.name() + "-" + field.getName() + ", "); } } } 复制代码
输出结果:
getInfo
id-id, first_name-firstName, last_name-lastName,
复制代码
在上面的代码的执行结果,咱们能够看出:使用了注解和反射以后,咱们成功的打印出了使用了注解的字段。这里咱们须要先获取指定的类的 Class 类型,而后用反射获取它的全部方法和字段信息并进行遍历,经过判断它们的 getAnnotation()
方法的结果来肯定这个方法和字段是否使用了指定类型的注解。
上面的代码能够解决一些问题,但同时,咱们还有一些地方须要注意:
也许你以前已经使用过 ButterKnife 这样的注入框架,不知道你是否记得在 Gradle 中引用它的时候加入了下面这行依赖:
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' 复制代码
这里的 annotationProcessor 就是咱们这里要讲的注解处理。本质上它会在编译的时候,在你调用 ButterKnife.bind(this);
方法的那个类所在的包下面生成一些类,当调用 ButterKnife.bind(this);
的时候实际上就完成了为使用注解的方法和控件绑定的过程。也就是,本质上仍是调用了 findViewById()
,只是这个过程被隐藏了,不用你来完成了,仅此而已。
下面,咱们就使用注解处理的功能来制做一个相似于 ButterKnife 的简单库。不过,在那以前咱们还须要作一些准备——一些知识点须要进行说明。即 Javapoet和AbstractProcessor
。
Javapoet 是一个用来生成 .java
文件的 Java API,由 Square 开发,你能够在它的 Github 主页中了解它的基本使用方法。它的好处就是对方法、类文件和代码等的拼接进行了封装,有了它,咱们就不用再按照字符串的方式去拼接出一段代码了。相比于直接使用字符串的方式,它还能够生成代码的同时直接 import
对应的引用,能够说是很是方便、快捷的一个库了。
这里的 AbstractProcessor
是用来生成类文件的核心类,它是一个抽象类,通常使用的时候咱们只要覆写它的方法中的4个就能够了。下面是这些方法及其定义:
init
:在生成代码以前被调用,能够从它参数 ProcessingEnvironment
获取到很是多有用的工具类;process
:用于生成代码的 Java 方法,能够从参数 RoundEnvironment
中获取使用指定的注解的对象的信息,并包装成一个 Element
类型返回;getSupportedAnnotationTypes
:用于指定该处理器适用的注解;getSupportedSourceVersion
:用来指定你使用的 Java 的版本。这几个方法中,除了 process
,其余方法都不是必须覆写的方法。这里的 getSupportedAnnotationTypes
和 getSupportedSourceVersion
可使用注 @SupportedAnnotationTypes
和 @SupportedSourceVersion
来替换,可是不建议这么作。由于前面的注解接收的参数是字符串,若是你使用了混淆可能就比较麻烦,后面的注解只能使用枚举,相对欠缺了灵活性。
另外一个咱们须要特别说明的地方是,继承 AbstractProcessor
并实现了咱们本身的处理器以后还要对它进行注册才能使用。一种作法是在与 java
同的目录下面建立一个 resources
文件夹,并在其中建立 META-INF/service
文件夹,而后在其中建立一个名为javax.annotation.processing.Processor
的文件,并在其中写上咱们的处理器的完整路径。另外一种作法是使用谷歌的 @AutoService
注解,你只须要在本身的处理器上面加上 @AutoService(Processor.class)
一行代码便可。固然,前提是你须要在本身的项目中引入依赖:
compile 'com.google.auto.service:auto-service:1.0-rc2' 复制代码
按照后面的这种方式同样会在目录下面生成上面的那个文件,只是这个过程不须要咱们来操做了。你能够经过查看buidl出的文件来找到生成的文件。
在定制以前,咱们先看一下程序的最终执行结果,也许这样会更有助于理解整个过程的原理。咱们程序的最终的执行结果是,在编译的时候,在使用咱们的工具的类的相同级别的包下面生成一个类。以下图所示:
这里的 me.shouheng.libraries
是咱们应用 MyKnife 的包,这里咱们在它下面生成了一个名为 MyKnifeActivity?Injector
的类,它的定义以下:
public class MyKnifeActivity?Injector implements Injector<MyKnifeActivity> { @Override public void inject(final MyKnifeActivity host, Object source, Finder finder) { host.textView=(TextView)finder.findView(source, 2131230952); View.OnClickListener listener; listener = new View.OnClickListener() { @Override public void onClick(View view) { host.OnClick(); } }; finder.findView(source, 2131230762).setOnClickListener(listener); } } 复制代码
由于咱们应用 MyKnife
的类是 MyKnifeActivity
,因此这里就生成了名为 MyKnifeActivity?Injector
的类。经过上面的代码,能够看出它实际上调用了 Finder
的方法来为咱们的控件 textView
赋值,而后使用控件的 setOnClickListener()
方法为点击事件赋值。这里的 Finder
是咱们封装的一个对象,用来从指定的源中获取控件的类,本质上仍是调用了指定源的 findViewById()
方法。
而后,与 ButterKnife 相似的是,在使用咱们的工具的时候,也须要在 Activity 的 onCreate()
中调用 bind()
方法。这里咱们看下这个方法作了什么操做:
public static void bind(Object host, Object source, Finder finder) { String className = host.getClass().getName(); try { Injector injector = FINDER_MAPPER.get(className); if (injector == null) { Class<?> finderClass = Class.forName(className + "?Injector"); injector = (Injector) finderClass.newInstance(); FINDER_MAPPER.put(className, injector); } injector.inject(host, source, finder); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } 复制代码
从上面的代码中能够看出,调用 bind()
方法的时候会从 FINDER_MAPPER
尝试获取指定 类名?Injector
的文件。因此,若是说咱们应用 bind()
的类是 MyKnifeActivity
,那么这里获取到的类将会是 MyKnifeActivity?Injector
。而后,当咱们调用 inject
方法的时候就执行了咱们上面的注入操做,来完成对控件和点击事件的赋值。这里的 FINDER_MAPPER
是一个哈希表,用来缓存指定的 Injector
的。因此,从上面也能够看出,这里进行值绑定的时候使用了反射,因此,在应用框架的时候还须要对混淆进行处理。
OK,看完了程序的最终结果,咱们来看一下如何生成上面的那个类文件。
首先,咱们须要定义注解用来提供给用户进行事件和控件的绑定,
@Target(ElementType.FIELD) @Retention(RetentionPolicy.CLASS) public @interface BindView { int id(); } @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface OnClick { int[] ids(); } 复制代码
如上面的代码所示,能够看出咱们分别用了 ElementType.FIELD
和 ElementType.METHOD
指定它们是应用于字段和方法的,而后用了 RetentionPolicy.CLASS
标明它们不会被保留到程序运行时。
而后,咱们须要定义 MyKnife
,它提供了一个 bind()
方法,其定义以下:
public static void bind(Object host, Object source, Finder finder) { String className = host.getClass().getName(); try { Injector injector = FINDER_MAPPER.get(className); if (injector == null) { Class<?> finderClass = Class.forName(className + "?Injector"); injector = (Injector) finderClass.newInstance(); FINDER_MAPPER.put(className, injector); } injector.inject(host, source, finder); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } 复制代码
这里的三个参数的含义分别是:host
是调用绑定方法的类,好比 Activity 等;source
是从用来获取绑定的值的数据源,通常理解是从 source
中获取控件赋值给 host
中的字段,一般二者是相同的;最后一个参数 finder
是一个接口,是获取数据的方法的一个封装,有两默认的实现,一个是 ActivityFinder
,一个是 ViewFinder
,分别用来从 Activity 和 View 中查找控件。
咱们以前已经讲过 bind()
方法的做用,即便用反射根据类名来获取一个 Injector
,而后调用它的 inject()
方法进行注入。这里的 Injector
是一个接口,咱们不会写代码去实现它,而是在编译的时候让编译器直接生成它的实现类。
在介绍 Javapoet 和 AbstractProcessor 的时候,咱们提到过 Element,它封装了应用注解的对象(方法、字段或者类等)的信息。咱们能够从 Element 中获取这些信息并将它们封装成一个对象来方便咱们调用。因而就产生了 BindViewField
和 OnClickMethod
两个类。它们分别用来描述使用 @BindView
注解和使用 @OnClick
注解的对象的信息。此外,还有一个 AnnotatedClass
,它用来描述使用注解的整个类的信息,而且其中定义了List<BindViewField>
和 List<OnClickMethod>
,分别用来存储该类中应用注解的字段和方法的信息。
与生成文件和获取注解的对象信息相关的几个字段都是从 AbstractProcessor 中获取的。以下面的代码所示,咱们能够从 AbstractProcessor 的 init()
方法的 ProcessingEnvironment
中获取到 Elements
、Filer
和 Messager
。它们的做用分别是:Elements
相似于一个工具类,用来从 Element
中获取注解对象的信息;Filer
用来支持经过注释处理器建立新文件;Messager
提供注释处理器用来报告错误消息、警告和其余通知的方式。
@Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); elements = processingEnvironment.getElementUtils(); messager = processingEnvironment.getMessager(); filer = processingEnvironment.getFiler(); } 复制代码
而后在 AbstractProcessor 的 process()
方法中的 RoundEnvironment
参数中,咱们又能够获取到指定注解对应的 Element
信息。代码以下所示:
private Map<String, AnnotatedClass> map = new HashMap<>(); @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { map.clear(); try { // 分别用来处理咱们定义的两种注解 processBindView(roundEnvironment); processOnClick(roundEnvironment); } catch (IllegalArgumentException e) { return true; } try { // 为缓存的各个使用注解的类生成类文件 for (AnnotatedClass annotatedClass : map.values()) { annotatedClass.generateFinder().writeTo(filer); } } catch (Exception e) { e.printStackTrace(); } return true; } // 从RoundEnvironment中获取@BindView注解的信息 private void processBindView(RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) { AnnotatedClass annotatedClass = getAnnotatedClass(element); BindViewField field = new BindViewField(element); annotatedClass.addField(field); } } // 从RoundEnvironment中获取@OnClick注解的信息 private void processOnClick(RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) { AnnotatedClass annotatedClass = getAnnotatedClass(element); OnClickMethod method = new OnClickMethod(element); annotatedClass.addMethod(method); } } // 获取使用注解的类的信息,先尝试从缓存中获取,缓存中没有的话就实例化一个并放进缓存中 private AnnotatedClass getAnnotatedClass(Element element) { TypeElement encloseElement = (TypeElement) element.getEnclosingElement(); String fullClassName = encloseElement.getQualifiedName().toString(); AnnotatedClass annotatedClass = map.get(fullClassName); if (annotatedClass == null) { annotatedClass = new AnnotatedClass(encloseElement, elements); map.put(fullClassName, annotatedClass); } return annotatedClass; } 复制代码
上面的代码的逻辑是,在调用 process()
方法的时候,会根据传入的 RoundEnvironment
分别处理两种注解。两个注解的相关信息都会被解析成 List<BindViewField>
和 List<OnClickMethod>
,而后把使用注解的整个类的信息统一放置在 AnnotatedClass
中。为了提高程序的效率,这里用了缓存来存储类信息。最后,咱们调用了 annotatedClass.generateFinder()
获取一个JavaFile,并调用它的 writeTo(filer)
方法生成类文件。
上面的代码重点在于解析使用注解的类的信息,至于如何根据类信息生成类文件,咱们还须要看下 AnnotatedClass
的 generateFinder()
方法,其代码以下所示。这里咱们用了以前提到的 Javapoet 来帮助咱们生成类文件:
public JavaFile generateFinder() { // 这里用来定义inject方法的签名 MethodSpec.Builder builder = MethodSpec.methodBuilder("inject") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(TypeName.get(typeElement.asType()), "host", Modifier.FINAL) .addParameter(TypeName.OBJECT, "source") .addParameter(TypeUtils.FINDER, "finder"); // 这里用来定义inject方法中@BindView注解的绑定过程 for (BindViewField field : bindViewFields) { builder.addStatement("host.$N=($T)finder.findView(source, $L)", field.getFieldName(), ClassName.get(field.getFieldType()), field.getViewId()); } // 这里用来定义inject方法中@OnClick注解的绑定过程 if (onClickMethods.size() > 0) { builder.addStatement("$T listener", TypeUtils.ONCLICK_LISTENER); } for (OnClickMethod method : onClickMethods) { TypeSpec listener = TypeSpec.anonymousClassBuilder("") .addSuperinterface(TypeUtils.ONCLICK_LISTENER) .addMethod(MethodSpec.methodBuilder("onClick") .addAnnotation(Override.class) .addModifiers(Modifier.PUBLIC) .returns(TypeName.VOID) .addParameter(TypeUtils.ANDROID_VIEW, "view") .addStatement("host.$N()", method.getMethodName()) .build()) .build(); builder.addStatement("listener = $L", listener); for (int id : method.getIds()) { builder.addStatement("finder.findView(source, $L).setOnClickListener(listener)", id); } } // 这里用来获取要生成的类所在的包的信息 String packageName = getPackageName(typeElement); String className = getClassName(typeElement, packageName); ClassName bindClassName = ClassName.get(packageName, className); // 用来最终组装成咱们要输出的类 TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "?Injector") .addModifiers(Modifier.PUBLIC) .addSuperinterface(ParameterizedTypeName.get(TypeUtils.INJECTOR, TypeName.get(typeElement.asType()))) .addMethod(builder.build()) .build(); return JavaFile.builder(packageName, finderClass).build(); } 复制代码
上面就是咱们用来最终生成类文件的方法,这里用了 Javapoet ,若是对它不是很了解能够到 Github 上面了解一下它的用法。
这样咱们就完成了整个方法的定义。
使用咱们定义的 MyKnife ,咱们只须要在 Gradle 里面引入咱们的包便可:
implementation project(':knife-api') implementation project(':knife-annotation') annotationProcessor project(':knife-compiler') 复制代码
也许你在有的地方看到过要使用 android-apt
引入注解处理器,其实这里的annotationProcessor
与之做用是同样的。这里推荐使用 annotationProcessor
,由于它更加简洁,不须要额外的配置,也是官方推荐的使用方式。
而后,咱们只须要在代码中使用它们就能够了:
public class MyKnifeActivity extends CommonActivity<ActivityMyKnifeBinding> { @BindView(id = R.id.tv) public TextView textView; @OnClick(ids = {R.id.btn}) public void OnClick() { ToastUtils.makeToast("OnClick"); } @Override protected int getLayoutResId() { return R.layout.activity_my_knife; } @Override protected void doCreateView(Bundle savedInstanceState) { MyKnife.bind(this); textView.setText("This is MyKnife demo!"); } } 复制代码
这里有几个地方须要注意:
protected
,由于咱们使用了直接引用的方式,而生成的文件和上面的类包相同,因此至少应该保证包级别访问权限;这里咱们总结一下按照第二种方式使用注解的时候须要步骤:
注解常见的第三种使用方式是用来取代枚举的。由于枚举相比于普通的字符串或者整数会带来额外的内存占用,所以对于 Android 这种对内存要求比较高的项目而言就须要对枚举进行优化。固然,咱们使用字符串常量或者整数常量替换枚举就能够了,可是这种方式的参数能够接受任意字符串和整型的值。假如咱们但愿可以像枚举同样对传入的参数的范围进行限制,就须要使用枚举了!
好比,咱们须要对相机的闪光灯参数进行限制,每一个参数经过一个整型的变量指定。而后,咱们经过一个方法接受整型的参数,并经过注解来要求指定的整型必须在咱们上述声明的整型范围以内。咱们能够这样定义,
首先,咱们定义一个类 Camera 用来存储闪光灯的枚举值和注解,
public final class Camera { public static final int FLASH_AUTO = 0; public static final int FLASH_ON = 1; public static final int FLASH_OFF = 2; public static final int FLASH_TORCH = 3; public static final int FLASH_RED_EYE = 4; @IntDef({FLASH_ON, FLASH_OFF, FLASH_AUTO, FLASH_TORCH, FLASH_RED_EYE}) @Retention(RetentionPolicy.SOURCE) public @interface FlashMode { } } 复制代码
如上所示,这样咱们就定义了枚举值及其注解。而后,咱们能够这样使用该注解,
public final class Configuration implements Parcelable { @Camera.FlashMode private int flashMode = Camera.FLASH_AUTO; public void setFlashMode(@Camera.FlashMode int flashMode) { this.flashMode = flashMode; } } 复制代码
这样当咱们传入的参数不在咱们自定义枚举的 @IntDef
指定的范围以内的时候,IDE 会自动给出提示。
以上就是注解的两种比较常见的使用方式。第一种是经过反射来进行的,由于反射自己的效率比较低,因此比较适用于发射比较少的场景;第二种方式是在编译期间经过编译器生成代码来实现的,相比于第一种,它仍是可能会用到反射的,可是没必要在运行时对类的每一个方法和字段进行遍历,于是效率高得多。
以上。
获取源码:Android-references