浅谈Android下的注解
创始人
2024-05-13 04:13:29
0

什么是注解

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

注解是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用Annotation,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证、处理或者进行部署。

Annotation不能运行,它只有成员变量,没有方法。Annotation跟public、final等修饰符的地位一样,都是程序元素的一部分,Annotation不能作为一个程序元素使用。

注解的作用

注解将一些本来重复性的工作,变成程序自动完成,简化和自动化该过程。比如用于生成Java doc,比如编译时进行格式检查,比如自动生成代码等,用于提升软件的质量和提高软件的生产效率。

常见的注解

Android已经定义好的注解大致分为4种,称之为4大元注解

@Retention:定义该Annotation被保留的时间长度

  • RetentionPoicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;用于做一些检查性的操作,比如 @Override 和 @SuppressWarnings

  • RetentionPoicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;用于在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife)

  • RetentionPoicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;用于在运行时去动态获取注解信息。这个注解大都会与反射一起使用

@Target:定义了Annotation所修饰的对象范围

  • ElementType.CONSTRUCTOR:用于描述构造器

  • ElementType.FIELD:用于描述域

  • ElementType.LOCAL_VARIABLE:用于描述局部变量

  • ElementType.METHOD:用于描述方法

  • ElementType.PACKAGE:用于描述包

  • ElementType.PARAMETER:用于描述参数

  • ElementType.TYPE:用于描述类、接口(包括注解类型) 或enum声明

未标注则表示可修饰所有

@Inherited:是否允许子类继承父类的注解,默认是false

@Documented 是否会保存到 Javadoc 文档中

自定义注解

自定义注解中使用到较多的是运行时注解和编译时注解

运行时注解

下面通过一个简单的动态绑定控件的例子来说明

首先定义一个简单的自定义注解,

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {intvalue() default-1;
}
复制代码

然后在app运行时,通过反射将findViewbyId()得到的控件,注入到我们需要的变量中。

public classAnnotationActivityextendsAppCompatActivity{@BindView(R.id.annotation_tv)privateTextView mTv;@Overrideprotected void onCreate(@NullableBundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_annotation);getAllAnnotationView();mTv.setText("Annotation");}private void getAllAnnotationView() {//获得成员变量Field[] fields = this.getClass().getDeclaredFields();for (Field field : fields) {try {//判断注解if (field.getAnnotations() != null) {//确定注解类型if (field.isAnnotationPresent(BindView.class)) {//允许修改反射属性field.setAccessible(true);BindView bindView = field.getAnnotation(BindView.class);//findViewById将注解的id,找到View注入成员变量中field.set(this, findViewById(bindView.value()));}}} catch (Exception e) {e.printStackTrace();}}}
}
复制代码

最后mTv上显示的就是我们想要的“Annotation”文字,这看起来是不是有点像ButterKnife,但是要注意反射是很消耗性能的,

所以我们常用的控件绑定库ButterKnife并不是采用运行时注解,而是采用的编译时注解.

编译时注解

定义

在说编译时注解之前,我们得先提一提注解处理器AbstractProcessor。

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

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

导入

因为AbstractProcessor是javac中的一个工具,所以在Android的工程下没法直接调用。下面提供一个本人尝试可行的导入方式。

File-->New Module-->java library 新建一个java module,注意一定要是java library,不是Android library

接下来就可以在对应的library中使用AbstractProcessor了

准备工作完成之后,下面通过一个简单的注解绑定控件的例子来讲述

工程目录
--app                 (主工程)
--app_annotation      (java module 自定义注解)
--annotation-api      (Android module)
--app_compiler        (java module 注解处理器逻辑)
复制代码

在annotation module下创建注解

@Retention(RetentionPolicy.CLASS)
public @interface BindView {//绑定控件intvalue();
}
复制代码

在compiler module下创建注解处理器 CustomProcessor

publicclassCustomProcessorextendsAbstractProcessor {//文件相关的辅助类privateFiler mFiler;//元素相关的辅助类privateElements mElements;//初始化参数@Overridepublic synchronized voidinit(ProcessingEnvironment processingEnvironment) {super.init(processingEnvironment);mElements = processingEnvironment.getElementUtils();mFiler = processingEnvironment.getFiler();}//核心处理逻辑,相当于java中的主函数main(),你需要在这里编写你自己定义的注解的处理逻辑//返回值 true时表示当前处理,不允许后续的注解器处理@Overridepublicbooleanprocess(Set set, RoundEnvironment env) {returntrue;}//自定义注解集合@OverridepublicSet getSupportedAnnotationTypes() {Set types = newLinkedHashSet<>();types.add(BindView.class.getCanonicalName());return types;}@OverridepublicSourceVersiongetSupportedSourceVersion() {returnSourceVersion.latestSupported();}
}
复制代码

其中核心代码process函数有两个参数,我们重点关注第二个参数,因为env表示的是所有注解的集合

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

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

  1. 将元素列表封装成对象,方便之后的处理(如同平时解析json数据一样)

  1. 通过JavaPoet库将对象以我们期望的形式生成java文件

遍历env,得到我们需要的元素列表
for(Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)){// todo ....// 判断元素的类型为Classif (element.getKind() == ElementKind.CLASS) {// 显示转换元素类型TypeElement typeElement = (TypeElement) element;// 输出元素名称System.out.println(typeElement.getSimpleName());// 输出注解属性值System.out.println(typeElement.getAnnotation(BindView.class).value());}
}
复制代码

直接通过getElementsAnnotatedWith函数就能获取到需要的注解的列表,函数体内加了些element简单的使用

2.将元素列表封装成对象,方便之后的处理

首先,我们需要明确,在绑定控件的这个事件下,我们需要的是控件的id。

新建类 BindViewField.class 用来保存自定义注解BindView相关的属性

BindViewField.class

publicclassBindViewField {private VariableElement mFieldElement;privateint mResId;publicBindViewField(Element element) throws IllegalArgumentException {if (element.getKind() != ElementKind.FIELD) {thrownew 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) {thrownew IllegalArgumentException(String.format("value() in %s for field % is not valid",BindView.class.getSimpleName(), mFieldElement.getSimpleName()));}}public Name getFieldName() {return mFieldElement.getSimpleName();}publicintgetResId() {return mResId;}public TypeMirror getFieldType() {return mFieldElement.asType();}
}
复制代码

上述的BindViewField只能表示一个自定义注解bindView对象,而一个类中很可能会有多个自定义注解,所以还需要创建一个对象Annotation.class来管理自定义注解集合、

AnnotatedClass.class

publicclassAnnotatedClass {//类publicTypeElement mClassElement;//类内的注解变量publicList mFiled;//元素帮助类publicElements mElementUtils;publicAnnotatedClass(TypeElement classElement, Elements elementUtils) {this.mClassElement = classElement;this.mElementUtils = elementUtils;this.mFiled = newArrayList<>();}//添加注解变量publicvoidaddField(BindViewField field) {mFiled.add(field);}//获取包名publicStringgetPackageName(TypeElement type) {return mElementUtils.getPackageOf(type).getQualifiedName().toString();}//获取类名privatestaticStringgetClassName(TypeElement type, String packageName) {int packageLen = packageName.length() + 1;returntype.getQualifiedName().toString().substring(packageLen).replace('.', '$');}
}
复制代码

给上完整的解析流程

//解析过后的目标注解集合privateMap mAnnotatedClassMap = newHashMap<>();@Overridepublicbooleanprocess(Set set, RoundEnvironment roundEnvironment) {mAnnotatedClassMap.clear();try {processBindView(roundEnvironment);} catch (Exception e) {e.printStackTrace();returntrue;}returntrue;
}privatevoidprocessBindView(RoundEnvironment env) {for (Element element : env.getElementsAnnotatedWith(BindView.class)) {AnnotatedClass annotatedClass = getAnnotatedClass(element);BindViewField field = newBindViewField(element);annotatedClass.addField(field);System.out.print("p_element=" + element.getSimpleName() + ",p_set=" + element.getModifiers());}
}privateAnnotatedClassgetAnnotatedClass(Element element) {TypeElement encloseElement = (TypeElement) element.getEnclosingElement();String fullClassName = encloseElement.getQualifiedName().toString();AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);if (annotatedClass == null) {annotatedClass = newAnnotatedClass(encloseElement, mElements);mAnnotatedClassMap.put(fullClassName, annotatedClass);}return annotatedClass;
}
复制代码
3.通过JavaPoet库将对象以我们期望的形式生成java文件

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

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

在process函数中增加生成java文件的逻辑

@Overridepublicbooleanprocess(Set set, RoundEnvironment roundEnvironment) {mAnnotatedClassMap.clear();try {processBindView(roundEnvironment);} catch (Exception e) {e.printStackTrace();returntrue;}try {for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {annotatedClass.generateFinder().writeTo(mFiler);}} catch (Exception e) {e.printStackTrace();}returntrue;
}
复制代码

其中核心逻辑annotatedClass.generateFinder().writeTo(mFiler);

具体实现在AnnotatedClass中

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);String className = getClassName(mClassElement, packageName);ClassName bindClassName = ClassName.get(packageName, className);//构建类对象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下新建

注入接口Injector

publicinterfaceInjector {voidinject(T host, Object source, Finder finder);
}
复制代码

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

publicinterfaceFinder {Context getContext(Object source);View findView(Object source, int id);
}
复制代码

activity实现类 ActivityFinder

publicclassActivityFinderimplementsFinder{@OverridepublicContextgetContext(Object source) {return (Activity) source;}@OverridepublicViewfindView(Object source, int id) {return ((Activity) (source)).findViewById(id);}
}
复制代码

核心实现类 ButterKnife

publicclassButterKnife {privatestatic final ActivityFinder finder = newActivityFinder();privatestaticMap FINDER_MAP = newHashMap<>();publicstaticvoidbind(Activity activity) {bind(activity, activity);}privatestaticvoidbind(Object host, Object source) {bind(host, source, finder);}privatestaticvoidbind(Object host, Object source, Finder finder) {String className = host.getClass().getName();try {Injector injector = FINDER_MAP.get(className);if (injector == null) {Class finderClass = Class.forName(className + "?Injector");injector = (Injector) finderClass.newInstance();FINDER_MAP.put(className, injector);}injector.inject(host, source, finder);} catch (Exception e) {e.printStackTrace();}}
}
复制代码

主工程下调用

对应的按钮可以直接使用,不需要findViewById()

public classMainActivityextendsAppCompatActivity{@BindView(R.id.annotation_tv)public TextView tv1;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);ButterKnife.bind(this);tv1.setText("annotation_demo");}
}
复制代码

JavaPoet的简单介绍

常用的几个类

  • MethodSpec 代表一个构造函数或方法声明。

  • TypeSpec 代表一个类,接口,或者枚举声明。

  • FieldSpec 代表一个成员变量,一个字段声明。

  • JavaFile包含一个顶级类的Java文件。

常用的占位符

$L for variable (变量)

$S for Strings

$T for Types

$N for Names(我们自己生成的方法名或者变量名等等)

补充内容

自定义Processor注解处理器中最主要的处理方法是process()函数,而process()函数中重要的是 RoundEnvironment参数,

通常的使用方式

for (Element element : env.getElementsAnnotatedWith(BindView.class)) {//todo 
}
复制代码

通过BindView注解获取所有的Element对象,而这个Element是什么呢?

Element表示一个程序元素,可以是包,类或者是方法,所有通过注解取到的元素都将以Element类型处理.准确的来说是Element对象的子类处理。

Element的子类

  • ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
    对应注解时的@Target(ElementType.METHOD)和@Target(ElementType.CONSTRUCTOR)

  • PackageElement 表示一个包程序元素,提供对有关包及其成员的信息访问。对应注解时@Target(ElementType.PACKAGE)

  • TypeElement 表示一个类或接口程序元素,提供对有关类型及其成员的信息访问。对应@Target(ElementType.TYPE)
    注意:枚举类型是一种类,而注解类型是一种接口。

  • TypeParameterElement 表示一般类、接口、方法或构造方法元素的类型参数。
    对应@Target(ElementType.PARAMETER)

  • VariableElement 表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数。
    对应@Target(ElementType.LOCAL_VARIABLE)

不同类型的Element的信息获取方式不同

修饰方法的注解和ExecutableElement

当你有一个注解是以@Target(ElementType.METHOD)定义时,表示该注解只能修饰方法。

获取我们需要的一些基本信息

//BindClick.class 以 @Target(ElementType.METHOD)修饰
for (Element element : roundEnv.getElementsAnnotatedWith(BindClick.class)) {//对于Element直接强转ExecutableElement executableElement = (ExecutableElement) element;//非对应的Element,通过getEnclosingElement转换获取TypeElement classElement = (TypeElement) element.getEnclosingElement();//当(ExecutableElement) element成立时,使用(PackageElement) element.getEnclosingElement();将报错。//需要使用elementUtils来获取Elements elementUtils = processingEnv.getElementUtils();PackageElement packageElement = elementUtils.getPackageOf(classElement);//全类名String fullClassName = classElement.getQualifiedName().toString();//类名String className = classElement.getSimpleName().toString();//包名String packageName = packageElement.getQualifiedName().toString();//方法名String methodName = executableElement.getSimpleName().toString();//取得方法参数列表List methodParameters = executableElement.getParameters();//参数类型列表List types = new ArrayList<>();for (VariableElement variableElement : methodParameters) {TypeMirror methodParameterType = variableElement.asType();if (methodParameterType instanceof TypeVariable) {TypeVariable typeVariable = (TypeVariable) methodParameterType;methodParameterType = typeVariable.getUpperBound();}//参数名String parameterName = variableElement.getSimpleName().toString();//参数类型String parameteKind = methodParameterType.toString();types.add(methodParameterType.toString());}
}
复制代码
修饰属性、类成员的注解和VariableElement
for (Element element : roundEnv.getElementsAnnotatedWith(IdProperty.class)) {//ElementType.FIELD注解可以直接强转VariableElementVariableElement variableElement = (VariableElement) element;TypeElement classElement = (TypeElement) element.getEnclosingElement();PackageElement packageElement = elementUtils.getPackageOf(classElement);//类名String className = classElement.getSimpleName().toString();//包名String packageName = packageElement.getQualifiedName().toString();//类成员名String variableName = variableElement.getSimpleName().toString();//类成员类型TypeMirror typeMirror = variableElement.asType();String type = typeMirror.toString();}
复制代码
修饰类的注解和TypeElement
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {//ElementType.TYPE注解可以直接强转TypeElementTypeElement classElement = (TypeElement) element;PackageElement packageElement = (PackageElement) element.getEnclosingElement();//全类名String fullClassName = classElement.getQualifiedName().toString();//类名String className = classElement.getSimpleName().toString();//包名String packageName = packageElement.getQualifiedName().toString();//父类名String superClassName = classElement.getSuperclass().toString();}

相关内容

热门资讯

美国不提安卓系统华为,迈向自主... 华为与美国:一场关于技术、市场与政策的较量在当今这个数字化的世界里,智能手机已经成为我们生活中不可或...
安卓系统怎么打开ppt,选择文... 你有没有遇到过这种情况:手里拿着安卓手机,突然需要打开一个PPT文件,却怎么也找不到方法?别急,今天...
谷歌退回到安卓系统,探索创新未... 你知道吗?最近科技圈可是炸开了锅,谷歌竟然宣布要退回到安卓系统!这可不是一个简单的决定,背后肯定有着...
安卓系统待机耗电多少,深度解析... 你有没有发现,手机电量总是不经用?尤其是安卓系统,有时候明明没怎么用,电量就“嗖”的一下子就下去了。...
小米主题安卓原生系统,安卓原生... 亲爱的手机控们,你是否曾为手机界面单调乏味而烦恼?想要给手机换换“衣服”,让它焕然一新?那就得聊聊小...
voyov1安卓系统,探索创新... 你有没有发现,最近你的手机是不是变得越来越流畅了?没错,我要说的就是那个让手机焕发青春的Vivo V...
电脑刷安卓tv系统,轻松打造智... 你有没有想过,家里的安卓电视突然变得卡顿,反应迟钝,是不是时候给它来个“大保健”了?没错,今天就要来...
安卓系统即将要收费,未来手机应... 你知道吗?最近有个大消息在科技圈里炸开了锅,那就是安卓系统可能要开始收费了!这可不是开玩笑的,这可是...
雷凌车载安卓系统,智能出行新体... 你有没有发现,现在的汽车越来越智能了?这不,我最近就体验了一把雷凌车载安卓系统的魅力。它就像一个聪明...
怎样拍照好看安卓系统,轻松拍出... 拍照好看,安卓系统也能轻松搞定!在这个看脸的时代,拍照已经成为每个人生活中不可或缺的一部分。无论是记...
安卓车机系统音频,安卓车机系统... 你有没有发现,现在越来越多的汽车都开始搭载智能车机系统了?这不,咱们就来聊聊安卓车机系统在音频方面的...
老苹果手机安卓系统,兼容与创新... 你手里那台老苹果手机,是不是已经陪你走过了不少风风雨雨?现在,它竟然还能装上安卓系统?这可不是天方夜...
安卓系统7.dns,优化网络连... 你有没有发现,你的安卓手机最近是不是有点儿“慢吞吞”的?别急,别急,让我来给你揭秘这可能与你的安卓系...
安卓手机系统怎么加速,安卓手机... 你有没有发现,你的安卓手机最近变得有点“慢吞吞”的?别急,别急,今天就来给你支几招,让你的安卓手机瞬...
小米note安卓7系统,探索性... 你有没有发现,手机更新换代的速度简直就像坐上了火箭呢?这不,小米Note这款手机,自从升级到了安卓7...
安卓和鸿蒙系统游戏,两大系统游... 你有没有发现,最近手机游戏界可是热闹非凡呢!安卓和鸿蒙系统两大巨头在游戏领域展开了一场激烈的较量。今...
安卓手机没有系统更,揭秘潜在风... 你有没有发现,现在安卓手机的品牌和型号真是五花八门,让人挑花了眼。不过,你知道吗?尽管市面上安卓手机...
充值宝带安卓系统,安卓系统下的... 你有没有发现,最近手机上的一款充值宝APP,在安卓系统上可是火得一塌糊涂呢!这不,今天就来给你好好扒...
安卓系统8.0镜像下载,轻松打... 你有没有想过,想要给你的安卓手机升级到最新的系统,却不知道从哪里下载那个神秘的安卓系统8.0镜像呢?...
安卓系统修改大全,全方位修改大... 你有没有想过,你的安卓手机其实是个大宝藏,里面藏着无数可以让你手机焕然一新的秘密?没错,今天就要来个...