拆 Jake Wharton 系列之 ButterKnife

Jake Wharton 是 Android 大神,同时也是开源狂魔。他开源的项目特点是小而美,且应用广泛,比如 butterknifeRxBindinghugo 等,本文从受众最广泛,star 最多的 ButterKnife 讲起。

(一) 你将获得什么

通过阅读 ButterKnife 源码和本文,你将收获:

  • android-apt 三件套:
  1. 注解处理器(AbstractProcess)
  2. 注解处理器注册(AutoService)
  3. 代码生成(JavaPoet)
  • 自定义 gradle 插件
  • 造一个优秀轮子应该具备的态度

(二)ButterKnife 简介

ButterKnife 使用注解的方式来替代繁琐的 findViewById 和注册监听器时大量的匿名内部类写法。

本文针对 8.5.1 版本的源码进行分析,自从 8.2.0 起已经支持 library 工程。github 地址为:https://github.com/JakeWharton/butterknife/releases/tag/8.5.1

(三)ButterKnife 总览

阅读源码切忌只见树木不见森林,因此先从大局上分析下这个项目。

组件依赖关系

ButterKnife 共7个组件,他们的依赖关系如下图所示(其中,butterknife-integration-test 工程不做介绍):

butterknife组件依赖
  • 0.sample:代表使用 ButterKnife 的业务项目,根据上图所示需要依赖与3个组件,因此我们在使用 ButterKnife 时需要做如下配置:
dependencies {
   compile 'com.jakewharton:butterknife:8.5.1'
   annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
}

如果项目是 library ,还将引入第三个依赖

dependencies {
   classpath 'com.jakewharton:butterknife-gradle-plugin:8.5.1'
 }

为什么需要这三个依赖,他们的作用分别是什么,下文将一一介绍。

  • 1.butterknife:这个工程提供了 ButterKnife.bind(this),这是 ButterKnife 对外提供的门面。也是运行时,触发 ActivityView 控件绑定的时机。
  • 2.butterknife-compiler:见名知意,编译期间将使用该工程,他的作用是解析注解,并且生成 ActivityView 绑定的 Java 文件。
  • 3.butterknife-annotations:将所有自定义的注解放在此工程下, 确保职责的单一。
  • 4.butterknife-gradle-plugin:gradle 插件,这是8.2.0版本起为了支持 library 工程而新增的一个插件工程,原理将在下文中详细介绍。
  • 5.butterknife-lint:针对 butterknife-gradle-plugin 而做的静态代码检查工具,非常有态度的一种做法,在下文做详细介绍。

整体流程

butterknife-流程图

将整个流程拆分成编译期间和运行期间,就不难理解 ButterKnife 的运行机制。伴随而来的几个问题:

  1. 编译期间如何处理注解的信息,并解析生成 Java 文件?
  2. 运行期间如何绑定 Activity 中 的View 控件?
  3. 由 R 生成 R2 的意义是什么?

(四)android-apt(Annotation Processing Tool)

首先来解决第一个问题,编译期间注解处理,通过这两个关键词,我们可以联想到的技术方案是: APT(Annotation Processing Tool),即注解处理工具。在该方案中,通常有个必备的三件套,分别是注解处理器 Processor,注册注解处理器 AutoService 和代码生成工具 JavaPoet。

三件套之注解处理器

ButterKnife 一切皆注解,因此首先需要个处理器来解析注解。 ButterKnifeProcessor 充当了该角色,其中 process 方法是触发注解解析的入口,所有的神奇的事情从这里发生。

process 方法中主要做两件事情,分别是:

  1. 解析所有包含了 ButterKnife 注解的类
  2. 根据解析结果,使用 JavaPoet 生成相应的Java文件
process源码

findAndParseTargets(env) 中解析注解的代码非常冗长,依次对 @BindArray@BindColor@BindString@BindView 等注解进行解析,解析结果存放在 bindingMap 中。

这里重点关注下 bindingMap 的键值对。key 值为 TypeElement 对象 ,可以简单的理解为被解析的类本身,而 value 值为 BindingSet 对象,该对象存放了解析结果,根据该结果,JavaPoet 将生成不同的 Java 文件,以官方 sample 为例,其映射关系如下:

key value JavaPoet 根据 value 生成的文件
SimpleActivity BindingSet SimpleActivity_ViewBinding.java
SimpleAdapter BindingSet SimpleAdapter$ViewHolder_ViewBinding.java
生成的 java 文件

Processor 是为三件套之一。

小插曲之 UT

在介绍余下二件套之前,先插播个小插曲,关于单元测试。

在阅读源码过程中,debug 断点工具往往可以帮助我们事半功倍,运行时的 debug 比较好处理,但是类似于 ButterKnife 这种需要在编译期间处理逻辑的代码应该如何进行 debug ?

单元测试可以把代码独立成一个单元,并且可以隔离对上下文、对环境的依赖(比如 Robolectric 对 Android 的 mock)。一个优秀的有态度的开源框架,往往都配备了齐全的单元测试,ButterKnife 也不例外。

butterknife 子组件中配备了大量的单元测试,这些单元测试是为 ButterKnifeProcessor 量身打造的。比如 ExtendActivityTest 中的 views() 对 Activity 包含@BindView 的注解时的处理做了单元测试,运行 UT 后,可以随意断点,如下图:

对ButterKnifeProcessor断点调试

建议读者用这种方式来理解 butterknife-compiler 中的源码。

三件套之注册注解处理器

定义完注解处理器后,还需要告诉编译器该注解处理器的信息,需在 src/main/resource/META-INF/service 目录下增加 javax.annotation.processing.Processor 文件,并将注解处理器的类名配置在该文件中。

整个过程比较繁琐,Google 为我们提供了更便利的工具,叫 AutoService,此时只需要为注解处理器增加 @AutoService 注解就可以了,如下:

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {

}

AutoService 是为 android-apt 三件套之二。

三件套之 Java 诗人

最后介绍下三件套中最诗情画意的一个工具—— JavaPoet。她提供了笔墨纸砚,让我们像写诗一样写一个 Java 类。

了解 JavaPoet ,最好的方式便是看官方文档。简而言之,当我们写一个类时,其实是有固定结构的,JavaPoet 提供了生成这些结构的 api,举例如下:

  • 类:TypeSpec.classBuilder()
  • 构造器:MethodSpec.constructorBuilder()
  • 方法:MethodSpec.methodBuilder()
  • 参数:ParameterSpec.builder()
  • 属性:FieldSpec.builder()
  • 程序片段:CodeBlock.builder()

JavaPoet 提供了很多 Builder,这便是我们手中的笔墨纸砚。

有了浪漫的 Java 诗人之后,可以做很多充满想象力的事情。以 ButterKnife 而言,他做的事情便是将注解处理器解析后的结果(实际上就是上文提到的 BindingSet 对象)生成 Activity_ViewBinding.java,该对象负责绑定 Activity 中的 View 控件以及设置监听器等。

举例如下,假设有如下 ActivIty,

package com.geniusmart;
// 省略 import 语句

public class TestActivity extends Activity {
 @BindView(1) View one; // 1 实际上是Android resource对应的id
}

经过 JavaPoet 处理后,将生成如下文件:

package butterknife.compiler;
// 省略 import 语句

public class TestActivity_ViewBinding implements Unbinder {
  private TestActivity target;

  @UiThread
  public TestActivity_ViewBinding(TestActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public TestActivity_ViewBinding(TestActivity target, View source) {
    this.target = target;
    target.one = Utils.findRequiredView(source, 1, "field 'one'");
  }

  @Override
  @CallSuper
  public void unbind() {
    TestActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;
    target.one = null;
  }
}

那么 JavaPoet 是如何处理的?实际上 ButterKnife 会将上文提到的 BindingSet 转换成类似于下文所示的代码:

// 创建类
TypeSpec typeSpec = TypeSpec.classBuilder("TestActivity_ViewBinding")
        .addModifiers(PUBLIC) // 类为public
        .addSuperinterface(UNBINDER) // 类为Unbinder的实现类
        .addField(targetField) // 生成属性 private TestActivity target
        .addMethod(constructorForActivity) // 生成构造器1
        .addMethod(otherConstructor) // 生成构造器2
        .addMethod(unBindeMethod) // 生成unbind()方法
        .build();

// 生成 Java 文件
JavaFile javaFile = JavaFile.builder("com.geniusmart", typeSpec)//包名和类
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();

javaFile.writeTo(System.out);

如需完整代码,请点击 PoetAboutButterKnife.java ,这是个单元测试,可直接运行,运行后可以在控制台看到生成的 Java 类。

最后总结下这三件套的协作流程,如下图:

(五)运行期间

接下来我们来分析下运行期间发生的事情,相比于编译期间,运行期间的逻辑简单了许多。

public class SimpleActivity extends Activity {
 
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
  }
}

运行时的入口在于 ButterKnife.bind(this),追溯源码发现,最终将会执行以下逻辑:

// 最终将找到 SimpleActivity_ViewBinding 的构造器,并实例化
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
constructor.newInstance(target, source);

也就是说 ButterKnife.bind(this) 等价于如下代码:

View sourceView = activity.getWindow().getDecorView();
new SimpleActivity_ViewBinding(activity,sourceView);

SimpleActivity_ViewBinding 持有Activity对象,并且在其构造器中,将会触发Activity 中 view 控件的绑定。

注:虽然这里使用了反射,但源码中将 Class.forName 的结果缓存起来后再通过 newInstance 创建实例,避免重复加载类,提升性能。

编译期间和运行期间相辅相成,这便是 android-apt 的普遍套路。

(六)支持 library

编译时和运行时的问题解决了,还有最后一个问题:由 R 生成 R2 的意义是什么?

如果你细心的话会发现在官方的 sample-library 中,注解的值均是由 R2 来引用的,如下图:


R2 的使用

如果非 library 工程,则仍然引用系统生成的 R 文件。所以可以猜测:R2 的诞生是为 library 工程量身打造的。

其实 ButterKinife 在 8.2.0 版本之前,并不支持 library 工程的使用。在 Android 组件化、模块化需求这么迫切的今天,如果不支持 library 工程实在可惜。JakeWharton 在2016年07月10日解决了此问题。

首先分析下为什么 library 工程不直接引用 R?当我们把 R2 改成 R 之后,编译器将会报错:Attribute value must be constant ,如下图:

编译器报错

也就是说 BindView 注解的属性必须是常量。但是在 library 工程中 R.id.title 的值为变量,如下图(注:并没有 final 修饰符):

R中的属性为变量

如何解决此问题?既然 R 不能满足要求,那就自己构建一个 R2,由 R 复制而来,并且将其属性都修改为 public static final 来修饰的常量。为了让使用者对整个过程无感知,因此使用 gradle 插件来解决这个需求,这也是 butterknife-gradle-plugin 工程的由来。

butterknife-gradle-plugin 有两个重要的第三方依赖,分别是 javaparserjavapoet ,前者用于解析 Java 文件,也就是解析 R 文件,后者在前文中已经浓彩重墨,用于将解析结果生成 R2 文件。

整个插件工程的源码并不难理解,在生成 R2 文件时,要将属性定义成 public static final ,在源码中我们可以看到此逻辑,在 FinalRClassBuilder.addResourceField() 中 :

FieldSpec.Builder fieldSpecBuilder = FieldSpec.builder(int.class, fieldName)
        .addModifiers(PUBLIC, STATIC, FINAL)
        .initializer(fieldValue);

butterknife 插件在 processResources 的 Task 中执行,该任务通常用来完成文件的 copy。有关插件的知识笔者将在接下来的另外一篇关于 hugo 的源码解析中介绍。

(七)有态度的 Lint 检查

生成了 R2 文件后,会产生一个问题:该文件仅是为注解而用的,对开发者并没有任何约束力,怎么防止开发者误用?如:

int id = R2.id.footer;

如果写代码是应付工作,如果工作是绩效驱动,这类问题完全不需要考虑。但是,作为优秀的、有态度的、有情怀的开源框架,JakeWharton 和 ButterKnife 给了我们榜样,为了解决这个问题,butterknife-lint 工程应运而生。

从工程名来看,不难理解这工程的意义:一个静态代码检查工具,用来验证非法的 R2 引用。一旦在我们的业务项目里不小心引用了 R2 文件,当执行 Lint 后,将会有如下图的提示信息:

Lint检查非法的R2调用

追求完美的 JakeWharton ,有态度的 ButterKnife !

(八)总结

轮子天天有,但是好轮子并不常见。轮子的创意、价值、技术选型、单元测试以及追求完美的态度是衡量一个优秀轮子的维度。ButterKnife 完美地诠释了这一切。

参考文章

http://blog.stablekernel.com/the-10-step-guide-to-annotation-processing-in-android-studio
https://github.com/google/auto/tree/master/service
https://github.com/square/javapoet

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容