注解是什么
注解是一种能被添加到java代码中的元数据,类、方法、变量、参数和包都可以用注解来修饰。注解对于它所修饰的代码并没有直接的影响。
1、什么是元注解
用于对注解类型进行注解的注解类,称之为元注解。JDK1.5中提供了4个标准元注解。
@Target: 描述注解的使用范围,说明被它所注解的注解类可修饰的对象范围 @Retention: 描述注解保留的时期,被描述的注解在它所修饰的类中可保留到何时 @Documented: 描述在使用Javadoc工具为类生成帮助文档时是否要保留其注解信息 @Inherited: 使被它修饰的注解修饰的注解类的子类能继承到注解
2、元注解@Target的取值及其含义
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Event {
}
@Target描述的是注解的使用范围,携带的值为枚举,作用是标明它修饰的注解可以用在哪些地方。比如上述例子中的@Event只能作用于属性和方法上
ElementType的取值和意义如下:
public enum ElementType {
//作用在类上
TYPE,
//作用在属性上
FIELD,
//作用在方法上
METHOD,
//作用在参数上
PARAMETER,
//作用在构造器上
CONSTRUCTOR,
//作用在局部变量上
LOCAL_VARIABLE,
//作用在注解上
ANNOTATION_TYPE,
//作用在包名上
PACKAGE,
private ElementType(){...}
}
注意:每个注解可以跟n个ElementType关联。当无指定时,注解可用于任何地方。
3、元注解@Retention的取值及其含义
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Event {
}
@Retention描述的是注解的存在时期。如上述例子中@Event为运行时注解,在源码,字节码及运行时皆存在
RetentionPolicy取值和意义如下:
public enum RetentionPolicy {
//源码时注解, 只在源码中存在,编译后便不存在了
SOURCE,
//编译时注解,源码和编译时存在,运行时不存在
CLASS,
//运行时注解,源码,编译时,运行时都存在
RUNTIME;
private RetentionPolicy(){...}
}
注意:每个注解只能和一个RetentionPolicy关联。当无指定时,默认为RetentionPolicy.CLASS
4、其他元注解介绍
@Documented: 类和方法的Annotation在缺省情况下是不出现在javadoc中的。如果使用@Documented修饰该注解,则表示它可以出现在javadoc中。
@Inheried: 当使用该注解的类有子类时,注解在子类仍然存在。通过反射其子类可获得父类相同的注解
5、自定义注解的参数
public @interface Person {
public String name();
//默认值
int age() default 18;
int[] array();
}
注解能够携带的参数类型有:基本数据类型,String, Class, Annotation, enum
注解的使用
注解目前比较常见的使用场景有
a、编译时动态检查,比如某参数的取值只能为某些int值,如颜色。则可以使用编译时注解在编译时对参数进行检查
b、编译时动态生成代码,使用注解处理器在编译时生成class文件。如ButterKnife实现
c、运行时动态注入,用注解实现IOC,许多框架将原有配置文件xml改成注解用的便是IOC注入。
1、编译时注解-Apt注解处理器使用
下面以实际案例讲解。案例目标: 实现注解绑定控件,效果如下
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv)
public TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
Toast.makeText(this, tv.getText().toString(), Toast.LENGTH_SHORT).show();
}
}
1、工程结构
- app模块, 一个android模块,是demo的主模块,内容是demo的演示部分MainActivity
- annotation模块,一个java Library模块,用于放置注解
- compile模块,一个java Library模块,注解处理器主要实现在这个模块中实现
- library模块,一个android Library模块,配合注解处理器生成的代码,实现注解绑定控件功能
app模块内容和上述实现目标一致,相信都看得懂。下面逐一介绍其他模块
2、annotation模块
注解模块存放注解,本案例中的注解为@BindView。由于要编译期获取注解,生成相关代码,所以该注解为编译时代码(@Retention(RetentionPolicy.CLASS);又因为要作用在属性上,所以该注解的作用目标为@Target(ElementType.FIELD);并且@BindView具有一个参数代表控件id,类型为int。由此可得出如下注解声明
//作用在属性上
@Target(ElementType.FIELD)
//编译时注解
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
//传递参数,此处为控件id
int value();
}
3、compile模块
1、本模块要使用注解处理器,首先在build.gradle中引入相关库,build.gradle内容如下
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//google出品,注解处理器库
compileOnly 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
//javapoet用于生成java类
implementation 'com.squareup:javapoet:1.10.0'
implementation project(':annotation')
}
sourceCompatibility = "7"
targetCompatibility = "7"
注解类:
第一步:扫描出代码中被注解的属性及其对应的activity,存放到map中
//作用是声明注解处理器
@AutoService(Processor.class)
//声明生成代码是基于java1.7
@SupportedSourceVersion(SourceVersion.RELEASE_7)
//声明注解处理器支持的注解
@SupportedAnnotationTypes("com.sq.annotation.BindView")
public class ButterKnifeProcessor extends AbstractProcessor {
//用于打印日志
private Messager mMessager;
//存放activity和activity内注解的控件
private Map<TypeElement, List<VariableElement>> mTargetMap;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mMessager = processingEnvironment.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//获得被BindView注解的所有元素
Set<Element> views = (Set<Element>) roundEnvironment.getElementsAnnotatedWith(BindView.class);
if (views != null && views.size() > 0) {
//将activity和对应的注解的控件放到map中
mTargetMap = new HashMap<>();
for (Element view : views) {
if (view instanceof VariableElement) {
//获得所属类元素,即Activity
TypeElement activityElement = (TypeElement) view.getEnclosingElement();
if (mTargetMap.get(activityElement) == null) {
ArrayList targetList = new ArrayList<VariableElement>();
targetList.add(view);
mTargetMap.put(activityElement, targetList);
} else {
mTargetMap.get(activityElement).add((VariableElement) view);
}
}
}
//遍历对应activity
if (mTargetMap.size() > 0) {
for (Map.Entry<TypeElement, List<VariableElement>> entry : mTargetMap.entrySet()) {
String activityName = entry.getKey().getSimpleName().toString();
mMessager.printMessage(Diagnostic.Kind.NOTE,"activity类名为:" + activityName);
for (VariableElement view : entry.getValue()) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "被注解的属性为: " + view.getSimpleName().toString());
}
//为每一个activity生成代码
generateCode(entry.getKey(), entry.getValue());
}
}
}
return false;
}
}
上述代码,打印出来的日志为: 注: activity类名为:MainActivity 注: 被注解的属性为: tv
第二步:生成代码 由于是为activity绑定控件,生成的代码如下:
public class MainActivity$ViewBinder implements ViewBinder<MainActivity> {
@Override
public void bind(final MainActivity target) {
target.tv = target.findViewById(2131165359);
}
}
其中,ViewBinder是接口,其代码放于library模块中,代码如下:
public interface ViewBinder<T> {
void bind(T target);
}
通常在生成代码前,首先也是要先想明白生成的代码是怎样的,先有模板再开始写生成的逻辑。
以下开始写generateCode()方法内容
private void generateCode(TypeElement activityElement, List<VariableElement> viewElements) {
//用于获得activity类名在javapoet中的表示
ClassName className = ClassName.get(activityElement);
//生成的类实现的接口
TypeElement viewBinderType = mElementUtils.getTypeElement("com.sq.library.ViewBinder");
//实现的接口在javapoet中的表示
ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBinderType), className);
//bind方法参数,即MainActivity target
ParameterSpec parameterSpec = ParameterSpec.builder(className, "target", Modifier.FINAL).build();
//方法声明:public void bind(final MainActivity target)
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterSpec);
//方法体
for (VariableElement viewElement : viewElements) {
//获取属性名
String fieldName = viewElement.getSimpleName().toString();
//获取@BindView注解的值
int annotationValue = viewElement.getAnnotation(BindView.class).value();
//target.tv = target.findViewById(R.id.tv);
String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
//加入方法内容
methodBuilder.addStatement(methodContent, "target", "target", annotationValue);
}
//生成代码
try {
JavaFile.builder(className.packageName(),
TypeSpec.classBuilder(className.simpleName() + "$ViewBinder")
.addSuperinterface(typeName)
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build())
.build()
.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
4、library模块
library模块的作用是配合compile模块生成的代码,提供给app模块使用。实现的是MainActivity中ButterKnife.bind(this)以及compile模块生成的MainActivity&MainActivityViewBinder实现的ViewBinder接口 ButterKnife类代码如下:
public class ButterKnife {
public static void bind(Activity activity) {
try {
//找到对应activity的ViewBinder类,调用bind方法并将activity作为参数传入
Class viewBinderClass = Class.forName(activity.getClass().getName() + "$ViewBinder");
ViewBinder viewBinder = (ViewBinder) viewBinderClass.newInstance();
viewBinder.bind(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
5、app模块使用compile模块
build.gradle配置如下:
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.sq.aptdemo"
minSdkVersion 19
targetSdkVersion 29
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation project(':annotation')
implementation project(':library')
//引用注解处理模块的方式如下:
annotationProcessor project(":compile")
}
如何触发?
make Module 'app'便可触发编译,使得compile模块开始执行。
查看生成的代码:
运行app模块后运行正常,控件成功和id绑定。
至此,使用apt注解处理器生成代码完成控件注入开发完成。
2、运行时注解实现控件注入
案例目标,效果如下:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv)
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//注入控件
InjectUtils.bind(this);
Toast.makeText(this, tv.getText().toString(), Toast.LENGTH_SHORT).show();
}
}
可以看出,使用时和利用编译时注解生成代码并无差别,不过这里的属性TextView 可以是私有成员。因为注入使用的是反射实现的。
1、工程结构
比使用编译时注解少了compile模块。下面一一介绍。
2、annotation模块
注解模块内容依然是存放注解,这里做演示,只用了一个BindView注解
@Target(ElementType.FIELD)
//运行时注解
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
int value();
}
3、library模块
public class InjectUtils {
public static void bind(Activity target) {
//获取activity的Class
Class activityClass = target.getClass();
//获取到activity所有属性
Field[] fields = activityClass.getDeclaredFields();
if (fields != null) {
//遍历所有属性,找到有注解的属性
for (Field field : fields) {
field.setAccessible(true);
BindView annotation = field.getAnnotation(BindView.class);
if (annotation != null) {
//获取到注解带的id
int id = annotation.value();
//找到id对应的view
View targetView = target.findViewById(id);
try {
//设置属性的值为对应的view,完成绑定
field.set(target, targetView);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
}
至此,便完成了控件注入。 可以看到实际上运行时注解实现控件注入相对简单些,但由于这种方式使用了反射,运行效率上相对差一些。