什么是注解

java.lang.annotation,接口 Annotation,在JDK5.0及以后版本引入。

注解处理器是 javac 的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。一个注解的注解处理器,以 Java 代码(或者编译过的字节码)作为输入,生成文件(通常是 .java 文件)作为输出。这些生成的 Java 代码是在生成的 .java 文件中,所以你不能修改已经存在的 Java 类,例如向已有的类中添加方法。这些生成的 Java 文件,会同其他普通的手动编写的 Java 源代码一样被 javac 编译。

基本的注解

  • @ Override--限定重写父类方法
  • @ Deprecated--标示已过时
  • @ SuppressWarning--抑制编译器警告
  • @ SafeVarargs--这货与Java7里面的堆污染有关,具体想了解的,到传送这里

JDK的元注解

JDK除了提供上述的几种基本的注释外,还提供了几种注释,用于修饰其他的注解定义

  1. @retention 这个是决定你注释存活的时间的,它包含一个RetationPolicy的值成员变量,用于指定它所修饰的注释保留时间,一般有:

      • Retationpolicy.CLASS:  编译器将把注释记录在类文件中,不过当Java的程序执行的时候,JVM将抛弃它。
      • Retationpolicy.SOURCE:  Annotation只保留在原代码中,当编译器编译的时候就会抛弃它。
      • Retationpolicy.RUNTIME:  在Retationpolicy.CLASS的基础上,JVM执行的时候也不会抛弃它,所以我们一般在程序中可以通过反射来获得这个注解,然后进行处理。
  2. @target 这个注解一般用来指定被修饰的注释修饰哪些元素,这个注解也包含一个值变量:

      • ElementType.ANNOTATION_TYPE:  指定该注释只能修饰注释。
      • ElementType.CONSTRUCTOR:  指定只能修饰构造器。
      • ElementType.FIELD:  指定只能成员变量。
      • ElementType.LOCAL_VARIABLE:  指定只能修饰局部变量。
      • ElementType.METHOD:  指定只能修饰方法。
      • ElementType.PACKAGE:  指定只能修饰包定义。
      • ElementType.PARAMETER:  指定只能修饰参数。
      • ElementType.TYPE:  指定可以修饰类,接口,枚举定义。
  3. @Document 这个注解修饰的注释类可以被 javadoc 的工具提取成文档

  4. @Inherited 被他修饰的注解具有继承性

自定义注释

上面讲了一些JDK自带的注释,那么我们现在就可以用这些JDK自带的注释来实现一些我们想要的功能。先一步一步地模仿 butterknife 的实现吧。

定义一个注解:

@Target(ElementType.FIELD) // 用于成员变量
@Retention(RetentionPolicy.CLASS) //注解保留在 class 文件, 当Java的程序执行的时候,JVM将抛弃它
public @interface BindView {
int value();
}
注解定义的方式就是 @interface 和接口的定义方式就少一个 @ 哦,不要搞混了。里面有一个变量值时,就是我们使用的时候 @BindView 
(R.id.textView) 指定的 R.id.textView id,旨在自动注入 view 的 id。

注解处理器

先来说下注解处理器 AbstractProcessor。它是 javac 的一个工具,用来在编译时扫描和处理注解 Annotation,你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。

一个注解的注解处理器,以 Java 代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这些由注解器生成的.java代码和普通的.java一样,可以被javac编译。

因为 AbstractProcessor 是 javac 中的一个工具,所以在 Android 的工程下没法直接调用。

新建一个 java library

在 build.gradle 引入相关 jar :

apply plugin: 'java-library'

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api 'com.squareup:javapoet:1.7.0'
api 'com.google.auto.service:auto-service:1.0-rc2'
api project(':lib-annotation')
} sourceCompatibility = "1.7"
targetCompatibility = "1.7" //指定编译的编码
tasks.withType(JavaCompile){
options.encoding = "UTF-8"
}

javapoet 和 auto-service 后面会讲到,这两个在注解处理器中有着极大的作用。

引入之后,就开始编写注解处理器了。

@AutoService(Processor.class)
public class CustomProcessor extends AbstractProcessor {
private static final String TAG = "CustomProcessor";
// 文件相关的辅助类
private Filer mFiler;
// 元素相关的辅助类
private Elements mElements;
// 元素相关的辅助类
private Elements mElementUtils; /**
* 解析的目标注解集合
*/
private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>(); @Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mElementUtils = processingEnvironment.getElementUtils();
mFiler = processingEnvironment.getFiler();
} //核心处理逻辑,相当于java中的主函数main(),你需要在这里编写你自己定义的注解的处理逻辑
//返回值 true时表示当前处理,不允许后续的注解器处理
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mAnnotatedClassMap.clear();
try {
processBindView(roundEnvironment);
} catch (IllegalArgumentException e) {
return true;
} try {
for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
annotatedClass.generateFinder().writeTo(mFiler);
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
} @Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
     // 标明该注解处理器是为了处理 BindView 注解的
types.add(BindView.class.getCanonicalName());
return types;
} private void processBindView(RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
// element = tv1;
AnnotatedClass annotatedClass = getAnnotatedClass(element);
BindViewField field = new BindViewField(element);
annotatedClass.addField(field);
       // 通过上面方法调用,可以获取到注解元素,以及和注解元素相关的类名,通过注解元素获得被注解的成员变量名,后续会对其进行初始化
}
} private AnnotatedClass getAnnotatedClass(Element element) {
     // 通过注解元素获取其封装类,获得类的引用
TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
// encloseElement.getSimpleName() = MainActivity;
String fullClassName = encloseElement.getQualifiedName().toString(); // com.sjq.recycletest.MainActivity
AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
if (annotatedClass == null) {
       // 存到map当中,不用每次都生成一次,这样一个类里面有多个注解的时候,可以加快处理速度
annotatedClass = new AnnotatedClass(encloseElement, mElementUtils);
mAnnotatedClassMap.put(fullClassName, annotatedClass);
}
return annotatedClass;
} @Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
  • init(ProcessingEnvironment processingEnvironment)每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入 ProcessingEnviroment 参数.ProcessingEnviroment 提供很多有用的工具类 Types 和 Filer。后面我们将看到详细的内容。

  • process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)这相当于每个处理器的主函数main()。你在这里写你的扫描,评估和处理注解的代码,以及生成Java文件。输入参数 RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。

  • getSupportedAnnotationTypes()这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换​​句话说,你在这里定义你的注解处理器注册到哪些注解上。

  • getSupportedSourceVersion()用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 7 的话,你也可以返回 SourceVersion.RELEASE_7。推荐你使用前者。

在类前面,使用了 AutoService 注解,这个在 build.gradle 中已经引入。AutoService 注解处理器是 Google 开发的,用来生成 META-INF/services/ javax.annotation.processing.Processor 文件的,你只需要在你定义的注解处理器上添加 @AutoService(Processor.class) 就可以了,简直不能再方便了。

注解器处理流程

首先我们先简单的说明一下 porcess 的处理流程:

  1. 遍历 env,得到我们需要的元素列表

  2. 将元素列表封装成对象,方便之后的处理,比如获取元素的各种属性等

  3. 通过 JavaPoet 库将对象以我们期望的形式生成 java 文件,来处理注解

第一步:遍历env,得到我们需要的元素列

if (element.getKind() == ElementKind.FEILD) {
// 显示转换元素类型
TypeElement typeElement = (TypeElement) element;
// 输出元素名称
System.out.println(typeElement.getSimpleName());
// 输出注解属性值
System.out.println(typeElement.getAnnotation(BindView.class).value());
}
}

上面的代码和 processBindView 的代码是一样的。判断元素类型,在进一步处理。有些注解可能对类和方法是同时生效的,这时候,判断类型分别处理就显得非常有必要了。

getElementsAnnotatedWith 能够获取到添加该注解的所有元素列表

第二步:将元素列表封装成对象,方便之后的处理

其实不进行封装也是可以的,但是这样当我们在使用的时候,可能就需要在不同的地方写很多重复的代码,为此,可以进一步封装,当我们需要获取元素属性的时候,直接调用相关方法即可。

新建类 BindViewField.class 用来保存自定义注解 BindView 相关的属性,后续需要元素上的信息都可以从该类获取。

public class BindViewField {
private VariableElement mFieldElement; private int mResId; public BindViewField(Element element) throws IllegalArgumentException {
if (element.getKind() != ElementKind.FIELD) {
throw new IllegalArgumentException(String.format("Only field can be annotated with @%s",
BindView.class.getSimpleName()));
}
     // 该注解用于成员变量的,因此需要进行转化
mFieldElement = (VariableElement) element;
     // 在进一步转化为注解类型
BindView bindView = mFieldElement.getAnnotation(BindView.class);
mResId = bindView.value();
if (mResId < 0) {
throw new IllegalArgumentException(String.format("value() in %s for field % is not valid",
BindView.class.getSimpleName(), mFieldElement.getSimpleName()));
}
} public Name getFieldName() {
return mFieldElement.getSimpleName();
} public int getResId() {
return mResId;
} public TypeMirror getFieldType() {
return mFieldElement.asType();
}
}

上述的 BindViewField 只能表示一个自定义注解 bindView 对象。很多时候,会同时存在很多其他注解,每一种注解都需要一个单独对象来管理属性。而一个类中很可能会有多个自定义注解,因此,对于在同一个类里面的注解,我们可以创建一个对象来进行管理,这就是 Annotation.class。

public class AnnotatedClass {

    //类
public TypeElement mClassElement; //类内的注解变量
public List<BindViewField> mFiled; //元素帮助类
public Elements mElementUtils; public AnnotatedClass(TypeElement classElement, Elements elementUtils) {
this.mClassElement = classElement;
this.mElementUtils = elementUtils;
this.mFiled = new ArrayList<>();
} //添加注解变量
public void addField(BindViewField field) {
mFiled.add(field);
} //获取包名
public String getPackageName(TypeElement type) {
return mElementUtils.getPackageOf(type).getQualifiedName().toString();
} //获取类名
private static String getClassName(TypeElement type, String packageName) {
int packageLen = packageName.length() + 1;
// type.getQualifiedName().toString() = com.sjq.recycletest.MainActivity
return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
}
}

第三步: 通过 JavaPoet 库将对象以我们期望的形式生成 java 文件

通过上述两步成功获取了自定义注解的元素对象,但是还是缺少一步关键的步骤,缺少一步 findViewById(),实际上 ButterKnife 这个很出名的库也并没有省略 findViewById()这一个步骤,只是在编译的时候,在 build/generated/source/apt/debug 下生成了一个文件,帮忙执行了findViewById()这一行为而已。

同样的,我们这里也需要生成一个 java 文件,采用的是 JavaPoet 这个库。具体的使用 参考链接

public JavaFile generateFinder() {

        //构建 inject 方法
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
.addParameter(TypeName.OBJECT, "source")
.addParameter(Utils.FINDER, "finder"); //inject函数内的核心逻辑,
// host.btn1=(Button)finder.findView(source,2131427450); ----生成代码
// host.$N=($T)finder.findView(source,$L) ----原始代码
// 对比就会发现这里执行了实际的findViewById绑定事件
for (BindViewField field : mFiled) {
methodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)", field.getFieldName()
, ClassName.get(field.getFieldType()), field.getResId());
} String packageName = getPackageName(mClassElement); // com.sjq.recycletest
String className = getClassName(mClassElement, packageName);
ClassName bindClassName = ClassName.get(packageName, className); // bindClassName.toString() com.sjq.recycletest.MainActivity //构建类对象,注意此处的 $$Injector,生成的类名是由我们自己来控制的
TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(Utils.INJECTOR, TypeName.get(mClassElement.asType()))) //继承接口
.addMethod(methodBuilder.build()) // 添加方法
.build(); return JavaFile.builder(packageName, finderClass).build();
}

上述代码先生成一个方法名,再添加函数体,接着把这个方法添加到一个类当中,这个类名是按照一定的规则拼接的,这也是后面采用反射获取生成类名的关键所在。

到这里,大部分逻辑都已实现,用来绑定控件的辅助类也已通关 JavaPoet 生成了,只差最后一步,宿主注册,如同 ButterKnife 一般,ButterKnife.bind(this)

编写调用接口

在 annotation-api 下新建 android library。

注入接口 Injector

最终会调用该方法来实现注解。

public interface Injector<T> {
void inject(T host, Object source, Finder finder);
}

宿主通用接口Finder(方便之后扩展到view和fragment)

public interface Finder {

    Context getContext(Object source);

    View findView(Object source, int id);
}

activity实现类 ActivityFinder

顾名思义,就是通过 activity 的 findViewById 来找到某个 view。

public class ActivityFinder implements Finder{

    @Override
public Context getContext(Object source) {
return (Activity) source;
} @Override
public View findView(Object source, int id) {
return ((Activity) (source)).findViewById(id);
}
}

核心实现类 ButterKnife

public class ButterKnife {
  //
private static final ActivityFinder finder = new ActivityFinder();
  // 用于存储已经绑定的class,避免重复绑定
private static Map<String, Injector> FINDER_MAP = new HashMap<>(); public static void bind(Activity activity) {
bind(activity, activity);
} private static void bind(Object host, Object source) {
bind(host, source, finder);
} private static void bind(Object host, Object source, Finder finder) {
String className = host.getClass().getName();
try {
Injector injector = FINDER_MAP.get(className);
if (injector == null) {
          // 此处拿到的类名就是通过注解生成的中间处理类,即 MainActivity$$Injector
Class<?> finderClass = Class.forName(className + "$$Injector");
          // 通过反射拿到class实例
injector = (Injector) finderClass.newInstance();
FINDER_MAP.put(className, injector);
}
injector.inject(host, source, finder);
} catch (Exception e) {
e.printStackTrace();
}
}
}

在 bind 方法内部,通过一定的规则拼接最后生成的 .java 文件类名,然后通过反射的方法拿到实例,最后,调用 injector.inject 的方法来完成初始化,该方法就是得通过注解来生成的。

主工程下调用

对应的按钮可以直接使用,不需要findViewById(),这样我们可以少写很多同样代码,逻辑上也变得非常清楚。

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.annotation_tv)
public TextView tv1; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
     // tv1 的初始化过程就是在bind过程中完成的
ButterKnife.bind(this);
tv1.setText("annotation_demo");
}
}

选择 build 下的 make project,会进行编译:

最终生成的类名如下:

public class MainActivity$$Injector implements Injector<MainActivity> {
@Override
public void inject(final MainActivity host, Object source, Finder finder) {
  // 最终还是通过 findViewById 来对 tv1 进行初始化
host.tv1=(TextView)finder.findView(source,2131165300);
}
}

到此,该实例讲解结束。

总结:

下面会对上面的实例的思想进行总结,方便大家进一步理解其实现原理:

1、先看代码层面的。可以看到在主工程先对成员变量 tv1 添加了注解,获得了 view 的 id;其次先调用 bind 方法,获取到 activity 实例。接着就开始调用 tv1.setText。可是有没有发现,tv1 没有初始化。其初始化过程就是在 bind 这个过程当中。

2、注解生成代码过程。通过获取成员变量所属的类,根据类名和命名规则获取最后注解会生成的类名,通过反射的形式,调用其中的 inject 方法。inject 方法中会有 activity 的 this 引用,通过 findViewById 方法,即可为 tv1 初始化。这样后面调用  tv1.setText 就不会出现空指针了。

项目源码:https://download.csdn.net/download/szengjiaqi/10629127

不能下载的,留下邮箱,发你

参考文献:

1、Android的编译时注解APT实战(AbstractProcessor)

2、浅谈Android下的注解

总结:其实你会发现最终被注解的 id 也还是通过 findViewById 方法来查找资源的。假设我们不使用注解,将生成的类放到外层来,直接使用的话,这样子就得在activity中引入更多的类,代码上也会更加混乱,不好维护。但是理解上可能比注解更好理解吧。

在假设,如果我们不是在 activty 使用注解呢,对于一般的 view 或者 其他类来使用的话可以吗?可以的,你只需要传入持有 findViewById 对象即可。比如 rootview.

使用注解的方式,让代码逻辑变得更加清楚,依赖也会降低。但是对于不了解的注解的同学,可能代码看起来会比较困难,但是使用很容易。

最后,我们想想,什么时候推荐使用注解呢?个人觉得是重复工作比较多的时候,这时候是需要注解的,因为工作重复,注解可以很方便就完成。

仿照 ButterKnife 的 Android 注解实例的更多相关文章

  1. Android注解使用之通过annotationProcessor注解生成代码实现自己的ButterKnife框架

    前言: Annotation注解在Android的开发中的使用越来越普遍,例如EventBus.ButterKnife.Dagger2等,之前使用注解的时候需要利用反射机制势必影响到运行效率及性能,直 ...

  2. Android注解框架实战-ButterKnife

    文章大纲 Android注解框架介绍 ButterKnife实战 项目源码下载   一.框架介绍 为什么要用注解框架?  在Android开发过程中,我们经常性地需要操作组件,操作方法有findVie ...

  3. Java Android 注解(Annotation) 及几个常用开源项目注解原理简析

    不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...

  4. android注解使用详解(图文)

    在使用Java的SSH框架的时候,一直在感叹注解真是方便啊,关于注解的原理,大家可以参考我的另一片文章Java注解详解.最近有时间研究了android注解的使用,今天与大家分享一下. android中 ...

  5. 开发自己的山寨Android注解框架

    目录 开发自己的山寨Android注解框架 开发自己的山寨Android注解框架 参考 Github黄油刀 Overview 在上一章我们学习了Java的注解(Annotation),但是我想大家可能 ...

  6. android注解使用具体解释(图文)

    在使用Java的SSH框架的时候,一直在感叹注解真是方便啊,关于注解的原理,大家能够參考我的还有一片文章Java注解具体解释. 近期有时间研究了android注解的使用,今天与大家分享一下. andr ...

  7. Android注解使用之使用Support Annotations注解优化代码

    前言: 前面学习总结了Java注解的使用,博客地址详见Java学习之注解Annotation实现原理,从本质上了解到什么注解,以及注解怎么使用?不要看见使用注解就想到反射会影响性能之类,今天我们就来学 ...

  8. Android HTTP实例 使用GET方法和POST方法发送请求

    Android HTTP实例 使用GET方法和POST方法发送请求 Web程序:使用GET和POST方法发送请求 首先利用MyEclispe+Tomcat写好一个Web程序,实现的功能就是提交用户信息 ...

  9. Android HTTP实例 发送请求和接收响应

    Android HTTP实例 发送请求和接收响应 Android Http连接 实例:发送请求和接收响应 添加权限 首先要在manifest中加上访问网络的权限: <manifest ... & ...

随机推荐

  1. VMware虚拟机Linux增加磁盘空间的扩容操作

    转载自点击打开链接 用VMwareware虚拟机安装的Red Hat Enterprise Linux系统剩余空间不足,造成软件无法正常安装.如果重新装一遍系统就需要重新配置好开发环境和软件的安装配置 ...

  2. sql批量新增,修改

    <insert id="insertExtDocList" parameterType="map"> INSERT INTO extprjdoc ( ...

  3. 计算机网络九:IP地址、子网掩码、默认网关、DHCP服务器、DNS服务器、WINS服务器

    一.IP地址与子网掩码 1.IP地址 ipv4下,ip地址=网络号+主机号. 2.子网掩码         子网掩码(subnet mask)又叫网络掩码.地址掩码.子网络遮罩,它是一种用来指明一个I ...

  4. Linux(Ubuntu-CentOS)

    2017.3.29 查看已安装软件版本 dpkg-query --list 2017.3.3 Ubuntu 14.04 安装 phpmyadmin make sure apache works wel ...

  5. Python 日常技巧

    jupyter notebook 本地开启jupyter,画图需打开限制:jupyter notebook --NotebookApp.iopub_data_rate_limit=2147483647 ...

  6. windows下安装nodejs以及python2502,2503解决方案

    1. 2053和2052为什么会出现出现这个提示的时候,是在程序安装步骤 到达copy new file的时候 进入下一步进行报错,可以推测出应该是软件包在安装的时候,解压缩部署核心文件的时候出错. ...

  7. C# WebSocket Fleck 调用非托管C++ DLL 实现通信(使用stringbuilder接收)

     [DllImport(@"XXX.dll", CallingConvention = CallingConvention.StdCall)]public static exter ...

  8. Spring Boot 2 - 使用CommandLineRunner与ApplicationRunner

    本篇文章我们将探讨CommandLineRunner和ApplicationRunner的使用. 在阅读本篇文章之前,你可以新建一个工程,写一些关于本篇内容代码,这样会加深你对本文内容的理解,关于如何 ...

  9. Akka-CQRS(6)- read-side

    前面我们全面介绍了在akka-cluster环境下实现的CQRS写端write-side.简单来说就是把发生事件描述作为对象严格按发生时间顺序写入数据库.这些事件对象一般是按照二进制binary方式如 ...

  10. MySQL Schema与数据类型的优化

    选择优化的数据类型: 1. 更小的通常更好: 一般情况下,应该尽量使用可以正确存储数据的最小数据类型.更小的数据类型通常更快,因为他们占用更少的磁盘,内存和cpu缓存,并且处理时需要的cpu周期也更少 ...