使用 butterknife 很简单。
在 Activity 中只需在setContentView() 之后配置一句 ButterKnife.bind(this),就可以省略繁琐的 findViewById 代码。可以推测它肯定是在内部帮我们处理然后生成了 findViewById 代码。它是如何生成的呢?
使用反射是可以达到这个目的的,但是一定存在性能问题。Butterknife 使用的是 APT(Annotation Processing Tool) 编译时解析技术,动态生成findViewById 代码。很多著名的开源库都使用了 APT,比如 Dagger2,DeepLinkDispatch。

APT(Annotation Processing Tool)


首先需要了解一下 Annotation Processing。

Annotation Processing 发布于 java 1.5,是一个年代久远但是十分牛逼的 API。


APT 的作用是根据注解帮助我们生成一些模板代码,减少我们的重复工作。代码生成的时机发生在 编译时,javac 会 build 所有的注解,并且在编译时扫描且处理它们。 我们使用 Annotation Processor 用来定制我们处理注解生成代码的规则。 具体使用规则可以看文档 中的 AbstractProcessor类。

可以用 6 步来概括这个过程:

  1. java compiler 开始 build
  2. Annotation Processors 开始工作
  3. 轮询内部所有的注解,找到注解的类、方法、变量
  4. 利用刚刚解析的数据生成一个新类(这里就会生成新的代码)
  5. 创建一个文件,将生成的代码写入
  6. 编译检查是否所有的 annotation processors 都执行了,如果还有没有执行的,就继续循环。



ButterKnife 的工作过程

不难推测完成 findViewById 的过程肯定是由ButterKnife.bind(this) 这句代码完成的。
我们着手看看它的内部做了什么。

@NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) {
    // 得到当前 Activity 的顶级 View,即 DecorView
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  }
复制代码
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    // 根据 class 创建构造器
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
      return Unbinder.EMPTY;
    }

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    
    
    return constructor.newInstance(target, source);
    // ...
     
  }
复制代码
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder>
    // 先从 map 表里找,有的话直接返回
    // 这里将构造器缓存起来了
    bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) {
      return bindingCtor;
    }
   
   
    String clsName = cls.getName();
    // 过滤以 android. 和 java. 开头的类
    // 这些是 framework 的类,不处理
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    
    
    try {
    // 获取APT生成的类 clsName
      Class<?> bindingClass = Class.forName(clsName + "_ViewBinding");
      //noinspection unchecked
      // 然后得到这个新类的构造器对象
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
      if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
    } 
    // .. 省略异常
    
    // 将新生成的构造器存入 map 
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }
复制代码

到这里我们知道ButterKnife.bind(this) 做的事就是找到 apt 生成的那个类的构造器然后通过构造器创建那个类的实例。其中用 LinkedHashMap 缓存了这些构造器,提高性能。

ButterKnife 生成的代码位于:app/build/generated/source/apt 路径下。

我们来看看这个新生成的类:

它实现了 Unbinder 接口, 类的命名以原类名加后缀 “_ViewBinding”。

public interface Unbinder {
  @UiThread void unbind();

  Unbinder EMPTY = new Unbinder() {
    @Override public void unbind() { }
  };
}

复制代码
public class LoginActivity_ViewBinding implements Unbinder {
  private LoginActivity target;

  @UiThread
  public LoginActivity_ViewBinding(LoginActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public LoginActivity_ViewBinding(LoginActivity target, View source) {
    // 获得原来的 Activity 对象
    this.target = target;

    // 这里 findViewById 就由 Utils.findRequiredViewAsType这个方法封装了,直接返回类型转换后的对象。
    // 这里为了能访问到 target 中的变量,mContainer 是不能为 private 的,否则会报错访问不到。
    target.mContainer = Utils.findRequiredViewAsType(source, R.id.login_container, "field 'mContainer'", FrameLayout.class);
}

  @Override
  @CallSuper
  public void unbind() {
    LoginActivity target = this.target;
    
    // 解绑时注意 target 不能为空
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;
    // 将所有变量置为空
    target.mContainer = null;
  }
}
复制代码

它做的事非常简单,构造器中传入了原来的 Activity 对象和当前 Activity 的顶级 View,使用封装后的方法完成了 findViewById 的代码。如此一来整个过程就明白了。

findRequiredViewAsType() 方法做的事就是平常我们天天需要手写的 findViewById,这里将其做了封装处理。

public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
      Class<T> cls) {
    View view = findRequiredView(source, id, who);
    return castView(view, id, who, cls);
  }
复制代码

一图胜千言: