理解ButterKnife(自动生成绑定资源的代码)

理解ButterKnife:自动生成绑定资源的代码 概述:ButterKnife重构代码 Android开发中,涉及UI设计相关的类总会有一段代码用来完成资源/事件绑定,例如:

**MainActivity.java:**private TextView txtTop; private ImageView imgTop; private ListView mListView; @Override protected void onCreate(Bundle savedInstanceState) { txtTop = (TextView)findViewById(R.id.txt_top); imgTop = (ImageView)findViewById(R.id.img_top); mListView = (ListView)findViewById(R.id.samples_list); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id){ // do something. } }); }

开源框架ButterKnife正是为自动生成这一段代码而设计的。使用ButterKnife重构上述代码之后:
**MainActivity.java:**@BindView(R.id.samples_list) ListView mListView; @BindView(R.id.txt_top) TextView txtTop; @BindView(R.id.img_top) ImageView imgTop; @Override protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); ButterKnife.bind(this); }@OnItemClick(R.id.samples_list) void onSampleListClick(AdapterView parent, View view, int position, long id) { // do something. }

自动生成的代码位于:
AndroidStudio开发工具Project目录下: app\build\generated\source\apt\……\MainActivity$$ViewBinder.java

**MainActivity$$ViewBinder.java**public class MainActivity$$ViewBinder implements ViewBinder { @Override public Unbinder bind(final Finder finder, final T target, Object source) { InnerUnbinder unbinder = createUnbinder(target); View view; // 绑定ListView:根据资源找到欲绑定的View资源 view = finder.findRequiredView(source, 2131558482, "field 'mListView' and method 'onSampleListClick'"); // 绑定ListView:将找到的View资源强制转型为目标类型(ListView类型) target.mListView = finder.castView(view, 2131558482, "field 'mListView'"); unbinder.view2131558482 = view; // 绑定ListView的itemOnClick事件 ((AdapterView) view).setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView p0, View p1, int p2, long p3) { target.onSampleListClick(p0, p1, p2, p3); } }); // 绑定TextView txtTop:根据资源找到欲绑定的View资源 view = finder.findRequiredView(source, 2131558480, "field 'txtTop'"); // 绑定TextView txtTop:将找到的View资源强制转型为目标类型(TextView类型) target.txtTop = finder.castView(view, 2131558480, "field 'txtTop'"); // 绑定ImageView imgTop:根据资源找到欲绑定的View资源 view = finder.findRequiredView(source, 2131558481, "field 'imgTop'"); // 绑定ImageView imgTop:将找到的View资源强制转型为目标类型(TextView类型) target.imgTop = finder.castView(view, 2131558481, "field 'imgTop'"); return unbinder; }protected InnerUnbinder createUnbinder(T target) { return new InnerUnbinder(target); }protected static class InnerUnbinder implements Unbinder { private T target; View view2131558482; protected InnerUnbinder(T target) { this.target = target; }@Override public final void unbind() { if (target == null) throw new IllegalStateException("Bindings already cleared."); // 将绑定的应用指向null,解绑定 unbind(target); target = null; } protected void unbind(T target) { ((AdapterView) view2131558482).setOnItemClickListener(null); target.mListView = null; target.txtTop = null; target.imgTop = null; } } }**butterknife.internal.Finder.java** public static View findRequiredView(View source, @IdRes int id, String who) { // 找到View资源 View view = source.findViewById(id); if (view != null) { return view; } } public static T castView(View view, @IdRes int id, String who, Class cls) { // 将View资源转型为目标类型 return cls.cast(view); }

ButterKnife框架实现示意 1)ButterKnife实现示意 理解ButterKnife(自动生成绑定资源的代码)
文章图片

1、在编译期,ButterKnife的自定义注解器可以从编译器中读取到使用了自定义注解的注解信息;
2、根据注解信息以及ButterKnife.bind(this)传入的类实例生成java类文件;
3、在运行期,ButterKnife通过反射调用的方式创建第2步中的java类的实例对象;
4、这里的java类正是引言代码列表中的MainActivity$$ViewBinder.java(注意MainActivity传入的类实例的类名);
5、在java类的实例对象的方法中绑定资源/事件。
2)ButterKnife实现时序 理解ButterKnife(自动生成绑定资源的代码)
文章图片

【理解ButterKnife(自动生成绑定资源的代码)】(编辑期)使用ButterKnife的自定义注解类注解UI对象(例如:@BindView(id));
(编译期)编译器会调用ButterKnife的自定义注解器生成java类文件;
(运行期)在加载布局文件的时候调用ButterKnife.bind(this)传入的类实例;
(运行期)在创建类实例的时候会进行资源绑定(不同的版本调用位置不一样,在ButterKnife8.0.1中会使用创建出来的类实例调用bind()方法进行资源绑定;在ButterKnife8.5.1中则直接在类构造方法中进行资源绑定)。
二、ButterKnife的自定义注解器 1)自定义插件式注解器的实现原理 《深入理解Java虚拟机<周志明>》中对插件式注解器的介绍:
在JDK1.5之后,Java语言提供了对注解的支持,这些注解和普通的代码一样,是在运行期间发挥作用的。在JDK1.6中实现了JSR-269规范,提供了一组插入式注解器的标准API在编译期间对注解进行处理,我们可以把它看做一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树种的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。
有了编译器注解处理的API以后,我们的代码才有可能干涉编译器的行为,由于语法树中的任意元素,甚至包括代码注释都可以在插件中访问到,所以通过插件式注解处理器实现的插件在功能上有很大的发挥空间。
也就是说,我们可以通过实现自定义的插入式注解器在注解期读取语法树中的注解信息。
注解期:Javac的编译过程大致可以分为三个部分
——— 解析与填充符号表过程(生成抽象语法树)
——— 插入式注解器的注解处理过程
——— 分析与字节码生成过程
语法树:抽象语法树是一种用来描述程序代码结构的树形表达方式,语法树的每一个节点都代表着程序代码中的一个语法结构,例如:包、类型、修饰符、运算符、接口、返回值设置代码注释等都可以是一个语法结构
2)ButterKnife中的自定义插件式注解器 自定义插件式注解器需要继承javax.annotation.processing.AbstractProcessor。ButterKnife中的自定义注解器是ButterKnifeProcessor类。Javac编译器在执行注解处理器代码时要调用AbstractProcessor.process()执行注解的处理。
process()方法首先会遍历语法树中的节点信息,并取出所有ButterKnife自定义注解的节点信息;然后根据相关的节点信息逐一进行解析,构建出用于生成代码的BindingSet对象集合;最后遍历BindingSet对象集合生成java类文件。
// AutoService属于Google的一个开源注解工具 // 其目的是将ButterKnifeProcessor声明为一个插入式注解器 // 使得编译器在执行插入式注解器的注解处理过程时可以遍历到ButterKnifeProcessor @AutoService(Processor.class) public final class ButterKnifeProcessor extends AbstractProcessor { …… // process()是Javac编译器在执行注解处理器代码时要调用的过程 @elements 语法树中的所有的注解信息集合 @env 包含了语法树中的所有的节点信息 @Override public boolean process(Set elements, RoundEnvironment env) { // 解析语法树,从语法树中获取到所有需要绑定的节点的信息 Map bindingMap = findAndParseTargets(env); // 解析完毕之后,需要生成的代码结构就保存在BindingSet中 for (Map.Entry entry : bindingMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingSet binding = entry.getValue(); JavaFile javaFile = binding.brewJava(sdk); try { // com.squareup.javapoet.JavaFile属于squareup公司的javapoet开源项目 // 其作用是让程序员可以使用代码生成符合Java规范的Java类文件 // 生成代码 javaFile.writeTo(filer); } catch (IOException e) { …… } } return false; }private Map findAndParseTargets(RoundEnvironment env) { // 保存所有用于生成代码的信息 Map builderMap = new LinkedHashMap<>(); Set erasedTargetNames = new LinkedHashSet<>(); …… for (Element element : env.getElementsAnnotatedWith(BindView.class)) { try { // 根据节点信息构建生成代码的信息 parseBindView(element, builderMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindView.class, e); } } …… }private void parseBindView(Element element, Map builderMap,Set erasedTargetNames) {TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); TypeMirror elementType = element.asType(); // 校验节点信息 …… BindingSet.Builder builder = builderMap.get(enclosingElement); if (builder != null) { // 如果元素已经被加载则报错 } else { // 根据节点信息构建生成代码的信息 builder = getOrCreateBindingBuilder(builderMap, enclosingElement); } // 查询实例对象的名称、类名 String name = simpleName.toString(); TypeName type = TypeName.get(elementType); // 判断是添加一个属性还是一个方法 boolean required = isFieldRequired(element); // 将生成代码的信息保存在BindingSet对象中 builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required)); erasedTargetNames.add(enclosingElement); } }

继续追溯ButterKnifeProcessor.getOrCreateBindingBuilder()方法到BindingSet.newBuilder()中:
import com.squareup.javapoet.ClassName; static Builder newBuilder(TypeElement enclosingElement) { TypeMirror typeMirror = enclosingElement.asType(); boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE); boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE); boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE); TypeName targetType = TypeName.get(typeMirror); if (targetType instanceof ParameterizedTypeName) { targetType = ((ParameterizedTypeName) targetType).rawType; } // 从节点信息中获取包信息 String packageName = getPackage(enclosingElement).getQualifiedName().toString(); // 从节点信息中获取类名信息 String className = enclosingElement.getQualifiedName().toString().substring( packageName.length() + 1).replace('.', '$'); com.squareup.javapoet.ClassName // javapoet项目中的类名实例 // 在这里生成了我们最开始所说的MainActivity$$ViewBinder.java // 早起的版本使用ViewBinder结尾,8.5.1版本使用ViewBinding结尾 ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding"); boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL); return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog); }

总结 JDK1.6提供的插入式注解器功能允许我们可以在自定义的注解处理器中查询、修改语法树中的信息。ButterKnife利用这一API获取到自定义的注解信息,根据这些注解信息生成一个java类文件。这个java类文件包含着原来应该由程序员手写的绑定资源代码。在运行时通过反射调用机制创建这个java类文件的实例,通过这个实例来调用这些绑定资源的代码。

    推荐阅读