Android注解基础介绍快速入门与解读
首先什么是注解?@override
就是注解,它的作用是:
1、检查是否正确的重写了父类中的方法。
2、标明代码,这是一个重写的方法。
1、体现在于:检查子类重写的方法名与参数类型是否正确;检查方法private/final/static等不能被重写。实际上@override
对于应用程序并没有实际影响,从它的源码中可以出来。
2、主要是表现出代码的可读性。
作为android开发中熟知的注解,override只是注解的一种体现,更多时候,注解还有以下作用:
降低项目的耦合度。
自动完成一些规律性的代码。
自动生成java代码,减轻开发者的工作量。
一、注解基础快读
1、元注解
元注解是由java提供的基础注解,负责注解其它注解,如上图override被@target
和@retention
修饰,它们用来说明解释其它注解,位于sdk/sources/android-25/java/lang/annotation
路径下。
元注解有:
@retention:注解保留的生命周期
@target:注解对象的作用范围。
@inherited:@inherited标明所修饰的注解,在所作用的类上,是否可以被继承。
@documented:如其名,javadoc的工具文档化,一般不关心。
@retention
retention说标明了注解被生命周期,对应retentionpolicy的枚举,表示注解在何时生效:
source:只在源码中有效,编译时抛弃,如上面的
@override
。class:编译class文件时生效。
runtime:运行时才生效。
如下图,com.android.support:support-annotations
中的nullable注解,会在编译期判断,被注解的参数是否会空,具体后续分析。
@target
target标明了注解的适用范围,对应elementtype枚举,明确了注解的有效范围。
type:类、接口、枚举、注解类型。
field:类成员(构造方法、方法、成员变量)。
method:方法。
parameter:参数。
constructor:构造器。
local_variable:局部变量。
annotation_type:注解。
package:包声明。
type_parameter:类型参数。
type_use:类型使用声明。
如上图所示,@nullable
可用于注解方法,参数,类成员,注解,包声明中,常用例子如下所示:
/** * nullable表明 * bind方法的参数target和返回值data可以为null */ @nullable public static data bind(@nullable context target) { //do something and return return bindxxx(target); }
@inherited
注解所作用的类,在继承时默认无法继承父类的注解。除非注解声明了 @inherited。同时inherited声明出来的注,只对类有效,对方法/属性无效。
如下方代码,注解类@ainherited
声明了inherited ,而注解bnotinherited 没有,所在在它们的修饰下:
类child继承了父类parent的
@ainherited
,不继承@bnotinherited
;重写的方法
testoverride()
不继承parent的任何注解;
testnotoverride()
因为没有被重写,所以注解依然生效。
@retention(retentionpolicy.runtime) @inherited public @interface ainherited { string value(); } @retention(retentionpolicy.runtime) public @interface bnotinherited { string value(); } @ainherited("inherited") @bnotinherited("没inherited") public class parent { @ainherited("inherited") @bnotinherited("没inherited") public void testoverride(){ } @ainherited("inherited") @bnotinherited("没inherited") public void testnotoverride(){ } } /** * child继承了parent的ainherited注解 * bnotinherited因为没有@inherited声明,不能被继承 */public class child extends parent { /** * 重写的testoverride不继承任何注解 * 因为inherited不作用在方法上 */ @override public void testoverride() { } /** * testnotoverride没有被重写 * 所以注解ainherited和bnotinherited依然生效。 */}
2、自定义注解
2.1 运行时注解
了解了元注解后,看看如何实现和使用自定义注解。这里我们简单介绍下运行时注解runtime,编译时注解class留着后面分析。
首先,创建一个注解遵循: public @interface 注解名 {方法参数},如下方@getviewto
注解:
@target({elementtype.field})@retention(retentionpolicy.runtime)public @interface getviewto { int value() default -1; }
然后如下方所示,我们将注解描述在activity的成员变量mtv
和mbtn
中,在app运行时,通过反射将findviewbyid得到的控件,注入到mtv
和mbtn
中。
是不是很熟悉,有点butterknife的味道?当然,butterknife比这个高级多,毕竟反射多了影响效率,不过我们明白了,可以通过注解来注入和创建对象,这样可以在一定程度节省代码量。
public class mainactivity extends appcompatactivity { @getviewto(r.id.textview) private textview mtv; @getviewto(r.id.button) private button mbtn; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); //通过注解生成view; getallannotationview(); } /** * 解析注解,获取控件 */ private void getallannotationview() { //获得成员变量 field[] fields = this.getclass().getdeclaredfields(); for (field field : fields) { try { //判断注解 if (field.getannotations() != null) { //确定注解类型 if (field.isannotationpresent(getviewto.class)) { //允许修改反射属性 field.setaccessible(true); getviewto getviewto = field.getannotation(getviewto.class); //findviewbyid将注解的id,找到view注入成员变量中 field.set(this, findviewbyid(getviewto.value())); } } } catch (exception e) { } } } }
2.2 编译时注解
运行时注解runtime如上2.1所示,大多数时候实在运行时使用反射来实现所需效果,这很大程度上影响效率,如果bufferknife的每个view注入不可能如何实现。实际上,butterknife使用的是编译时注解class,如下图x2.2,是butterknife的@bindview
注解,它是一个编译时注解,在编译时生成对应java代码,实现注入。
说到编译时注解,就不得不说注解处理器 abstractprocessor,如果你有注意,一般第三方注解相关的类库,如bufferknike、arouter,都有一个compiler命名的module,如下图x2.3,这里面一般都是注解处理器,用于编译时处理对应的注解。
注解处理器(annotation processor)是javac的一个工具,它用来在编译时扫描和处理注解(annotation)。你可以对自定义注解,并注册相应的注解处理器,用于处理你的注解逻辑。
如下所示,实现一个自定义注解处理器,至少重写四个方法,并且注册你的自定义processor,详细可参考下方代码customprocessor
。
@autoservice(processor.class),谷歌提供的自动注册注解,为你生成注册processor所需要的格式文件(
com.google.auto
相关包)。init(processingenvironment env),初始化处理器,一般在这里获取我们需要的工具类。
getsupportedannotationtypes(),指定注解处理器是注册给哪个注解的,返回指定支持的注解类集合。
getsupportedsourceversion() ,指定java版本。
process(),处理器实际处理逻辑入口。
@autoservice(processor.class)public class customprocessor extends abstractprocessor { /** * 注解处理器的初始化 * 一般在这里获取我们需要的工具类 * @param processingenvironment 提供工具类elements, types和filer */ @override public synchronized void init(processingenvironment env){ super.init(env); //element代表程序的元素,例如包、类、方法。 melementutils = env.getelementutils(); //处理typemirror的工具类,用于取类信息 mtypeutils = env.gettypeutils(); //filer可以创建文件 mfiler = env.getfiler(); //错误处理工具 mmessages = env.getmessager(); } /** * 处理器实际处理逻辑入口 * @param set * @param roundenvironment 所有注解的集合 * @return */ @override public boolean process(set<? extends typeelement> annoations, roundenvironment env) { //do something } //指定注解处理器是注册给哪个注解的,返回指定支持的注解类集合。 @override public set<string> getsupportedannotationtypes() { set<string> sets = new linkedhashset<string>(); //大部分class而已getname、getcanonicalnam这两个方法没有什么不同的。 //但是对于array或内部类等就不一样了。 //getname返回的是[[ljava.lang.string之类的表现形式, //getcanonicalname返回的就是跟我们声明类似的形式。 sets(bindview.class.getcanonicalname()); return sets; } //指定java版本,一般返回最新版本即可 @override public sourceversion getsupportedsourceversion() { return sourceversion.latestsupported(); } }
首先,我们梳理下一般处理器处理逻辑:
1、遍历得到源码中,需要解析的元素列表。
2、判断元素是否可见和符合要求。
3、组织数据结构得到输出类参数。
4、输入生成java文件。
5、错误处理。
然后,让我们理解一个概念:element
,因为它是我们获取注解的基础。
processor处理过程中,会扫描全部java源码,代码的每一个部分都是一个特定类型的element,它们像是xml一层的层级机构,比如类、变量、方法等,每个element代表一个静态的、语言级别的构件,如下方代码所示。
package android.demo; // packageelement// typeelementpublic class democlass { // variableelement private boolean mvariabletype; // variableelement private variableclasse m variableclasse; // executeableelement public democlass () { } // executeableelement public void resolvedata (demo data //typeelement ) { } }
其中,element
代表的是源代码,而typeelement
代表的是源代码中的类型元素,例如类。然而,typeelement
并不包含类本身的信息。你可以从typeelement
中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过typemirror
获取。你可以通过调用elements.astype()
获取元素的typemirror
。
1、知道了element
,我们就可以通过process 中的roundenvironment
去获取,扫描到的所有元素,如下图x2.4,通过env.getelementsannotatedwith
,我们可以获取被@bindview注解的元素的列表,其中validateelement
校验元素是否可用。
2、因为env.getelementsannotatedwith
返回的,是所有被注解了@ bindview的元素的列表。所以有时候我们还需要走一些额外的判断,比如,检查这些element是否是一个类:
@override public boolean process(set<? extends typeelement> an, roundenvironment env) { for (element e : env.getelementsannotatedwith(bindview.class)) { // 检查元素是否是一个类 if (ae.getkind() != elementkind.class) { ... } } ... }
3、javapoet (com.squareup:javapoet
)是一个根据指定参数,生成java文件的开源库,有兴趣了解javapoet的可以看下javapoet——让你从重复无聊的代码中解放出来,在处理器中,按照参数创建出 javafile
之后,通filer
利用javafile.writeto(filer);
就可以生成你需要的java文件。
4、错误处理,在处理器中,我们不能直接抛出一个异常,因为在process()中抛出一个异常,会导致运行注解处理器的jvm崩溃,导致跟踪栈信息十分混乱。因此,注解处理器就有一个messager类,一般通过messager.printmessage( diagnostic.kind.error, stringmessage, element)
即可正常输出错误信息。
至此,你的注解处理器完成了所有的逻辑。可以看出,编译时注解实在编译时生成java文件,然后将生产的java文件注入到源码中,在运行时并不会像运行时注解一样,影响效率和资源。
总结
我们就利用butterknife的流程,简单举例做个总结吧。
1、
@bindview
在编译时,根据acitvity生产了xxxactivity$$viewbinder.java。2、activity中调用的
butterknife.bind(this);
,通过this的类名字,加$$viewbinder,反射得到了viewbinder
,和编译处理器生产的java文件关联起来了,并将其存在map中缓存,然后调用viewbinder.bind()
。3、在viewbinder的bind方法中,通过id,利用butterknife的
butterknife.internal.utils
工具类中的封装方法,将findviewbyid()控件注入到activity的参数中。
好了,通过上面的流程,是不是把编译时注解的生成和使用连接起来了呢?
本文只是介绍了android注解基础内容,解读实例代码的具体作用,更深入的关于android注解内容可以阅读下面相关文章
推荐阅读