ButterKnife View 注解框架原理解析

ButterKnife View 注解框架原理解析

这里以 ButterKnife 的 10.2.0 版本为例。

10.2.0 版本的 ButterKnife 采用的是运行时注解。@BindView 注解代码如下:

@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

@Retention(RUNTIME) 说明是运行时注解。

@Target(FIELD) 说明是针对成员变量的注解。

使用 @BindView 注解 TextView 的代码如下:

public class ButterKnifeActivity extends Activity {

    @BindView(R.id.butter_text)
    TextView textView;
...
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_butter_knife);
        ButterKnife.bind(this);
...
    }
}

ButterKnifeProcessor 源码分析

ButterKnifeProcessor 是 ButterKnife 的注解处理器,它位于 butterknife-compiler 目录。

ButterKnifeProcessor 继承 AbstractProcessor ,它的主要处理逻辑都在 process 方法中。ButterKnifeProcessor 代码如下:

@AutoService(Processor.class)
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.DYNAMIC)
@SuppressWarnings("NullAway") // TODO fix all these...
public final class ButterKnifeProcessor extends AbstractProcessor {
  ...
  @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

      JavaFile javaFile = binding.brewJava(sdk, debuggable);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }

process 方法调用 findAndParseTargets(env) 方法,生成了 TypeElement 和 BindingSet 的 Map,即某个元素对应的 BindingSet。

  private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
...
    // Process each @BindView element.
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      // we don't SuperficialValidation.validateElement(element)
      // so that an unresolved View type can be generated by later processing rounds
      try {
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
...
    return bindingMap;
  }

findAndParseTargets 方法会查找所有 ButterKnife 的注解来进行解析,这里只看 @BindView 注解。可以看出使用 getElementsAnnotatedWith(BindView.class) 方法找到所有使用 @BindView 注解的元素,然后对每个元素调用 parseBindView 方法。

parseBindView 方法如下:

  private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Start by verifying common generated code restrictions.
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);
...

    if (hasError) {
      return;
    }

    // Assemble information on the field.
    int id = element.getAnnotation(BindView.class).value();
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    Id resourceId = elementToId(element, BindView.class, id);
...

    String name = simpleName.toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    builder.addField(resourceId, new FieldViewBinding(name, type, required));

    // Add the type-erased version to the valid binding targets set.
    erasedTargetNames.add(enclosingElement);
  }

parseBindView 首先检查了生成代码的限制。

  1. isInaccessibleViaGeneratedCode 判断代码的是否可访问到
  2. isBindingInWrongPackage 判断代码是否在错误的包名内

isInaccessibleViaGeneratedCode 方法做了 3 个判断:

判断注解的成员变量的修饰符是否是 private 或者 static:

    if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC))

判断注解的成员变量所在的类是否是 class 类型:

    if (enclosingElement.getKind() != CLASS)

判断注解的成员变量所在的类的修饰符是否是 private:

    if (enclosingElement.getModifiers().contains(PRIVATE))

isBindingInWrongPackage 方法判断代码是否在错误的包名内

如果注解的成员变量所在类是 ButterKnifeActivity,那么 ButterKnifeActivity 不能以 android. 开头的包名定义:

    if (qualifiedName.startsWith("android.")) {

如果注解的成员变量所在类是 ButterKnifeActivity,那么 ButterKnifeActivity 不能以 java. 开头的包名定义:

    if (qualifiedName.startsWith("java.")) {

最后通过获取 @BindView 对应的 id 值,生成 FieldViewBinding,放入 BindingSet。

    int id = element.getAnnotation(BindView.class).value();
    ...
    builder.addField(resourceId, new FieldViewBinding(name, type, required))

回到 ButterKnifeProcessor 的 process 方法。

      BindingSet binding = entry.getValue();

      JavaFile javaFile = binding.brewJava(sdk, debuggable);
      try {
        javaFile.writeTo(filer);
      }
      ...

可以看出会将所有的注解绑定信息生成一个 JavaFile,然后输出 Java 辅助文件。

重新构建项目,可以在 app\build\generated\source\apt 目录下找到生成的 ViewBinding 文件,比如 ButterKnifeActivity_ViewBinding。这个类会在 ButterKnife bind activity 的时候被反射调用。

ButterKnife 的 bind 方法分析

为了使用 ButterKnife,需要使用 ButterKnife 的 bind 方法绑定上下文。bind 方法如下:

  public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return bind(target, sourceView);
  }

可以看出调用了 bind 的 2 参数方法:

  public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

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

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);
    }
    ...
  }

可以看出 bind 方法调用了 findBindingConstructorForClass 获取 target 的构造器,然后使用 newInstance 方法产生一个实例对象。

findBindingConstructorForClass 方法如下:

  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
      if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
    }
    ...
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

findBindingConstructorForClass 首先从 BINDINGS 中读取缓存的 Constructor,如果存在缓存就直接返回。然后判断了参数类的名字是否以 android、java、androidx 开头,如果以这些标准的包名开头就返回 null,最后通过 classLoader 加载带 “_ViewBinding” 后缀的 ViewBinding 类,这个 ViewBinding 类是由 ButterKnife-compiler 注解处理器生成的,位于 app\build\generated\source\apt 目录。最后将反射得到的构造器存入 BINDINGS 缓存,用来方便下次查找。

ButterKnife 生成的辅助类分析

这里以 ButterKnifeActivity 的辅助类 ButterKnifeActivity_ViewBinding 为例。

上一节说过 ButterKnife 的 bind 方法会调用 ViewBinding 辅助类的 2 参数构造方法生成实例。

constructor.newInstance(target, source);

所以会调用 ButterKnifeActivity_ViewBinding 的 2 参数构造方法如下:

  public ButterKnifeActivity_ViewBinding(final ButterKnifeActivity target, View source) {
    this.target = target;

    View view;
    view = Utils.findRequiredView(source, R.id.butter_button1, "field 'button1' and method 'showToast'");
    target.button1 = Utils.castView(view, R.id.butter_button1, "field 'button1'", Button.class);
    view7f070021 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.showToast();
      }
    });
    ...
  }

这里以 button1 为例分析 View 查找和点击事件的实现。首先把第一个参数 ButterKnifeActivity 传递给成员变量 target,然后调用 findRequiredView 查找 id 是 R.id.butter_button1 的 view。

findRequiredView 方法如下:

  public static View findRequiredView(View source, @IdRes int id, String who) {
    View view = source.findViewById(id);
    if (view != null) {
      return view;
    }
    ...
  }

可以看出 findRequiredView 调用了 source 的 findViewById 方法,这里的 source 就是 activity 的 decorView,它是所有 view 的祖先节点,所以可以找到布局里面定义的 R.id.butter_button1。

找到对应 id 的 View 之后,需要将它转换为初始定义的 Button 类型。所以调用了 castView 方法。

castView 方法如下:

  public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
      return cls.cast(view);
    }
    ...

cast 方法就是把 View 强制转换为指定类型,比如 Button。

辅助类也提供了 unbind 解绑操作,用来将目标 activity 和辅助类生成的 View 对象解绑。

unbind 方法如下:

  public void unbind() {
    ButterKnifeActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;

    target.button1 = null;
...
  }

所以用 @BindView(R.id.butter_button1) 注解的 Button 实际上是 ButterKnifeActivity findViewById(R.id.butter_button1) 得到的,但是 ButterKnife 通过注解的方式生成 ViewBinding 辅助类,帮助我们省去了大量的样板代码,提高开发效率。

总结

  1. ButterKnife 通过注解处理器 ButterKnifeProcessor 和 Activity 的成员变量和方法的注解信息生成了 ViewBinding 辅助类,在辅助类的构造方法里面做了 findViewById 等资源查找操作。
  2. ButterKnife 的 bind 方法会调用 ViewBinding 辅助类的构造方法,将 target activity 注入到辅助类,并给 target activity 的成员变量赋值或者设置点击事件,帮助我们省去了大量的样板代码。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值