注解是一种能被添加到java代码中的元数据,类、方法、变量、参数和包均可以用注解来修饰。注解对于它所修饰的代码并无直接的影响。java
用于对注解类型进行注解的注解类,称之为元注解。JDK1.5中提供了4个标准元注解。android
@Target: 描述注解的使用范围,说明被它所注解的注解类可修饰的对象范围 @Retention: 描述注解保留的时期,被描述的注解在它所修饰的类中可保留到什么时候 @Documented: 描述在使用Javadoc工具为类生成帮助文档时是否要保留其注解信息 @Inherited: 使被它修饰的注解修饰的注解类的子类能继承到注解markdown
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Event {
}
复制代码
@Target描述的是注解的使用范围,携带的值为枚举,做用是标明它修饰的注解能够用在哪些地方。好比上述例子中的@Event只能做用于属性和方法上app
ElementType的取值和意义以下:框架
public enum ElementType {
//做用在类上
TYPE,
//做用在属性上
FIELD,
//做用在方法上
METHOD,
//做用在参数上
PARAMETER,
//做用在构造器上
CONSTRUCTOR,
//做用在局部变量上
LOCAL_VARIABLE,
//做用在注解上
ANNOTATION_TYPE,
//做用在包名上
PACKAGE,
private ElementType(){...}
}
复制代码
注意:每一个注解能够跟n个ElementType关联。当无指定时,注解可用于任何地方。ide
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Event {
}
复制代码
@Retention描述的是注解的存在时期。如上述例子中@Event为运行时注解,在源码,字节码及运行时皆存在工具
RetentionPolicy取值和意义以下:gradle
public enum RetentionPolicy {
//源码时注解, 只在源码中存在,编译后便不存在了
SOURCE,
//编译时注解,源码和编译时存在,运行时不存在
CLASS,
//运行时注解,源码,编译时,运行时都存在
RUNTIME;
private RetentionPolicy(){...}
}
复制代码
注意:每一个注解只能和一个RetentionPolicy关联。当无指定时,默认为RetentionPolicy.CLASSui
@Documented: 类和方法的Annotation在缺省状况下是不出如今javadoc中的。若是使用@Documented修饰该注解,则表示它能够出如今javadoc中。this
@Inheried: 当使用该注解的类有子类时,注解在子类仍然存在。经过反射其子类可得到父类相同的注解
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注入。
下面以实际案例讲解。案例目标: 实现注解绑定控件,效果以下
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();
}
}
复制代码
app模块内容和上述实现目标一致,相信都看得懂。下面逐一介绍其余模块
注解模块存放注解,本案例中的注解为@BindView。因为要编译期获取注解,生成相关代码,因此该注解为编译时代码(@Retention(RetentionPolicy.CLASS);又由于要做用在属性上,因此该注解的做用目标为@Target(ElementType.FIELD);而且@BindView具备一个参数表明控件id,类型为int。由此可得出以下注解声明
//做用在属性上
@Target(ElementType.FIELD)
//编译时注解
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
//传递参数,此处为控件id
int value();
}
复制代码
一、本模块要使用注解处理器,首先在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();
}
}
复制代码
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();
}
}
}
复制代码
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注解处理器生成代码完成控件注入开发完成。
案例目标,效果以下:
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 能够是私有成员。由于注入使用的是反射实现的。
比使用编译时注解少了compile模块。下面一一介绍。
注解模块内容依然是存放注解,这里作演示,只用了一个BindView注解
@Target(ElementType.FIELD)
//运行时注解
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
int value();
}
复制代码
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();
}
}
}
}
}
}
复制代码
至此,便完成了控件注入。 能够看到实际上运行时注解实现控件注入相对简单些,但因为这种方式使用了反射,运行效率上相对差一些。