上一篇博文写了运行时注解实现ButterKnife:简单实现ButterKnife(运行时注解),这篇讲讲关于编译时注解实现ButterKnife,如果你还不了解在AndroidStudio实现编译时注解,请参考我另一篇博文:Android 编译时注解。
运行时注解,实现原理很简单,就是通过java反射机制获取view的id,然后实例化后再注入即可。但是编译时注解,由于注解只保留到class或source,因此我们无法在运行时对其具体的view进行操作,因此要找别的方法。
阅读ButterKnife的实现源码,发现其原理是这样的,在编译期间,通过编译处理器,生成一个用于注入控件等的类文件,在运行期间,通过反射来获取该类,执行注入即可。在这里我只实现BindView与OnClick的注解。
注:保留到class与source有何区别,我在网上找到一种说法是:源代码级别的注解有两个意图, 一是作为文档的补充, 给人看的, 比如Override注解, 二是作为源代码生成器(java和android都有注解处理器APT)的材料。同样字节码级别的注解, 可以作为字节码修改, 插桩, 代理的依据, 可以使用aspectj, asm等工具进行字节码修改. 比如一些模块间调用, 如果你直接写代码, 会导致耦合, 此时可以加入一个注解, run一下asm这样的工具, 将注解标注的方法字段以generate的方式插入进去。
具体的AndroidStudio上的编译时注解操作就不多说了,参考上面提到的的博文,我就直接简述步骤上代码了。
新建Module(Java Library):annotations,并新建BindView与OnClick注解。
BindView.java
package com.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* BindView 注解
* Created by DavidChen on 2017/7/26.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
OnClick.java
package com.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* OnClickListener
* Created by DavidChen on 2017/7/26.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
int[] value();
}
没啥可多说的,注意由于是编译时注解,需要指定Retention为CLASS,为SOURCE也行。
新建Module(Java Library):compiler,并新建注解处理器。这里由于每个类内的所有注解都生成在同一个java文件,所以,用一个InjectorInfo来存储新生成的注入类中包含的变量与方法等信息。
InjectorInfo.java
package com.example;
import java.util.HashMap;
import java.util.Map;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import static javax.lang.model.element.ElementKind.PACKAGE;
/**
* 要生成的注入类相关信息
* Created by DavidChen on 2017/7/26.
*/
public class InjectorInfo {
private static final String SUFFIX = "_ViewBinding"; // 注入类统一后缀
private static final String VIEW_TYPE = "android.view.View";
private String mPackageName; // 包名
private String mInjectorName; // 注入类名
private TypeElement mTypeElement; // 原类中的类Element
Map variableElementMap = new HashMap<>(); // 存储BindView注解的变量
Map executableElementMap = new HashMap<>(); // 存储OnClick注解的变量
InjectorInfo(TypeElement typeElement) {
this.mTypeElement = typeElement;
this.mPackageName = getPackage(mTypeElement).getQualifiedName().toString();
this.mInjectorName = generateClassName(mPackageName, mTypeElement);
}
TypeElement getTypeElement() {
return mTypeElement;
}
String generateCode() {
StringBuilder builder = new StringBuilder();
builder.append("// Generated code from Butter Knife. Do not modify!\n");
builder.append("package ").append(mPackageName).append(";\n\n");
builder.append("import ").append(VIEW_TYPE).append(";\n");
builder.append("\n");
builder.append("public class ").append(mInjectorName).append(" implements").append(" VIEW.OnClickListener").append(" {\n");
builder.append("private ").append(mTypeElement.getQualifiedName()).append(" target;\n");
generateMethods(builder);
builder.append("\n");
implementsEvents(builder);
builder.append("\n");
builder.append(" }\n");
return builder.toString();
}
/**
* 生成代码,实现OnClickListener事件中的回调方法
*/
private void implementsEvents(StringBuilder builder) {
builder.append("public void onClick(VIEW v) {\n");
builder.append("switch(v.getId()) {\n");
for (Integer[] ids : executableElementMap.keySet()) {
ExecutableElement executableElement = executableElementMap.get(ids);
for (int id : ids) {
builder.append("case ").append(id).append(":\n");
}
builder.append("target.").append(executableElement.getSimpleName()).append("(v);\n");
builder.append("break;\n");
}
builder.append("}\n");
builder.append("}\n");
}
/**
* 生成代码,在构造方法中实现View的绑定与点击事件绑定
*/
private void generateMethods(StringBuilder builder) {
builder.append("public ").append(mInjectorName).append("(")
.append(mTypeElement.getQualifiedName()).append(" target, ");
builder.append("View source) {\n");
builder.append("this.target = target;\n");
for (int id : variableElementMap.keySet()) {
VariableElement variableElement = variableElementMap.get(id);
TypeMirror typeMirror = variableElement.asType();
String type = typeMirror.toString();
String name = variableElement.getSimpleName().toString();
builder.append("target.").append(name).append(" = ");
builder.append("(").append(type).append(")");
builder.append("source.findViewById(");
builder.append(id).append(");\n");
}
for (Integer[] ids : executableElementMap.keySet()) {
for (int id : ids) {
builder.append("source.findViewById(").append(id).append(").setOnClickListener(this);\n");
}
}
builder.append(" }\n");
}
/**
* 生成注入类文件名
*
* @param packageName 包名
* @param typeElement 类元素
* @return 注入类类名,如,MainActivity.java 生成类名为MainActivity_ViewBinding.java
*/
private static String generateClassName(String packageName, TypeElement typeElement) {
String className = typeElement.getQualifiedName().toString().substring(
packageName.length() + 1).replace('.', '$');
return className + SUFFIX;
}
/**
* 获取PackageElement
*
* @throws NullPointerException 如果element为null
*/
private static PackageElement getPackage(Element element) {
while (element.getKind() != PACKAGE) {
element = element.getEnclosingElement();
}
return (PackageElement) element;
}
}
InjectorInfo类主要是封装了每个注解类中包含的BindView与OnClick注解,以及生成注解类相应的类名与包名,还有就是生成代码的几个方法。相信大家看起来也不会很吃力。
ButterKnifeProcessor.java
package com.example;
import com.google.auto.service.AutoService;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.JavaFileObject;
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
private Filer mFiler; // 用于生成java文件
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
}
@Override
public Set getSupportedAnnotationTypes() {
Set annotationTypes = new LinkedHashSet<>();
annotationTypes.add(BindView.class.getCanonicalName());
return annotationTypes;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
// 用于存放类与该类包含的注解集合
private Map injectorInfoMap = new HashMap<>();
@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
Set elements = roundEnv.getElementsAnnotatedWith(BindView.class);
// 遍历所有的BindView注解
for (Element element : elements) {
parseBindView(element);
}
Set eventElements = roundEnv.getElementsAnnotatedWith(OnClick.class);
// 遍历获取所有OnClick注解
for (Element element : eventElements) {
parseOnClick(element);
}
// 生成类文件
for (String qualifiedName : injectorInfoMap.keySet()) {
InjectorInfo injectorInfo = injectorInfoMap.get(qualifiedName);
try {
JavaFileObject sourceFile = mFiler.createSourceFile(qualifiedName + "_ViewBinding", injectorInfo.getTypeElement());
Writer writer = sourceFile.openWriter();
writer.write(injectorInfo.generateCode());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
private void parseOnClick(Element element) {
ExecutableElement executableElement = (ExecutableElement) element;
TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
String qualifiedName = typeElement.getQualifiedName().toString();
InjectorInfo injectorInfo = injectorInfoMap.get(qualifiedName);
if (injectorInfo == null) {
injectorInfo = new InjectorInfo(typeElement);
injectorInfoMap.put(qualifiedName, injectorInfo);
}
OnClick onClick = executableElement.getAnnotation(OnClick.class);
if (onClick != null) {
int[] idInt = onClick.value();
Integer ids[] = new Integer[idInt.length];
for (int i = 0; i < idInt.length; i++) {
ids[i] = idInt[i];
}
injectorInfo.executableElementMap.put(ids, executableElement);
}
}
private void parseBindView(Element element) {
VariableElement variableElement = (VariableElement) element;
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
String qualifiedName = typeElement.getQualifiedName().toString();
InjectorInfo injectorInfo = injectorInfoMap.get(qualifiedName);
if (injectorInfo == null) {
injectorInfo = new InjectorInfo(typeElement);
injectorInfoMap.put(qualifiedName, injectorInfo);
}
BindView bindView = variableElement.getAnnotation(BindView.class);
if (bindView != null) {
int id = bindView.value();
injectorInfo.variableElementMap.put(id, variableElement);
}
}
}
ButterKnifeProcessor类继承自AbstractProcessor类,并在process方法中,执行对注解的收集与存储,为了保证每个类只有一个注解类,因此用map存放,key为全限定类名,value为注入信息。最后再使用Filer工具生成注入java文件。
注意,这里使用AutoService来帮助我们配置Processor相关的信息。同时要依赖annotations。
新建Module(Android Library):api,并创建ButterKnife类。
ButterKnife.java
package com.example.api;
import android.app.Activity;
import android.view.View;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* ButterKnife工具类
* Created by DavidChen on 2017/7/26.
*/
public class ButterKnife {
public static void bind(Activity target) {
View sourceView = target.getWindow().getDecorView();
createBinding(target, sourceView);
}
private static void createBinding(Activity target, View source) {
Class targetClass = target.getClass();
String className = targetClass.getName();
try {
Class bindingClass = targetClass.getClassLoader().loadClass(className + "_ViewBinding");
Constructor constructor = bindingClass.getConstructor(targetClass, View.class);
constructor.newInstance(target, source);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
该类只实现了一个bind(Activity)方法,同时在createBinding()方法中,运用java反射加载我们生成的×××_ViewBinding类,并创建实例。由于我们上面的生成的注入类所有的注入方法都在构造方法中,因此只要创建实例就可以执行绑定操作了(与ButterKnife源码一致)。
在主项目中进行测试,注意要依赖annotations与api以及compiler这三个Module。当然也可以使用apt工具将comipler在打包时去除。具体看文章开头提到的博客。
package com.example.davidchen.blogdemo;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.example.BindView;
import com.example.OnClick;
import com.example.api.ButterKnife;
/**
* 测试activity
* Created by DavidChen on 2017/7/25.
*/
public class TestActivity extends AppCompatActivity {
@BindView(R.id.btn_enter)
public Button btn_enter;
@BindView(R.id.tv_result)
public TextView tv_result;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
ButterKnife.bind(this);
}
@OnClick({R.id.btn_enter, R.id.tv_result})
public void click(View view) {
switch (view.getId()) {
case R.id.btn_enter:
tv_result.setText("注入成功");
break;
case R.id.tv_result:
Toast.makeText(TestActivity.this, "guin", Toast.LENGTH_SHORT).show();
break;
}
}
@OnClick({R.id.btn_test})
public void click2(View view) {
switch (view.getId()) {
case R.id.btn_test:
Toast.makeText(TestActivity.this, "test2", Toast.LENGTH_SHORT).show();
break;
}
}
}
这时,CleanProject再Make Project,可以在…\app\build\generated\source\apt\debug\目录中看到生成的java文件,如下图:
而且,编译器已经将它编译为class文件,在…\app\build\intermediates\classes\debug\目录下,如图:
运行结果如下:
JavaPoet是专门用于生成java文件的库,在注解处理器生成java文件时,非常高效与方便,避免了我们在手动拼接时因为一时大意导致的各种括号或者分好等的遗漏。而且在生成的文件代码量过大时更加能凸显优势。
看一下javapoet版的InjectorInfo.java
package com.example;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import java.util.HashMap;
import java.util.Map;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import static javax.lang.model.element.ElementKind.PACKAGE;
/**
* 要生成的注入类相关信息
* Created by DavidChen on 2017/7/26.
*/
public class InjectorInfo {
private static final String SUFFIX = "_ViewBinding"; // 注入类统一后缀
private static final ClassName ONCLICK = ClassName.get("android.view.View", "OnClickListener");
private static final ClassName VIEW = ClassName.get("android.view", "View");
private String mPackageName; // 包名
private ClassName mInjectorClassName; // 注入类
private ClassName mOriginClassName; // 原类
Map variableElementMap = new HashMap<>();
Map executableElementMap = new HashMap<>();
InjectorInfo(TypeElement typeElement) {
this.mPackageName = getPackage(typeElement).getQualifiedName().toString();
mInjectorClassName = ClassName.get(mPackageName, typeElement.getSimpleName() + SUFFIX);
mOriginClassName = ClassName.get(mPackageName, typeElement.getSimpleName().toString());
}
/**
* 获取PackageElement
*
* @throws NullPointerException 如果element为null
*/
private static PackageElement getPackage(Element element) {
while (element.getKind() != PACKAGE) {
element = element.getEnclosingElement();
}
return (PackageElement) element;
}
JavaFile brewJava() {
return JavaFile.builder(mPackageName, createType())
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
private TypeSpec createType() {
// 生成类
TypeSpec.Builder builder = TypeSpec.classBuilder(mInjectorClassName.simpleName())
.addModifiers(Modifier.PUBLIC);
// 实现接口
builder.addSuperinterface(ONCLICK);
builder.addField(generateTarget());
builder.addMethod(generateConstructor());
builder.addMethod(generateEvent());
//
return builder.build();
}
private MethodSpec generateEvent() {
MethodSpec.Builder builder = MethodSpec.methodBuilder("onClick")
.addModifiers(Modifier.PUBLIC)
.addParameter(VIEW, "v")
.returns(void.class);
builder.beginControlFlow("switch(v.getId())");
for (Integer[] ints : executableElementMap.keySet()) {
ExecutableElement executableElement = executableElementMap.get(ints);
CodeBlock.Builder code = CodeBlock.builder();
for (int id : ints) {
code.add("case $L:\n", id);
}
code.add("target.$L(v)", executableElement.getSimpleName());
builder.addStatement("$L", code.build());
builder.addStatement("break");
}
builder.endControlFlow();
return builder.build();
}
private FieldSpec generateTarget() {
FieldSpec.Builder builder = FieldSpec.builder(mOriginClassName, "target", Modifier.PRIVATE);
return builder.build();
}
private MethodSpec generateConstructor() {
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(mOriginClassName, "target")
.addParameter(VIEW, "source");
builder.addStatement("this.target = target");
for (int id : variableElementMap.keySet()) {
CodeBlock.Builder code = CodeBlock.builder();
VariableElement variableElement = variableElementMap.get(id);
ClassName className = getClassName(variableElement);
code.add("target.$L = ", variableElement.getSimpleName());
code.add("($T)source.findViewById($L)", className, id);
builder.addStatement("$L", code.build());
}
for (Integer[] ints : executableElementMap.keySet()) {
for (int id : ints) {
builder.addStatement("source.findViewById($L).setOnClickListener(this)", id);
}
}
return builder.build();
}
private ClassName getClassName(Element element) {
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
TypeName type = TypeName.get(elementType);
if (type instanceof ParameterizedTypeName) {
return ((ParameterizedTypeName) type).rawType;
}
return (ClassName) type;
}
}
这块其实只是把生成的代码变成了brewJava(),以及用相关的api生成代码,其他没太多变化。而使用JavaPoet之后使得代码生成更加的面向对象化。
ButterKnifeProcessor.java
package com.example;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.JavaFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
private Filer mFiler; // 用于生成java文件
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
}
@Override
public Set getSupportedAnnotationTypes() {
Set annotationTypes = new LinkedHashSet<>();
annotationTypes.add(BindView.class.getCanonicalName());
return annotationTypes;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
// 用于存放类与该类包含的注解集合
private Map injectorInfoMap = new HashMap<>();
@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
Set elements = roundEnv.getElementsAnnotatedWith(BindView.class);
// 遍历所有的BindView注解。
for (Element element : elements) {
parseBindView(element);
}
Set eventElements = roundEnv.getElementsAnnotatedWith(OnClick.class);
// 遍历获取所有OnClick注解
for (Element element : eventElements) {
parseOnClick(element);
}
// 生成类文件
for (String qualifiedName : injectorInfoMap.keySet()) {
InjectorInfo injectorInfo = injectorInfoMap.get(qualifiedName);
JavaFile javaFile = injectorInfo.brewJava();
try {
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
private void parseOnClick(Element element) {
ExecutableElement executableElement = (ExecutableElement) element;
TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
String qualifiedName = typeElement.getQualifiedName().toString();
InjectorInfo injectorInfo = injectorInfoMap.get(qualifiedName);
if (injectorInfo == null) {
injectorInfo = new InjectorInfo(typeElement);
injectorInfoMap.put(qualifiedName, injectorInfo);
}
OnClick onClick = executableElement.getAnnotation(OnClick.class);
if (onClick != null) {
int[] idInt = onClick.value();
Integer ids[] = new Integer[idInt.length];
for (int i = 0; i < idInt.length; i++) {
ids[i] = idInt[i];
}
injectorInfo.executableElementMap.put(ids, executableElement);
}
}
private void parseBindView(Element element) {
VariableElement variableElement = (VariableElement) element;
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
String qualifiedName = typeElement.getQualifiedName().toString();
InjectorInfo injectorInfo = injectorInfoMap.get(qualifiedName);
if (injectorInfo == null) {
injectorInfo = new InjectorInfo(typeElement);
injectorInfoMap.put(qualifiedName, injectorInfo);
}
BindView bindView = variableElement.getAnnotation(BindView.class);
if (bindView != null) {
int id = bindView.value();
injectorInfo.variableElementMap.put(id, variableElement);
}
}
}
这里也没有太多变化,就是把生成文件的工作交给了javaFile.writeTo(mFiler),其中javaFile就是InjectorInfo中brewJava()方法生成的。
ok了,其他不用变,make project就可以了。来看一下生成的java文件:
可以看到所有绑定代码都差不多。但是自动添加了import,并且强制转换不再是全限定类名,更加倾向于我们的使用习惯。当然手动拼接也可以实现,但是更加繁琐罢了。
这里只是简单的实现了ButterKnife框架的两个功能,但是实现思路是一致的。都是先在编译时获取并处理注解,生成注入java文件,之后在运行时,通过反射获取到指定的注入类执行绑定操作,就实现了控件以及事件的注入。当然这里可能很多地方都欠缺考虑,请见谅。
附上源码地址:ButterKnife(编译时注解)