源码地址:https://github.com/renzhenming/MyButterknife
相信大多数安卓人员开发中都在使用ButterKnife进行代码生成。使用ButterKnife有几个好处,第一,提高开发效率,可以一键生成布局中所有的View对象或者设置View的点击事件,这两个功能是我们最常用的。第二,不会影响app运行效率,ButterKnife采用编译时注解的方式生成代码,相对于XUtils的注解方式,在效率上有很大的优势,看过Xutils源码的话你会发现,里边的注解是反射的方式实现的,而反射在一定程度上是会增大运行的开销的。
ButterKnife的作者是就职于谷歌的JakeWharton,在GitHub上可以看到完整的代码,https://github.com/JakeWharton/butterknife/tree/master/butterknife-annotations/src/main/java/butterknife,ButterKnife的功能是相当多的,不止是我们最长用的bindView set onClick等,看看这些注解就大概知道它所涉及到的功能点
今天打算实现的是其中一个BindView注解,后期可能会实现onClick方法,借此来加深一下编译时注解的使用
当我们在一个Activity中使用ButterKnife.bind()之后,重新编译会生成这样一个类,xxxx_ViewBinding,xxxx代表的是当前Activity的名字,这个类是什么样的有什么作用,我们简单拷贝一个来看
public final class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
MainActivity_ViewBinding(MainActivity target) {
this.target = target;
target.world = Utils.findViewById(target,2131427424);
target.bitch = Utils.findViewById(target,2131427425);
}
@Override
@CallSuper
public final void unbind() {
target.world = null;
target.bitch = null;
}
}
从这个类上可以看到,使用ButterKnife并非不用findViewById了,而是把这个工作交给了编译器去自动生成,然后在bind方法调用的时候,反射一次创建这个自动生成的类的对象从而实现view注入的功能,那么为什么编译时会自动生成类呢,这要用到注解的知识,接下来我们开始自己手写一个简单的ButterKnife工具
我们仿照JakeWharton的分包形式,创建一个butterknife-annotation和butterknife-compiler的Java library(butterknife-compiler必须时Java library,因为注解生成器需要继承AbstractProcessor,这个类只有Java工程可以引用到),我们在butterknife-annotation中创建注解BindView
/**
* Created by renzhenming on 2018/4/24.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
在butterknife-compiler中创建注解生成器
/**
* Created by renzhenming on 2018/4/24.
* AbstractProcessor这个类是Java中的,只能在ava Library中使用
*/
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
private Filer mFiler;
private Elements mElementUtils;
/**
* init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。
* ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
* @param processingEnv 提供给 processor 用来访问工具框架的环境
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
}
/**
* 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6
* @return 使用的Java版本
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
* @return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
//仿照Butternife源码
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation: getSupportAnnotations()){
types.add(annotation.getCanonicalName());
}
return types;
}
private Set<Class<? extends Annotation>> getSupportAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindView.class);
return annotations;
}
/**
* 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
* 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素
* @param set 请求处理的注解类型
* @param roundEnvironment 有关当前和以前的信息环境
* @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;
* 如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
*
* 这里的log信息只能在gradle console中看到,Android logcat看不到。要注意
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
...........................
}
}
注意注解生成器的这个library build.gradle的配置
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
//AutoService注解使用
compile 'com.google.auto.service:auto-service:1.0-rc3'
//自动生成类相关
compile 'com.squareup:javapoet:1.7.0'
compile project(':butterknife-annotation')
}
//解决 错误: 编码GBK的不可映射字符 的报错
tasks.withType(JavaCompile){
options.encoding='UTF-8'
}
//android studio不完全能兼容Java8,所以指定编译版本位1.7
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
在我们项目主工程的app 配置文件build.gradle中设置依赖
compile project(':butterknife-annotation')
compile project(':butterknife-compiler')
这时候编译一下工程就可以执行到ButterKnifeProcessor 的process方法中了,如果看不到打印,检查两点
第一:目标文件(bind绑定的activity)是否相比于上一次编译有修改,如果代码没有改变,不会重新编译
第二:因为是Java library中打印的log,在Android studio的logcat中是无法看到的,你需要到Gradle Console中去看
process方法有执行了,那么进行下一步,编写自动生成类的代码,在process方法中加入如下代码
//------------获取注解-----------
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
//LinkedHashMap输出和输入的顺序相同,先输入就先输出
Map<Element,List<Element>> elementsMap = new LinkedHashMap<>();
for (Element element : elements) {
//这里会把所有跟注解有关的field全部拿到,包括各个类中的field,也就是说在
//编译时,项目中所有涉及到这个注解的地方的所有field都在这个Set中返回了,
//我们需要手动进行分类
System.out.println("-----------------------"+element.getSimpleName());
//得到的enclosingElement是这个field所在类的类名
Element enclosingElement = element.getEnclosingElement();
System.out.println("------------enclosingElement-----------"+enclosingElement.getSimpleName());
//以类名位key值存储一个类中所有的field到集合中
List<Element> bindViewElements = elementsMap.get(enclosingElement);
if (bindViewElements == null){
bindViewElements = new ArrayList<>();
elementsMap.put(enclosingElement,bindViewElements);
}
bindViewElements.add(element);
}
//------------生成代码-----------
for (Map.Entry<Element,List<Element>> entry:elementsMap.entrySet()){
Element enclosingElement = entry.getKey();
List<Element> bindViewElements = entry.getValue();
ClassName unbinderClassName = ClassName.get("com.rzm.butterknife","Unbinder");
System.out.println("------------Unbinder-----------"+unbinderClassName.simpleName());
//得到类名的字符串
String activityName = enclosingElement.getSimpleName().toString();
ClassName activityClassName = ClassName.bestGuess(activityName);
//拼装这一行代码:public final class xxx_ViewBinding implements Unbinder
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName+"_ViewBinding")
//类名前添加public final
.addModifiers(Modifier.FINAL,Modifier.PUBLIC)
//添加类的实现接口
.addSuperinterface(unbinderClassName)
//添加一个成员变量,这个名字target是仿照butterknife
.addField(activityClassName,"target",Modifier.PRIVATE);
//实现Unbinder的方法
//CallSuper这个注解不像Override可以直接拿到,需要用这种方式
ClassName callSuperClass = ClassName.get("android.support.annotation","CallSuper");
MethodSpec.Builder unbindMethod = MethodSpec.methodBuilder("unbind")//和你创建的Unbinder中的方法名保持一致
.addAnnotation(Override.class)
.addAnnotation(callSuperClass)
.addModifiers(Modifier.FINAL, Modifier.PUBLIC);
//添加构造函数
MethodSpec.Builder constructMethodBuilder = MethodSpec.constructorBuilder()
.addParameter(activityClassName,"target");
constructMethodBuilder.addStatement("this.target = target");
for (Element bindViewElement : bindViewElements) {
String fieldName = bindViewElement.getSimpleName().toString();
//在构造方法中添加初始化代码
ClassName utilsClassName = ClassName.get("com.rzm.butterknife", "Utils");
BindView annotation = bindViewElement.getAnnotation(BindView.class);
int resId = annotation.value();
constructMethodBuilder.addStatement("target.$L = $T.findViewById(target,$L)",fieldName,utilsClassName,resId);
//在unbind方法中添加代码 target.textView1 = null;
//不能用addCode,因为它不会在每一行代码后加分号和换行
unbindMethod.addStatement("target.$L = null",fieldName);
}
classBuilder.addMethod(constructMethodBuilder.build());
classBuilder.addMethod(unbindMethod.build());
//开始生成
try {
//得到包名
String packageName = mElementUtils.getPackageOf(enclosingElement)
.getQualifiedName().toString();
JavaFile.builder(packageName,classBuilder.build())
//添加类的注释
.addFileComment("butterknife 自动生成")
.build().writeTo(mFiler);
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
创建一个butterknife安卓library,创建类ButterKnife,实现bind方法,通过反射将自动生成的类生成注入view
public class ButterKnife {
public static Unbinder bind(Activity activity){
try {
Class<? extends Unbinder> clazz = (Class<? extends Unbinder>) Class.forName(activity.getClass().getName() + "_ViewBinding");
//构造函数
Constructor<? extends Unbinder> unbinderConstuctor = clazz.getDeclaredConstructor(activity.getClass());
Unbinder unbinder = unbinderConstuctor.newInstance(activity);
return unbinder;
} catch (Exception e) {
e.printStackTrace();
}
return Unbinder.EMPTY;
}
}
Unbinder
···
public interface Unbinder {
//仿照butterknife源码
@UiThread
void unbind();
Unbinder EMPTY = new Unbinder() {
@Override
public void unbind() {
}
};
}
···
Utils
public class Utils {
public static <T extends View> T findViewById(Activity activity,int viewId){
return (T)activity.findViewById(viewId);
}
}
测试一下使用
public class MainActivity extends AppCompatActivity {
@BindView(R.id.text1)
TextView world;
@BindView(R.id.text2)
TextView bitch;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
world.setText("aaaaa");
}
}
重新编译一下,可以看到自动生成的类
运行成功,至此我们不需要写findViewById也可以得到view对象了