当前位置: 首页 > 科技观察

Android注解快速入门与实战分析

时间:2023-03-13 18:32:43 科技观察

首先,什么是注解?@Override是一个注解。它的作用是检查父类中的方法是否被正确改写。标记代码,这是一个重写的方法。1、体现在:检查子类重写的方法名和参数类型是否正确;检查方法private/final/static不能被重写。其实@Override对应用程序没有实际影响,从它的源码就可以看出来。2、主要表现代码的可读性。Override作为Android开发中比较知名的注解,只是注解的一种表现形式。更多时候,注解还有以下作用:降低项目的耦合度。自动完成一些常规代码。自动生成java代码,减少开发人员的工作量。一、注解基础速读1、元注解元注解是java提供的基础注解,负责对其他注解进行注解。如上图所示,Override被@Target和@Retention修饰。它们用于解释其他注解,位于sdk/sources/android-25/java/lang/annotation路径下。元注解包括:@Retention:注解保留的生命周期@Target:注解对象的范围。@Inherited:@Inherited表示修改后的注解是否可以在代理类上继承。@Documented:顾名思义,javadoc的工具是有文档的,一般不用管。@RetentionRetention表示注解的生命周期,对应RetentionPolicy的枚举,表示注解何时生效:SOURCE:只在源码中有效,编译时丢弃,如上面的@Override。CLASS:编译class文件时生效。RUNTIME:只有在运行时才生效。如下图X1所示,编译时会判断com.android.support:support-annotations中的Nullable注解,注解的参数是否为null,具体后续分析。@TargetTarget表示注解的适用范围,对应ElementType枚举,明确注解的有效范围。TYPE:类、接口、枚举、注解类型。FIELD:类成员(构造函数、方法、成员变量)。方法:方法。参数:参数。构造器:构造器。LOCAL_VARIABLE:局部变量。ANNOTATION_TYPE:注释。PACKAGE:包裹声明。TYPE_PARAMETER:类型参数。TYPE_USE:类型使用声明。如上图X1所示,@Nullable可用于注解方法、参数、类成员、注解和包声明。常见示例如下:/***Nullable表示*bind方法的参数target和返回值Data可以为null*/@NullablepublicstaticDatabind(@NullableContexttarget){//dosomeThingandreturnreturnbindXXX(target);}应用@Inherited注解在继承时默认不能继承父类的注解。除非注解声明了@Inherited。同时,Inherited声明的注解只对类有效,对方法/属性无效。如下代码所示,注解类@AInherited声明了Inherited,而注解BNotInherited没有声明Inherited,这是在他们的修改下:类Child继承父类Parent的@AInherited,不继承@BNotInherited;重写的方法testOverride()没有继承Parent的任何注解;testNotOverride()没有被重写,所以注解仍然有效。@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic@interfaceAInherited{Stringvalue();}@Retention(RetentionPolicy.RUNTIME)public@interfaceBNotInherited{Stringvalue();}@AInherited("Inherited")@BNotInherited("NoInherited")publicclassParent{@AInherited("Inherited")@BNotInherited("NoInherited")publicvoidtestOverride(){}@AInherited("Inherited")@BNotInherited("NoInherited")publicvoidtestNotOverride(){}}/***子继承自ParentAInherited注解*BNotInherited不能继承,因为没有@Inherited声明*/publicclassChildextendsParent{/***重写的testOverride不继承任何注解*因为Inherited对方法不起作用*/@OverridepublicvoidtestOverride(){}/***testNotOverride未覆盖*,因此注释AInherited和BNotInherited仍然有效。*/}2.CustomAnnotations2.1RuntimeAnnotations了解元注解后,我们来看看如何实现和使用自定义注解。这里简单介绍一下运行时注解RUNTIME,以及编译时注解CLASS,以备后面分析。首先创建注解遵循:public@interface注解名{方法参数},如下@getViewTo注解:@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)public@interfacegetViewTo{intvalue()default-1;然后如下图,我们在Activity的成员变量mTv和mBtn中描述注解,在App运行时将findViewbyId获取到的控件通过反射注入到mTv和mBtn中。你熟悉ButterKnife吗?当然,ButterKnife比这更高级。毕竟反射多了影响效率,但是我们理解可以通过注解来注入创建对象,一定程度上可以节省代码。publicclassMainActivityextendsAppCompatActivity{@getViewTo(R.id.textview)privateTextViewmTv;@getViewTo(R.id.button)privateButtonmBtn;@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activityviaannotation_main);/生成看法;getAllAnnotationView();}/***解析注解,获取控件*/privatevoidgetAllAnnotationView(){//获取成员变量Field[]fields=this.getClass().getDeclaredFields();for(Fieldfield:fields){try{//判断注解if(field.getAnnotations()!=null){//判断注解类型if(field.isAnnotationPresent(GetViewTo.class)){//允许修改反射属性field.setAccessible(true);GetViewTogetViewTo=field.getAnnotation(GetViewTo.class);//findViewById找到annotation的id,将View注入成员变量field.set(this,findViewById(getViewTo.value()));}}}catch(Exceptione){}}}}2.2Compile-timeannotationsRuntimeannotationsRUNTIME如上面2.1所示,大部分时候都是通过反射在运行时达到预期的效果,非常影响效率。如果不可能实现BufferKnife的每个View注入。其实ButterKnife使用的是编译时注解CLASS,如下图X2.2所示,也就是ButterKnife的@BindView注解。是编译期注解,在编译期生成相应的java代码,实现注入。说到编译期注解,就不得不说到注解处理器AbstractProcessor。大家注意的话,一般第三方注解相关的类库,比如bufferKnike、ARouter,都有一个名为Compiler的Module,如下图X2.3所示。一般都是注解处理器,用于在编译时处理相应的注解。注解处理器(AnnotationProcessor)是javac的一个工具,用于在编译时扫描和处理注解(Annotation)。您可以自定义注解并注册相应的注解处理器来处理您的注解逻辑。如下所示,实现一个自定义注解处理器,至少重写四个方法,并注册你的自定义处理器。具体可以参考下面的代码CustomProcessor。@AutoService(Processor.class),Google提供的自动注册注解,为你生成注册Processor所需的格式文件(com.google.auto相关包)。init(ProcessingEnvironmentenv),初始化处理器,这里一般会得到我们需要的工具。getSupportedAnnotationTypes(),指定注解处理器注册到哪个注解,返回指定的一组支持的注解类。getSupportedSourceVersion(),指定java版本。process(),处理器实际处理逻辑入口。@AutoService(Processor.class)publicclassCustomProcessorextendsAbstractProcessor{/***注解处理器的初始化*一般在这里获取我们需要的工具类*@paramprocessingEnvironment提供工具类Elements,Types和Filer*/@Overridepublicsynchronizedvoidinit(ProcessingEnvironmentenv){super.init(env);//Element表示程序的元素,如包、类、方法等。mElementUtils=env.getElementUtils();//处理TypeMirror工具类,用于获取类信息mTypeUtils=env.getTypeUtils();//Filer可以创建文件mFiler=env.getFiler();//错误处理工具mMessages=env.getMessager();}/***处理器实际处理逻辑入口*@paramset*@paramroundEnvironmentallannotationsset*@return*/@Overridepublicbooleanprocess(Setannoations,RoundEnvironmentenv){//dosomeThing}//指定注解处理器注册到哪个注解,并返回指定的一组支持的注解类。@OverridepublicSetgetSupportedAnnotationTypes(){Setsets=newLinkedHashSet();//对于大多数类来说,getName和getCanonicalNam没有区别。//但是对于数组或者内部类就不一样了。//getName返回的形式类似于[[Ljava.lang.String,//getCanonicalName返回的形式类似于我们的语句。sets(BindView.class.getCanonicalName());returnsets;}//指定Java版本,一般返回最新版本@OverridepublicSourceVersiongetSupportedSourceVersion(){returnSourceVersion.latestSupported();}}首先我们梳理一下处理器的通用处理逻辑:遍历得到源码中需要解析的元素列表。判断元素是否可见,是否满足要求。组织数据结构以获得输出类参数。输入生成java文件。错误处理。那么,我们来了解一个概念:Element,因为它是我们获取注解的基础。在处理器处理期间,将扫描所有Java源代码。代码的每一部分都是一个特定类型的Element,就像XML层的层次结构,例如类、变量、方法等。每个Element代表一个静态的、Language-level的组件,如下代码所示。packageandroid.demo;//PackageElement//TypeElementpublicclassDemoClass{//VariableElementprivatebooleanmVariableType;//VariableElementprivateVariableClassEmVariableClassE;//ExecuteableElementpublicDemoClass(){}//ExecuteableElementpublicvoidresolveData(Demodata//TypeElement){代表}}其中E为源代码,是类型元素在源代码,例如类。但是,TypeElement不包含有关类本身的信息。您可以从TypeElement中获取类的名称,但无法获取有关该类的信息,例如它的父类。这些信息需要通过TypeMirror获取。您可以通过调用elements.asType()来获取元素的TypeMirror。1、知道了Element,我们可以通过流程中的RoundEnvironment获取所有扫描到的元素,如下图X2.4所示。通过env.getElementsAnnotatedWith,我们可以获取到@BindView注解的元素列表,其中validateElement验证元素是否可用。2.因为env.getElementsAnnotatedWith返回的是所有用@BindView注解的元素的列表。所以有时候我们需要做一些额外的判断,比如检查这些Element是否是一个类:@Overridepublicbooleanprocess(Setan,RoundEnvironmentenv){for(Elemente:env.getElementsAnnotatedWith(BindView.class)){//检查元素是否为classif(ae.getKind()!=ElementKind.CLASS){...}}...}3.javapoet(com.squareup:javapoet)是根据指定参数生成的java文件开源库,如果你有兴趣了解javapoet,可以阅读javapoet——让你从重复枯燥的代码中解脱出来。在处理器中,根据参数创建JavaFile后,使用javaFile.writeTo(filer);通过Filer生成你需要的java文件。4.错误处理。在处理器中,我们不能直接抛出异常,因为在process()中抛出异常会导致运行注解处理器的JVM崩溃,导致跟踪堆栈信息非常混乱。所以注解处理器有一个Messager类,一般可以通过messager.printMessage(Diagnostic.Kind.ERROR,StringMessage,element)正常输出错误信息。至此,您的注释处理器已完成所有逻辑。可以看出,编译时注解实际上是在编译时生成java文件,然后将生成的java文件注入到源码中。在运行时,它们不会像运行时注释那样影响效率和资源。总结一下,我们用ButterKnife的流程,举个简单的例子做个总结。@BindView在编译时根据Acitvity生成XXXActivity$$ViewBinder.java。ButterKnife.bind(this);在Activity中调用,通过this的类名,添加$$ViewBinder,通过反射获取ViewBinder,将其与编译处理器产生的java文件关联起来,存入map缓存中,然后调用ViewBinder。绑定()。在ViewBinder的bind方法中,使用ButterKnife的butterknife.internal.Utils工具类中的封装方法,将findViewById()控件通过id注入到Activity的参数中。那么,通过上面的过程,是不是可以把编译期注解的生成和使用联系起来呢?如果您有任何问题,请留言讨论。