日常项目开发中,注解使用越来越广泛,我们会经常用到各类注解框架为我们减轻工作中的一些重复劳动,比如AndroidAnnotation、Dagger2、ButterKnife等这些大名鼎鼎的框架。这些框架有一个共同点就是使用编译时生成代码代替反射,大大优化了性能。
那为什么一个小小的注解@BindView就可以实现view的查找功能呢?本文就来一探究竟,打造一个类似ButterKnife简单实现view绑定的注解框架。
在开始之前,我们先认识一项关键技术APT(Annotation Processing Tool)。官方解释:APT是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码。简单的说就是APT在编译时把注解生成代码,关于APT更多知识此处不展开,有兴趣可以百度查看相关资料。
伴随着去年Android Gradle 插件 2.2版本发布,android-apt的作者在官网发表声明后续将不会继续维护android-apt,并推荐大家使用 Android 官方插件annotationProcessor。不过由于很多框架还是用的APT,本文还是基于APT实现。
一、创建工程
首先在Android studio里新建一个Android工程APTDemo。为了使用android-apt插件,需要在工程的build.gradle中加入依赖。
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
二、创建 annotation Module
新建一个Java Library Module,命名annotation,然后创建一个BindView注解类。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value() default 0;
}
BindView的target是FIELD,只对成员变量进行注解,有一个int类型的参数。默认值为0,用来传入view的Id。
build.gradle,采用默认就好。
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
三、创建 compiler Module
新建一个Java Library Module,命名compiler。
build.gradle
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.7.0'
compile project(':annotation')
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
依赖了annotation模块,因为要使用annotation中定义的Bindview注解,另外引入了auto-service和javapoet库。auto-service ,主要用于注解 Processor,对其生成 META-INF 配置信息。javapoet可以通过预先设置好的规则,自动生成Java 代码文件,这个真是个好东西。
新建BindViewProcesor类,这个类就是整个注解框架的核心,包括自动生成代码等。
@AutoService(Processor.class)
public class BindViewProcesor extends AbstractProcessor {
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
下面我们来编辑BindViewProcesor类,主要逻辑都在process方法中。
为了一些工具类的方便使用,重写父类的init方法。
private Elements elements;
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
elements = processingEnvironment.getElementUtils();
filer = processingEnvironment.getFiler();
}
Elements是元素操作相关的辅助类,主要用于获取各种元素,结构类似DOM树。Filer是文件操作的辅助类。parentAndChildMap是一个map,用来存放类与方法的对应关系。
修改getSupportedAnnotationTypes,指定可以被注解处理器处理的类型,这里是BindView.class。
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(BindView.class.getCanonicalName());
}
另外还有一个指定java版本的方法getSupportedSourceVersion,这里我们使用注解@SupportedSourceVersion(SourceVersion.RELEASE_7)。
下面就是核心方法process的修改,大致的步骤如下:
1、获取所有标注了@BindView注解的的Element。
2、遍历标注了注解的Element集合,获取每一个Element的父元素,由于@BindView的Target是FIELD,那么父元素就是该FIELD的类即TypeElement。当然由于一个类中可能有多个标记了@BindView的字段,此处用HashMap来存放之间的对应关系。
3、遍历HashMap,通过javapoet生成目标类。先指定MethodSpec的生成规则,接着指定TypeSpec和JavaFile的规则,最后调用javaFile.writeTo(filer)生成Java文件。
for (Map.Entry<TypeElement, List<Element>> entry : parentAndChildMap.entrySet()) {
TypeElement typeElement = entry.getKey();
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bindView").addModifiers(Modifier.PUBLIC,
Modifier.STATIC).returns(void.class).addParameter(ClassName.get(typeElement.asType()), "activity");
List<Element> childElementList = entry.getValue();
for (Element element : childElementList) {
int id = element.getAnnotation(BindView.class).value();
String statement = String.format("activity.%s = (%s)activity.findViewById(%d)", element.getSimpleName
(), ClassName.get(element.asType()).toString(), id);
methodSpecBuilder.addStatement(statement);
}
TypeSpec typeSpec = TypeSpec.classBuilder("BindView$$" + typeElement.getSimpleName()).addModifiers
(Modifier.PUBLIC, Modifier.FINAL).superclass(ClassName.get(typeElement.asType())).addMethod
(methodSpecBuilder.build()).build();
JavaFile javaFile = JavaFile.builder(elements.getPackageOf(typeElement).getQualifiedName().toString(),
typeSpec).build();
try {
javaFile.writeTo(filer);
} catch (Exception e) {
e.printStackTrace();
}
}
具体javapoet的使用方法,可以参见Javapoet源码。后面有时间再单独写一篇关于javapoet的。
完整的代码如下:
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindViewProcesor extends AbstractProcessor {
private Elements elements;
private Filer filer;
private HashMap<TypeElement,List<Element>> parentAndChildMap;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
elements = processingEnvironment.getElementUtils();
filer = processingEnvironment.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
Set<? extends Element> fieldElements = roundEnv.getElementsAnnotatedWith(BindView.class);
if (fieldElements == null) {
return false;
}
parentAndChildMap = new LinkedHashMap<>();
for (Element fieldEle : fieldElements) {
TypeElement typeElement = (TypeElement) fieldEle.getEnclosingElement();
if (parentAndChildMap.containsKey(typeElement)) {
parentAndChildMap.get(typeElement).add(fieldEle);
} else {
List<Element> childEleList = new ArrayList<>();
childEleList.add(fieldEle);
parentAndChildMap.put(typeElement, childEleList);
}
}
for(Map.Entry<TypeElement,List<Element>> entry:parentAndChildMap.entrySet()){
TypeElement typeElement = entry.getKey();
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(ClassName.get(typeElement.asType()), "activity");
List<Element> childElementList = entry.getValue();
for(Element element:childElementList){
int id = element.getAnnotation(BindView.class).value();
String statement = String.format("activity.%s = (%s)activity.findViewById(%d)", element
.getSimpleName(), ClassName.get(element.asType()).toString(), id);
methodSpecBuilder.addStatement(statement);
}
TypeSpec typeSpec = TypeSpec.classBuilder("BindView$$" + typeElement.getSimpleName()).addModifiers(Modifier
.PUBLIC, Modifier.FINAL).superclass(ClassName.get(typeElement.asType())).addMethod(methodSpecBuilder.build())
.build();
JavaFile javaFile = JavaFile.builder(elements.getPackageOf(typeElement).getQualifiedName().toString(),
typeSpec).build();
try {
javaFile.writeTo(filer);
} catch (Exception e) {
e.printStackTrace();
}
}
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(BindView.class.getCanonicalName());
}
}
四、使用注解
为了在app模块中使用注解,需要在app的build.gradle配置注解的依赖。
compile project(':compiler')
新建一个MainActivity,在textview上标记注解@BindView(R.id.text)
public class MainActivity extends AppCompatActivity {
@BindView(R.id.text)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
然后编译一下整个工程,编译完成之后,会在app/build/generated/source/apt/debug/目录下自动生成BindView$$MainActivity类。
public final class BindView$$MainActivity extends MainActivity {
public static void bindView(MainActivity activity) {
activity.textView = (android.widget.TextView)activity.findViewById(2131427415);
}
}
这个就是compiler中根据注解按照设定的规则使用Javapoet自动生成的。如果不知道在compiler中怎样重写process方法,我们可以先手动写出BindView$$MainActivity类,然后参考这个类再去想我们该怎样写自动生成代码的规则,这样会简单很多。
此时MainActivity中textview还没有初始化,需要在onCreate方法中调用BindView$$MainActivity.bindView(this)进行注册。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindView$$MainActivity.bindView(this);
textView.setText("Hello World!");
}
此时就完成了通过一个@BindView注解实现view的初始化的目的,整个流程都是套路,具体实现就是compiler中的process方法了。当然本文中的自定义框架只是初步的实现了@BindView的功能,如果想要完整的实现类似ButterKnife的功能,可以参考ButterKnife的源码。