社会犹如一条船,每个人都要有掌舵的准备。 — 易卜生
写在前面
在上一篇文章《注解入门》最后提出了一个问题编译时注解是什么?
注解的生命周期为RUNTIME称为运行时注解,注解的生命周期为CLASS就称为编译时注解。
APT(Annotation Processing Tool)技术就是通过编译期解析注解,并且生成Java代码的一种技术,一般会结合Javapoet技术生成Java代码。
前期准备
在正式讲解APT技术之前,先认识两个知识点,分别是Element和SPI机制,这是APT技术的基础,一定要掌握。
1.Element
Element指的是一系列与之相关的接口集合,存在javax.lang.model.element包下。Element代表的是程序的一个元素,可以是包,类,接口,属性变量,方法,方法形参,泛型参数等元素。Element所代表的元素只在编译器可见,用于保存元素在编译期间的各种状态。
2.SPI机制
SPI(Service Provider Interface)的作用是为接口寻找服务实现,在项目中通过配置文件为接口寻找服务实现。
下面就介绍如何配置文件:
- 首先在main目录下创建resources目录。
- 然后在resources目录下创建META-INF目录。
- 接下来在META-INF目录下创建services目录。
- 最后创建一个文件名为javax.annotaion.processing.Processor的文件,其中的内容就是为接口寻找服务实现的全类名。
正式讲解
现在通过一个例子介绍APT技术,在Android应用开发中,ButterKnife是一个很好用的第三方框架,不知道的同学请自行百度,这里就通过写一个简单的ButterKnife实现自动绑定View的框架。
1.创建注解
新建一个Java Library Module,名称为apt_annotation,并创建一个注解BindView。
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int viewId() default 0;
}
2.创建注解处理器
新建一个Java Library Module,名称为apt_processor,并创建一个注解处理器AptProcessor,用来生成java代码。
public class AptProcessor extends AbstractProcessor {
/**
* 初始化函数
*
* @param processingEnv
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
/**
* 指定Java版本号,一般返回最新版本号
* 可以使用@SupportedSourceVersion注解代替
*
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 指定可解析的注解类型
* 可以使用@SupportedAnnotationTypes注解代替
*
* @return
*/
@Override
public Set getSupportedAnnotationTypes() {
Set annotationTypes = new LinkedHashSet<>();
annotationTypes.add(BindView.class.getCanonicalName());
return annotationTypes;
}
/**
* 核心函数
*
* @param annotations
* @param roundEnv
* @return
*/
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
Map>> elementMap = parseElements(roundEnv.getElementsAnnotatedWith(BindView.class));
generateJavaFile(elementMap);
return true;
}
/**
* 解析全部元素
*
* @param elements
* @return
*/
private Map>> parseElements(Set extends Element> elements) {
Map>> elementMap = new LinkedHashMap<>();
// 遍历全部元素
for (Element element : elements) {
// 判断当前元素是否是属性变量
if (!element.getKind().isField()) {
continue;
}
// 获取属性变量的上一级元素,即类元素
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
// 获取类名
String typeName = typeElement.getSimpleName().toString();
// 获取类的上一级元素,即包元素
PackageElement packageElement = (PackageElement) typeElement.getEnclosingElement();
// 获取包名
String packageName = packageElement.getQualifiedName().toString();
Map> typeElementMap = elementMap.get(packageName);
if (typeElementMap == null) {
typeElementMap = new LinkedHashMap<>();
}
Set variableElements = typeElementMap.get(typeName);
if (variableElements == null) {
variableElements = new LinkedHashSet<>();
}
variableElements.add(element);
typeElementMap.put(typeName, variableElements);
elementMap.put(packageName, typeElementMap);
}
return elementMap;
}
/**
* 生成Java文件
*
* @param elementMap
*/
private void generateJavaFile(Map>> elementMap) {
Set>>> packageElements = elementMap.entrySet();
for (Map.Entry>> packageEntry : packageElements) {
String packageName = packageEntry.getKey();
Map> typeElementMap = packageEntry.getValue();
Set>> typeElements = typeElementMap.entrySet();
for (Map.Entry> typeEntry : typeElements) {
String typeName = typeEntry.getKey();
Set variableElements = typeEntry.getValue();
ClassName className = ClassName.get(packageName, typeName);
FieldSpec.Builder fieldSpecBuilder = FieldSpec.builder(className, "target", Modifier.PRIVATE);
MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC)
.addParameter(className, "activity")
.addStatement("target = activity");
MethodSpec.Builder unbindMethodBuilder = MethodSpec.methodBuilder("unbind")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC);
for (Element element : variableElements) {
VariableElement variableElement = (VariableElement) element;
String variableName = variableElement.getSimpleName().toString();
BindView bindView = variableElement.getAnnotation(BindView.class);
bindMethodBuilder.addStatement("target." + variableName + " = activity.findViewById(" + bindView.viewId() + ")");
unbindMethodBuilder.addStatement("target." + variableName + " = null");
}
unbindMethodBuilder.addStatement("target = null");
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(typeName + "_ViewBinding")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ClassName.get("com.chad.apt.binder", "UnBinder"))
.addField(fieldSpecBuilder.build())
.addMethod(bindMethodBuilder.build())
.addMethod(unbindMethodBuilder.build());
JavaFile javaFile = JavaFile.builder(packageName, typeBuilder.build()).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
由于BindView在apt_annotation中,并且通过Javapoet技术生成Java代码,所以需要在apt_processor的build.gradle中引入apt_annotation和javapoet库。如下图:
3.创建SPI配置文件
接下来在apt_processor中创建SPI配置文件,创建步骤在前面已经讲过,这里不再复述,只看javax.annotation.processing.Processor文件内容如何定义,如下图的文件内容为com.chad.apt.processor.AptProcessor,也就是为Processor接口寻找AptProcessor服务实现类。
4.创建绑定者
新建一个Android Library Module,名称为apt_binder,并且创建一个绑定者AptBinder和一个UnBinder接口,AptBinder中的bind函数通过反射机制创建注解处理器生成的Java类并绑定View,UnBinder接口用来解绑View。
public class AptBinder {
public static UnBinder bind(Activity activity) {
try {
Class> activityClass = activity.getClass();
Class> viewBindingClass = Class.forName(activityClass.getName() + "_ViewBinding");
Method bindMethod = viewBindingClass.getMethod("bind", activityClass);
UnBinder unBinder = (UnBinder) viewBindingClass.newInstance();
bindMethod.invoke(unBinder, activity);
return unBinder;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public interface UnBinder {
void unbind();
}
5.如何使用
新建一个Android Module,名称为app,并且将apt_annotation,apt_processor和apt_binder三个Library引入到app build.gradle。如下图:
- 创建activity_main.xml
- 创建MainActivity
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
@BindView(viewId = R.id.id_hello)
TextView mTvHello;
@BindView(viewId = R.id.id_world)
TextView mTvWorld;
private UnBinder unBinder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 绑定
unBinder = AptBinder.bind(this);
mTvHello.setText("Hello!!!");
mTvWorld.setText("World!!!");
Log.d(TAG, "bind : mTvHello = " + mTvHello + " , mTvWorld = " + mTvWorld);
// 解绑
unBinder.unbind();
Log.d(TAG, "unbind : mTvHello = " + mTvHello + " , mTvWorld = " + mTvWorld);
}
}
在MainActivity中通过注解声明TextView属性变量,在onCreate()函数中通过AptBinder.bind()实现自动绑定View并返回一个UnBinder,UnBinder调用unbind()函数用来解绑View。
然后rebuild就会自动生成java代码,即build > generated > source > apt > debug/release > com.chad.annotation目录下的MainActivity_ViewBinding.java文件。
public class MainActivity_ViewBinding implements UnBinder {
private MainActivity target;
public void bind(MainActivity activity) {
target = activity;
target.mTvHello = activity.findViewById(2131165252);
target.mTvWorld = activity.findViewById(2131165253);
}
@Override
public void unbind() {
target.mTvHello = null;
target.mTvWorld = null;
target = null;
}
}
6.工程目录
下图就是整个工程的目录结构:
总结
APT技术可以在编译期间生成Java代码,对于注解而言,有效的避免了运行时注解通过反射解析注解信息影响效率的问题。
SPI机制通过配置文件的方式使用起来很麻烦,即要创建目录又要创建文件。Google的auto-service库提供了一种简单的配置方式,通过注解就可以解决繁琐的创建过程,使用方式很简单,这里就不详细讲解了。
项目地址:https://github.com/zhangjunxiang1995/Apt