文章纯属个人学习的代码实现
网易云微专业公开课这节课 用
JavaPoet简单实现了ButterKnife的BindView功能
原理其实就是通过注解处理器处理注解,然后利用 JavaPoet面向对象的语法动态生成了一个Java文件
这里加入我们希望完成MainActivity的BindView功能,MainActivity的代码如下
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);
ButterKnife.bind(this);
tv.setText("Hello android");
}
}
我们要看到两个需要注意的地方,第一个就是
@BindView(R.id.tv)
TextView tv;
这里就是我们要绑定的视图
第二个地方是这里
ButterKnife.bind(this);
我们要用注解处理器和JavaPoet生成这样的文件
public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
@Override
public void bind(MainActivity target) {
target.tv = target.findViewById(2131165359);
}
}
这里可以看到我们生成了一个以MainActivity+$$ViewBinder的一个 java文件,他需要实现一个接口ViewBinder<MainActivity>
我们可以看看ViewBinder<MainActivity>这个接口
public interface ViewBinder<T> {
void bind(T target);
}
这里声明了一个泛型接口,表示他 接受任何泛型对象,不同泛型对象实现不同的bind(T target)
方法,比如我们刚刚看到的MainActivity$$ViewBinder类实现的
@Override
public void bind(MainActivity target) {
target.tv = target.findViewById(2131165359);
}
这里传入的类型是MainActivity
然后我们可以看到这个文件其实就是帮我们做了一个操作,利用传进来的MainActivity完成了tv控件的绑定 ,记住了这里的代码不用自己写,是注解处理器和JavaPoet生成的。
但是看到这里有同学要问了,你生成了这个文件,但是我没有调用呀?嗯,问的很好,这里我们还记得
ButterKnife.bind(this);
吗,我们来看看他的代码
public class ButterKnife {
//target.tv=target.findViewById(R.id.tv)
public static void bind(Object object) {
String className=object.getClass().getName()+"$$ViewBinder";
//类加载
try {
Class<?> clazz = Class.forName(className);
ViewBinder viewBinder = (ViewBinder) clazz.newInstance();
viewBinder.bind(object);
} catch (Exception e) {
e.printStackTrace();
}
}
}
其实这里是通过类加载就是这里的Class.forName实现了java文件的查找,找到了之后我们就可以操作这个Class对象,我们这里直接调用Class对象的实例化方法newInstance()去新建一个对象,这个对象是什么呢?
很简单,我们看到这里
String className=object.getClass().getName()+"$$ViewBinder";
这是我们上面看到的那个类呀MainActivity$$ViewBinder,那我们应该记得它实现了ViewBinder接口,那接口的实现类就可以转成对应的接口对象,这个有点拗口,不对想想也应该能理解,我们安卓里面很多类似的操作,比如把点击事件的接口返回一个接口对象,比如下面这样
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
这个匿名内部类不就完成了一个接口实现类,不同的点击事件就是不同的接口实现类,结果都返回一个View.OnClickListener的接口对象
好回到我们刚刚那里,
viewBinder.bind(object);
最后我们调用了这个,啥意思?其实就相当于
MainActivity$$ViewBinder.bind(mainActivity)
这里是不是就调用了我们之前用注解处理器和JavaPoet生成的java文件,所以这里也很清楚的可以知道ButterKinfe为啥如果我们不调用这个方法,页面视图绑定会空指针。
上面基本把原理讲完,最后我们看看注解处理器和JavaPoet怎么生成我们需要的这个文件
public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
@Override
public void bind(MainActivity target) {
target.tv = target.findViewById(2131165359);
}
}
我先扔一堆代码给大家,但是不要慌,后面我会一个个分析
@AutoService(Processor.class)
@SupportedAnnotationTypes({Constants.BINDERVIEW_ANNOTATION_TYPES})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindViewProcessor extends AbstractProcessor {
private Elements elementUitls;
private Types typeUtils;
private Messager messager;
private Filer filer;
private Map<TypeElement, List<Element>> tempBindViewMap = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUitls = processingEnv.getElementUtils();
typeUtils = processingEnv.getTypeUtils();
messager = processingEnv.getMessager();
filer = processingEnv.getFiler();
messager.printMessage(Diagnostic.Kind.NOTE, "init finish,start process annotation ..........");
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
if (set.isEmpty()) return false;
Set<? extends Element> bindViewElements = roundEnv.getElementsAnnotatedWith(BindView.class);
if (!bindViewElements.isEmpty()) {
valueOfMap(bindViewElements);
try {
createJavaFile();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
return false;
}
private void createJavaFile() throws IOException {
if (tempBindViewMap.isEmpty()) return;
for (Map.Entry<TypeElement, List<Element>> entry : tempBindViewMap.entrySet()) {
//获取当前类名
ClassName className = ClassName.get(entry.getKey());
//生成一个名为target参数 类型就是上面的typeName
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
.build();//target
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterSpec);
for (Element fieldElement : entry.getValue()) {
String fieldName = fieldElement.getSimpleName().toString();
//获取BindView上面的id值
int viewId = fieldElement.getAnnotation(BindView.class).value();
String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
//拼接成target.tv=target.findViewById(R.id.tv)
methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
Constants.TARGET_PARAMETER_NAME, viewId);
}
//找得到所有实现了ViewBinder接口的类
TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
// 这里需要理解一下 实现接口泛型 ViewBinder<MainActivity> 拼接成这个样子
ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
className);
if(typeName!=null) {
JavaFile.builder(className.packageName(),
TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")
.addSuperinterface(typeName) //实现viewBinder接口
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build()
).build().writeTo(filer);
}
}
}
private void valueOfMap(Set<? extends Element> bindViewElements) {
for (Element element : bindViewElements) {
//注解在属性之上,属性节点父节点是类节点
TypeElement classElement = (TypeElement) element.getEnclosingElement();
if (tempBindViewMap.containsKey(classElement)) {
tempBindViewMap.get(classElement).add(element);
} else {
List<Element> fields = new ArrayList<>();
fields.add(element);
tempBindViewMap.put(classElement, fields);
}
}
}
}
首先我们看看这个类头部的注解
@AutoService(Processor.class)
@SupportedAnnotationTypes({Constants.BINDERVIEW_ANNOTATION_TYPES})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
这里@AutoService(Processor.class)表示这是一个注解处理器
@SupportedAnnotationTypes({Constants.BINDERVIEW_ANNOTATION_TYPES})表示这个注解处理器需要处理的注解类型,这里这个名字必须正确,比如我这里的是
public static final String BINDERVIEW_ANNOTATION_TYPES="com.example.annotation.BindView";
错了就出问题了,如果发现注解处理器不生效首先检查一下这里
@SupportedSourceVersion(SourceVersion.RELEASE_7)
然后这里就是支持JDK7
我们再看看下面这个几个部分
private Elements elementUitls;
private Messager messager;
private Filer filer;
private Map<TypeElement, List<Element>> tempBindViewMap = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUitls = processingEnv.getElementUtils();
messager = processingEnv.getMessager();
filer = processingEnv.getFiler();
messager.printMessage(Diagnostic.Kind.NOTE, "init finish,start process annotation ..........");
}
首先是这个Elements elementUitls是用来操作类元素的,具体作用我们等一下分析下面的代码可以看到
Messager messager用于控制台打印消息的,Filer filer用于输出Java文件的,Map<TypeElement, List<Element>> tempBindViewMap主要用来存储类里面的元素信息,啥意思?就是用来存储类似
@BindView(R.id.tv)
TextView tv;
这样子的类变量信息,后面会分析到
init方法基本就是初始化这几个变量
下面就是核心方法,注解处理了
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
if (set.isEmpty()) return false;
Set<? extends Element> bindViewElements = roundEnv.getElementsAnnotatedWith(BindView.class);
if (!bindViewElements.isEmpty()) {
valueOfMap(bindViewElements);
try {
createJavaFile();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
return false;
}
我们通过
Set<? extends Element> bindViewElements = roundEnv.getElementsAnnotatedWith(BindView.class);
找到所有包含BindView注解的对象,然后我们看看valueOfMap做了啥
private void valueOfMap(Set<? extends Element> bindViewElements) {
for (Element element : bindViewElements) {
//注解在属性之上,属性节点父节点是类节点
TypeElement classElement = (TypeElement) element.getEnclosingElement();
if (tempBindViewMap.containsKey(classElement)) {
tempBindViewMap.get(classElement).add(element);
} else {
List<Element> fields = new ArrayList<>();
fields.add(element);
tempBindViewMap.put(classElement, fields);
}
}
}
这个方法其实就是通过遍历所有的添加了BindView注解的对象,然后
TypeElement classElement = (TypeElement) element.getEnclosingElement();
找到对应的属于哪个类的属性,这个 classElement就是对应的类名,我们这里其实就是MainActivity类,然后判断Map里面是否有该类,如果存在,就取出来然后给他的List<Element>对象添加一个element即使一个属性对象,然后不存在,就新建一个List<Element>对象并且添加一个element然后把对应的类和List<Element>放入Map存起来
说的很复杂哈,其实就是遍历你类里面使用到BindView注解的属性,然后完事
下面到了重头戏createJavaFile()这个方法即使真正生成Java文件的地方
private void createJavaFile() throws IOException {
if (tempBindViewMap.isEmpty()) return;
for (Map.Entry<TypeElement, List<Element>> entry : tempBindViewMap.entrySet()) {
//获取当前类名
ClassName className = ClassName.get(entry.getKey());
//生成一个名为target参数 类型就是上面的typeName
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
.build();//target
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterSpec);
for (Element fieldElement : entry.getValue()) {
String fieldName = fieldElement.getSimpleName().toString();
//获取BindView上面的id值
int viewId = fieldElement.getAnnotation(BindView.class).value();
String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
//拼接成target.tv=target.findViewById(R.id.tv)
methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
Constants.TARGET_PARAMETER_NAME, viewId);
}
//找得到所有实现了ViewBinder接口的类
TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
// 这里需要理解一下 实现接口泛型 ViewBinder<MainActivity> 拼接成这个样子
ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
className);
if(typeName!=null) {
JavaFile.builder(className.packageName(),
TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")
.addSuperinterface(typeName) //实现viewBinder接口
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build()
).build().writeTo(filer);
}
}
}
这个方法很长,没关系,我们从下往上看
//找得到所有实现了ViewBinder接口的类
TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
// 这里需要理解一下 实现接口泛型 ViewBinder<MainActivity> 拼接成这个样子
ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
className);
if(typeName!=null) {
JavaFile.builder(className.packageName(),
TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")
.addSuperinterface(typeName) //实现viewBinder接口
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build()
).build().writeTo(filer);
}
这里啥意思,首先这两句代码
TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
// 这里需要理解一下 实现接口泛型 ViewBinder<MainActivity> 拼接成这个样子
ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
className);
我们通过开头讲到的Element elementUitls这个可以操作类的对象,找到一个接口类型Constants.VIEWBINDER=public static final CharSequence VIEWBINDER = "com.example.library.ViewBinder";这里也不能错,错了玩完
找到这个接口之后,通过ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType), className);构建成一个类似这样的接口ViewBinder<MainActivity>
这里的className在开头的这个位置我们能看到
//获取当前类名
ClassName className = ClassName.get(entry.getKey());
作用就是获取当前类名,这是一个对象哈,不单单是类名还有其他属性
然后我们看这里
JavaFile.builder(className.packageName(),
TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")
通过className获取包名,然后传入类型对应要生成的类名这里其实就是MainActivity$$ViewBinder
然后
.addSuperinterface(typeName) //实现viewBinder接口
实现ViewBinder<MainActivity>接口
然后
.addModifiers(Modifier.PUBLIC)
声明类的访问权限为public
然后
.addMethod(methodBuilder.build())
传入要实现的方法,这个我们等一下往上看能看到,可以先预告一下,我们还记得这个类吗
public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
@Override
public void bind(MainActivity target) {
target.tv = target.findViewById(2131165359);
}
}
我们上面基本完成了类的外壳的搭建,结构是这样子的
package com.example.javapoetbutterknife;
public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
}
有包名,然后的类的一些配置,那我们知道等一下要实现的就是bind方法
先把上面收尾
).build().writeTo(filer);
其实就是写入Java文件,比如我这里的这个文件

不同版本的AS,位置不一样,不过基本都在 这个
build文件夹,具体可以百度一下
回到我们上面的,现在要实现一个方法,先分析一下,这个方法首先需要一个参数,这个参数怎么获取,参数类型是什么,还有参数有了之后,我们要实现里面的findViewById方法,好,我们先看代码
//生成一个名为target参数 类型就是上面的typeName
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
.build();//target
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterSpec);
for (Element fieldElement : entry.getValue()) {
String fieldName = fieldElement.getSimpleName().toString();
//获取BindView上面的id值
int viewId = fieldElement.getAnnotation(BindView.class).value();
String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
//拼接成target.tv=target.findViewById(R.id.tv)
methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
Constants.TARGET_PARAMETER_NAME, viewId);
}
又是一大坨,没事,我们先看第一句话
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
.build();//target
我们先看看这个Constants.TARGET_PARAMETER_NAME=public static final String TARGET_PARAMETER_NAME = "target";
其实就是声明一个参数类型 ClassName.get(entry.getKey())取出来的是MainActivity,然后这个对象ParameterSpec parameterSpec其实就是类似这个(MainActivity target),记得对比上面那个方法代码
继续走起
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterSpec);
这里构造了一个方法对象,首先老规矩看看这个 Constants.BIND_METHOD_NAME是啥
public static final String BIND_METHOD_NAME = "bind";
其实就是方法名称bind,没错第一个参数传的就是方法 名称,然后是Override.class注解,方法访问权限为public,参数为(MainActivity target)上面生成的
最后最后啦 ,就是里面的方法 实现
for (Element fieldElement : entry.getValue()) {
String fieldName = fieldElement.getSimpleName().toString();
//获取BindView上面的id值
int viewId = fieldElement.getAnnotation(BindView.class).value();
String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
//拼接成target.tv=target.findViewById(R.id.tv)
methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
Constants.TARGET_PARAMETER_NAME, viewId);
}
首先 解释一下这个循环,因为一个类里面一般都不止一个控件需要绑定把,我们这里是demo所以我只写了一个TextView tv ,然后
String fieldName = fieldElement.getSimpleName().toString();
获取控件名称,比如这里fieldName ==tv
然后获取注解的ID值
//获取BindView上面的id值
int viewId = fieldElement.getAnnotation(BindView.class).value();
我们demo就是R.id.tv的整型值
下面有个比较需要稍微了解JavaPoet的东西
String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
$N for Name就是名字 $L for Link就是引用 这里 $N就是target
$L 即使R.id.tv的整型值
最后
//拼接成target.tv=target.findViewById(R.id.tv)
methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
Constants.TARGET_PARAMETER_NAME, viewId);
拼接成target.tv=target.findViewById(R.id.tv)
Ok 全剧终
大家想了解更多直接去我的github看代码实现