自定义ButterKnife(中)使用Javapoet库动态生成java文件
上篇之后,我们的自定义注解处理器已经可以读取到我们的注解了,但是没有内容。中篇就是带着大家动态生成java类XXX$ViewBinder,实现ButterKnife功能。(这里暂时还没有封装层API,所以看起来不是很像,下篇进行封装,就很像了。)
背景
如果了解ButterKnife的原理可以跳过下面的斜体字
众所周知,android项目通过gradle构建工具编译打包的时候,会执行很多task任务。大致会经历以下几个阶段
.java文件–>.class文件–>.dex文件–>.apk文件
ButterKnife的原理就是编译的时候动态生成XXX$ViewBinder类,在我们调用BufferKnife.bind(this)的时候,通过动态的类加载方式创建出这个ViewBinder类的实例,并且调用bind()方法完成给带有注解的对象进行id绑定的操作。
动态生成java文件我们也可以不用这个库,但是容易出错。
Javapoet这个库可以很方便地帮我们的构建一个java文件,下面我们就用这个库来帮我们生成一个XXX$ViewBinder类
正文
完善自定义注解
获取到所有的注解,并且按所属文件名区分
完善注解编译器,生成文件
完善自定义注解,让其接收view的 id
GavinBindView
// SOURCE 注解仅在源码中保留,class文件中不存在
// CLASS 注解在源码和class文件中都存在,但运行时不存在
// RUNTIME 注解在源码,class文件中存在且运行时可以通过反射机制获取到
@Target(ElementType.FIELD) // 注解作用在属性之上
@Retention(RetentionPolicy.CLASS) // 编译期原理(交予注解处理器)
public @interface GavinBindView {
// 返回R.id.xx值
int value();
}
编写生成文件的逻辑
那在初始化方法中获取几个比较重要的成员变量
GavinButterknifeProcessor
//..
public class GavinButterknifeProcessor extends AbstractProcessor {
// 操作Element工具类 (类、函数、属性都是Element)
private Elements elementUtils;
// type(类信息)工具类,包含用于操作TypeMirror的工具方法
private Types typeUtils;
// Messager用来报告错误,警告和其他提示信息
private Messager messager;
// 文件生成器 类/资源,Filter用来创建新的类文件,class文件以及辅助文件
private Filer filer;
// key:类节点, value:被@BindView注解的属性集合
private Map<TypeElement, List<VariableElement>> tempBindViewMap = new HashMap<>();
// 该方法主要用于一些初始化的操作,通过该方法的参数ProcessingEnvironment可以获取一些列有用的工具类
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
// 初始化
elementUtils = processingEnvironment.getElementUtils();
typeUtils = processingEnvironment.getTypeUtils();
messager = processingEnvironment.getMessager();
filer = processingEnvironment.getFiler();
messager.printMessage(Diagnostic.Kind.NOTE,
"注解处理器初始化完成,开始处理注解------------------------------->");
}
//...
}
process方法中我们做两件事
1、获取代码中使用了GavinBindView注解的元素集合,按照类名分组elementsInGroup(bindViewElements);
2、动态生成XXX$ViewBinder文件createJavaFile();
关于生成代码的api我们不常用,所以理解起来困难也很正常,多看几次就好了。耐心读代码和注释,会有很大的收获。已经将完整的源码上传到github,配合使用,效果更佳。
//..
public class GavinButterknifeProcessor extends AbstractProcessor {
//..
/*代码中几个常量
public class Constant {
public static final String CODE_CLASS_EXT_NAME = "$ViewBinder";
public static final String CODE_PARAM = "target";
public static final String CODE_METHOD_NAME = "bind";
}*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
System.out.println("GavinButterknifeProcessor.process...");
// 一旦有属性上使用@BindView注解
if (!SimpleUtils.isEmpty(set)) {
// 获取所有被 @BindView 注解的 元素集合
Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(GavinBindView.class);
elementsInGroup(bindViewElements);
try {
createJavaFile();
} catch (IOException e) {
e.printStackTrace();
messager.printMessage(Diagnostic.Kind.NOTE, "@IOException >>> " + e.getMessage());
}
}
return false;
}
/**
* 分类,key为类名,value为注解的Element集合
* {"Sample1Activity":[textview1Element,textview2Element]
* ,"Sample2Activity":[textview1Element,textview2Element]}
*
* @param bindViewElements
*/
private void elementsInGroup(Set<? extends Element> bindViewElements) {
if (!SimpleUtils.isEmpty(bindViewElements)) {
for (Element element : bindViewElements) {
messager.printMessage(Diagnostic.Kind.NOTE, "@BindView >>> " + element.getSimpleName());
if (element.getKind() == ElementKind.FIELD) {
VariableElement fieldElement = (VariableElement) element;
// 注解在属性之上,属性节点父节点是类节点
TypeElement enclosingElement = (TypeElement) fieldElement.getEnclosingElement();
// 如果map集合中的key:类节点存在,直接添加属性
if (tempBindViewMap.containsKey(enclosingElement)) {
tempBindViewMap.get(enclosingElement).add(fieldElement);
} else {
List<VariableElement> fields = new ArrayList<>();
fields.add(fieldElement);
tempBindViewMap.put(enclosingElement, fields);
}
}
}
}
}
void createJavaFile() throws IOException {
TypeElement viewBinderType = elementUtils.getTypeElement(GavinViewBinder.class.getName());
for (Map.Entry<TypeElement, List<VariableElement>> entry :
tempBindViewMap.entrySet()) {
//类名 TestActivity
ClassName className = ClassName.get(entry.getKey());
//实现接口泛型 implements GavinViewBinder<TestActivity>
ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBinderType), className);
//参数体配置(final TestActivity target)
ParameterSpec parameterSpec = ParameterSpec.builder(className, Constant.CODE_PARAM)
.addModifiers(Modifier.FINAL)
.build();
//声明方法
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constant.CODE_METHOD_NAME)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterSpec);
for (Element fieldElement :
entry.getValue()) {
//获取属性名
String fieldName = fieldElement.getSimpleName().toString();
//获取@GavinBindView注解的值
int annotationValue = fieldElement.getAnnotation(GavinBindView.class).value();
messager.printMessage(Diagnostic.Kind.NOTE, fieldName + ".annotationValue >>> " + annotationValue);
//target.tv = target.findViewById(R.id.tv);
String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
methodBuilder.addStatement(methodContent,
Constant.CODE_PARAM,
Constant.CODE_PARAM,
annotationValue);
messager.printMessage(Diagnostic.Kind.NOTE, methodBuilder.toString());
}
//生成类
TypeSpec typeSpec = TypeSpec.classBuilder(className.simpleName() + Constant.CODE_CLASS_EXT_NAME)//class XXX$ViewBinder
.addSuperinterface(typeName)//implements GavinViewBinder<XXXActivity>
.addModifiers(Modifier.PUBLIC)//public
.addMethod(methodBuilder.build())// 加入前面的方法
.build();
//生成文件
JavaFile javaFile = JavaFile.builder(className.packageName(), typeSpec)
.build();
javaFile.writeTo(filer);
}
}
//...
}
代码写好,我们引入主工程测试一下
public class TestActivity extends AppCompatActivity {
@GavinBindView(R.id.textView)
public TextView textView;
@GavinBindView(R.id.textView2)
public TextView textViewxx;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
new TestActivity$ViewBinder().bind(this);
textView.setText("没有手动findById");
textViewxx.setText("未封装api,直接使用Javapoet生成的类");
}
}
点击构建
完成后即可看见生成的java文件,生成的文件在 app/build/generated/ap_generated_sources/debug/out/包名/xxx
如果没有找到,那就按照类名去搜索
福利
些同学比较急着看效果,我标记出来给大家看看。
拓展
后面会带大家进入封装,上传到公共仓库提供给其他小伙伴使用。请看
Android进阶之路-自定义ButterKnife(下)