超级白话ButterKnife源码

ButterKnife简单使用

class ExampleActivity extends Activity {
  @BindView(R.id.title) TextView title;
  @BindView(R.id.subtitle) TextView subtitle;
  @BindView(R.id.footer) TextView footer;

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
    // TODO Use fields...
  }
}

首先使用注解标记View,然后再onCreate中setContentView之后调用bind方法,被注解标记的View就可以直接使用了。省去了findViewById的操作,但是是真的省去了吗?那就开始看源码吧~

源码分析

既然是注解标记,那就从注解开始看起。就看BindView这个注解。

注解解析源码

代码1

//Retention标记注解解析的时机,CLASS说明是编译时候生成.class文件,编译时注解。
//Target标记注解修饰的类型,FIELD说明该注解修饰字段
@Retention(CLASS) @Target(FIELD)
public @interface BindViews {
  /** View IDs to which the field will be bound. */
  //value 是 字段(也就是View)的Id
  @IdRes int[] value();
}

既然是编译时注解,那就应该看看编译时注解的使用。这就用到了强大的apt(annotation process tools)。 编译时注解,需要自己写一个类,继承AbstractProcessor这个类,这个类是存在在Java包里的,所以应该使用JavaLibrary开发。这个类有个非常重要的方法,getSupportedAnnotationTypes,通过这个方法的名字可以看出来,是获取这个processor能够支持的注解类型。还有个方法是process。这个方法就是处理被注解标记的element。 所以我们先看下ButterKnife里面实现的Processor类。代码有点长,慢慢看。

我这里用的是8.0.1,butterknife-compiler这个包并没有编译到本地,使用apt “com.jakewharton:butterknife-compiler:8.0.1”进行编译。可以下载源码看。

首先看支持的注解。

代码2

//把注解的名字保存在一个Set里面。看一下它支持的注解,当然我们的BindView也在里面(这是废话,当然必须在里面)
//这个方法最终就是返回了支持类型的Set。看代码3
@Override public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
      types.add(annotation.getCanonicalName());
    }
    return types;
  }

  private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

    annotations.add(BindArray.class);
    annotations.add(BindBitmap.class);
    annotations.add(BindBool.class);
    annotations.add(BindColor.class);
    annotations.add(BindDimen.class);
    annotations.add(BindDrawable.class);
    annotations.add(BindFloat.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
  }
  private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(//
      OnCheckedChanged.class, //
      OnClick.class, //
      OnEditorAction.class, //
      OnFocusChange.class, //
      OnItemClick.class, //
      OnItemLongClick.class, //
      OnItemSelected.class, //
      OnLongClick.class, //
      OnPageChange.class, //
      OnTextChanged.class, //
      OnTouch.class //
  );

代码3

//最关键的是这个方法,为什么这个方法代码这么少。
//其实,所有关键的动作,都是在这里完成的。里面的函数慢慢看。
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {

   //首先看下findAndParseTargets()这个方法。转代码4
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    //看完代码4,这里就知道了bindingMap里面保存了关于被注解标记的所有元素的信息

    //接下来就是开始解析这些保存的信息了。
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

    //生成的文件的信息就都在brewJava这个方法里面了。看下,转代码9
      JavaFile javaFile = binding.brewJava(sdk);
      try {
      //终于写了。这个在源码里追溯的话可以看到文件最开始其实是写了属于哪个包(package ...)还有导入了什么类(import ...)属于的包就是跟被bindView这些注解标记的元素的包。所以可以调用生成的class文件中的方法。也就是我们在声明View的时候,不能够声明成private的。那样就没有权限使用这些方法了。
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }
//这里我们就已经把class文件写完了。由于是编译期完成的(编译时注解),没有用到反射,在运行时性能没有影响。接下来就应该看看生成的文件了。
    return false;
  }

代码4

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    scanForRClasses(env);
    //这里省略了一些扫描注解的源码,都是跟下面这个类似
    // Process each @BindView element.
    //getElementsAnnotatedWith这个方法是获得所有被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 {
      //这个方法开始解析,看这个方法,转代码5
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
    //这里也省略了一些
    // Process each annotation that corresponds to a listener.
    for (Class<? extends Annotation> listener : LISTENERS) {
      findAndParseListener(env, listener, builderMap, erasedTargetNames);
    }
    
    //到这里,builderMap是把所有的被注解标注的信息存起来了。然后开始构建buildingMap。我们看一下这个Map的key-value就可以了。
    Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
        new ArrayDeque<>(builderMap.entrySet());
    Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
    while (!entries.isEmpty()) {
      Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();

      TypeElement type = entry.getKey();
      BindingSet.Builder builder = entry.getValue();

      TypeElement parentType = findParentType(type, erasedTargetNames);
      if (parentType == null) {
      //key就是被注解标注的element,value就是BindingSet,用builder模式构建的,我们刚刚在builderMap中只是存了他的Builder,这里挨个build()了。那BindingSet里面的字段大概有个了解了。
        bindingMap.put(type, builder.build());
      } else {
        BindingSet parentBinding = bindingMap.get(parentType);
        if (parentBinding != null) {
          builder.setParent(parentBinding);
          bindingMap.put(type, builder.build());
        } else {
          // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
          entries.addLast(entry);
        }
      }
    }
    return bindingMap;
  }
  //看完代码4回去看代码3了

代码5

//首先可以看到,传进来的参数,element是刚刚获得的被BindView注解的对象,bulderMap是findAndParseTargets一开始声明的builderMap,erasedTargetNames也是刚刚的方法声明
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
      //这个getEnclosingElement是返回封装此元素的最里层的子元素。
      //TypeElement 表示一个类或接口元素,继承了Element
      //因为我们用BindView注解标注的是一个View,一个类,所以我们知道这个是TypeElement,所以这里把它强转成了TypeElement
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

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

    // Verify that the target type extends from View.
    //校验目标对象的类型是继承自View类的。也就是判断使用BindView注解标记的元素是一个View。
    //asType()返回此元素定义的类型
    //TypeMirror表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。还可以表示通配符类型参数、executable 的签名和返回类型,以及对应于包和关键字 void 的伪类型。
    TypeMirror elementType = element.asType();
    //getKind()返回类型的种类,而TypeKind是一个枚举类型。里边包含上面描述的类型。而TYPEVAR是代表了类型变量
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      //如果是类型变量,那就可以强转成TypeVariable,TypeVariable继承了TypeMirror,所以可以强转。
      TypeVariable typeVariable = (TypeVariable) elementType;
      //getUpperBound()方法返回类型变量的上边界
      elementType = typeVariable.getUpperBound();
    }
    //getQualifiedName()返回此类型元素的完全限定名称,Name是继承了CharSequence的接口
    Name qualifiedName = enclosingElement.getQualifiedName();
    //getSimple返回该元素的简单名称
    Name simpleName = element.getSimpleName();
    //这里是判断,如果既不是一个View,也不是一个接口,那么就直接返回,hasError=true.
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
      if (elementType.getKind() == TypeKind.ERROR) {
      //这里如果判断是个ERROR类型,就只是打印LOG
        note(element, "@%s field with unresolved type (%s) "
                + "must elsewhere be generated as a View or interface. (%s.%s)",
            BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
      } else {
        error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
            BindView.class.getSimpleName(), qualifiedName, simpleName);
        hasError = true;
      }
    }

    if (hasError) {
      return;
    }

    // Assemble information on the field.
    //这里终于拿到了BindView标记的View的id
    int id = element.getAnnotation(BindView.class).value();

    //这里出现一个BindingSet类,还是build模式。我们先不看这个类,就根据方法名字看看能不能看懂在干嘛
    //首先从刚刚传进来的bulderMap里get一下,第一次肯定没有的吧,是null.
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    //这个方法还是看一下。QualifiedId构造函数接收两个参数,一个是包名,一个是id。为什么传了一个element进去。看看到底干了什么勾当。转代码6
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    //看完代码6,知道了qualifiedId里面存了element 的包名,还有id
    //首先判断builder是不是null,如果第一次,一定是null 啊,看来这里是缓存了下来,不然每次也是挺费劲的。那就看第一次的吧。
    if (builder != null) {
      String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
      if (existingBindingName != null) {
        error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
            BindView.class.getSimpleName(), id, existingBindingName,
            enclosingElement.getQualifiedName(), element.getSimpleName());
        return;
      }
    } else {
    //第一次用这个方法创建了builder。看看去,转代码7
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
      //看完代码7明白了,这里首先在getOrCreateBindingBuilder构建了一个builder,然后将enclosingElement做key,builder做value保存到了builderMap里,最后返回了刚刚构建的builder.
    }

    String name = simpleName.toString();
    //TypeName是javapoet包里的,这里就是获取了elementType类型的名字
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    //还是通过方法的名字看。
    //第一个参数是是获取了view的id。第二个参数是用element的name,还有TypeName构建了一个FieldViewBinding的对象。还是看看addField这个方法吧。转代码8
    builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

    // Add the type-erased version to the valid binding targets set.
    //这里把这个element添加到了erasedTargetNames。然后这个方法就结束了。
    erasedTargetNames.add(enclosingElement);
    //看完这个方法,我大概知道了builderMap中是有东西了。回过去看代码4
  }

代码6

//原来里面做了处理,elementUtils是个Elements,是用来对程序元素进行操作的使用工具方法。在init里面初始化过了。
//getPackageOf可以很好理解了,就是返回element元素的包。
//后面的方法也说过,就是获取全限定名称。
//所以这里就把传进来的element转换成了所在包的全限定名称。回到代码5
private QualifiedId elementToQualifiedId(Element element, int id) {
    return new QualifiedId(elementUtils.getPackageOf(element).getQualifiedName().toString(), id);
  }

代码7

private BindingSet.Builder getOrCreateBindingBuilder(
      Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
    //这里还是先看看有没有缓存,从方法名字也可以看出来,应该有这一步,毕竟是getOrCreate嘛
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder == null) {
    //这里把用注解标记的元素传进去,构建了一个builder,然后用元素本身做key,builder做value。保存到了builderMap里面。注意,这里是引用传递,也就是直接修改了传进来的参数。回到代码5
      builder = BindingSet.newBuilder(enclosingElement);
      builderMap.put(enclosingElement, builder);
    }
    return builder;
  }

代码8

//拿到id后是创建了一个ViewBinding.Builder对象,并且还以id作为key,用这个对象作为value,存到了viewIdMap中。接着又setFieldBinding,跟id一样,是ViewBinding.Builder的一个字段。先回到代码5
void addField(Id id, FieldViewBinding binding) {
      getOrCreateViewBindings(id).setFieldBinding(binding);
}
    
private ViewBinding.Builder getOrCreateViewBindings(Id id) {
      ViewBinding.Builder viewId = viewIdMap.get(id);
      if (viewId == null) {
        viewId = new ViewBinding.Builder(id);
        viewIdMap.put(id, viewId);
      }
      return viewId;
}

代码9

//又是Builder模式,那就看这个Builder了。里面有个createType方法,转代码10。
JavaFile brewJava(int sdk) {
    return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
        //看到代码12.我们没有仔细看,但是类文件中代码的内容应该是构造好了。就等着写了。回到代码3
  }

代码10

//哇塞,终于知道你。就是在这里,创建了文件的内容,随便找一个看看。就看看createBindingConstructor,转代码11
private TypeSpec createType(int sdk) {
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    if (isFinal) {
      result.addModifiers(FINAL);
    }

    if (parentBinding != null) {
      result.superclass(parentBinding.bindingClassName);
    } else {
      result.addSuperinterface(UNBINDER);
    }

    if (hasTargetField()) {
      result.addField(targetTypeName, "target", PRIVATE);
    }

    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
      result.addMethod(createBindingConstructorForDialog());
    }
    if (!constructorNeedsView()) {
      // Add a delegating constructor with a target type + view signature for reflective use.
      result.addMethod(createBindingViewDelegateConstructor());
    }
    result.addMethod(createBindingConstructor(sdk));

    if (hasViewBindings() || parentBinding == null) {
      result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
  }

代码11

private MethodSpec createBindingConstructor(int sdk) {
    //前面省略了很多
    if (hasViewBindings()) {
      //前面省略了很多
      //就看addViewBinding这个方法,转代码12
      for (ViewBinding binding : viewBindings) {
        addViewBinding(constructor, binding);
      }
      //后面省略了很多
    }
    //后面省略了很多
}

代码12

//大体看一眼,看到了findViewById这个方法,好熟悉啊。总之这个就是在生成类文件中的代码。其实就是用字符串拼起来的。哦,这样子。还是回到代码9吧
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
    if (binding.isSingleFieldBinding()) {
      // Optimize the common case where there's a single binding directly to a field.
      FieldViewBinding fieldBinding = binding.getFieldBinding();
      CodeBlock.Builder builder = CodeBlock.builder()
          .add("target.$L = ", fieldBinding.getName());

      boolean requiresCast = requiresCast(fieldBinding.getType());
      if (!requiresCast && !fieldBinding.isRequired()) {
        builder.add("source.findViewById($L)", binding.getId().code);
      } else {
        builder.add("$T.find", UTILS);
        builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
        if (requiresCast) {
          builder.add("AsType");
        }
        builder.add("(source, $L", binding.getId().code);
        if (fieldBinding.isRequired() || requiresCast) {
          builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
        }
        if (requiresCast) {
          builder.add(", $T.class", fieldBinding.getRawType());
        }
        builder.add(")");
      }
      result.addStatement("$L", builder.build());
      return;
    }
//省略了一些
  }

生成的文件与ButterKnife源码

首先看一下生成的文件格式

生成的文件

//生成的class文件实现了ViewBinder接口。可以看到ViewBinder接口里面有个bind方法。并且第一个参数是Finder,着我们就应该看看Finder是个什么东西了。转代码13
public class NewsFragment$$ViewBinder<T extends NewsFragment> implements ViewBinder<T> {
  @Override
  public Unbinder bind(final Finder finder, final T target, Object source){
        View view;
        //这里就开始找view ,转类型
        view = finder.findRequiredView(source, 2131624662, "field 'myView'");
        target.svNews = finder.castView(view, 2131624662, "field 'myView'");
  }
 }

代码13

//这里省略了很多代码
//可以看到,Finder是一个枚举。里面有三种枚举类型。VIEW,ACTIVITY,DIALOG。并且都有了findView,getContext等方法。我们看一下刚刚在生成文件中调用的findRequiredView这个方法。
public enum Finder {
  VIEW {
    @Override protected View findView(Object source, int id) {
      return ((View) source).findViewById(id);
    }

    @Override public Context getContext(Object source) {
      return ((View) source).getContext();
    }

    @Override protected String getResourceEntryName(Object source, int id) {
      final View view = (View) source;
      // In edit mode, getResourceEntryName() is unsupported due to use of BridgeResources
      if (view.isInEditMode()) {
        return "<unavailable while editing>";
      }
      return super.getResourceEntryName(source, id);
    }
  },
  ACTIVITY {
    @Override protected View findView(Object source, int id) {
    //在这里才调用了findViewById,不过总算找到了。最后返回这个View。还没有强转。回去看findOptionalView
      return ((Activity) source).findViewById(id);
    }

    @Override public Context getContext(Object source) {
      return (Activity) source;
    }
  },
  DIALOG {
    @Override protected View findView(Object source, int id) {
      return ((Dialog) source).findViewById(id);
    }

    @Override public Context getContext(Object source) {
      return ((Dialog) source).getContext();
    }
  };

//又调了findOptionalView
  public <T> T findRequiredView(Object source, int id, String who) {
    T view = findOptionalView(source, id, who);
    return view;
  }
//这里能区分到底是Activity还是view了。假设source是Activity,那它调用的应该是刚刚ACTIVITY枚举类型的findView,去看看。
  public <T> T findOptionalView(Object source, int id, String who) {
    View view = findView(source, id);
    return castView(view, id, who);
  }

//到这里才强转成功
  @SuppressWarnings("unchecked") // That's the point.
  public <T> T castView(View view, int id, String who) {
    try {
      return (T) view;
    } catch (ClassCastException e) {
      
    }
  }


  protected String getResourceEntryName(Object source, int id) {
    return getContext(source).getResources().getResourceEntryName(id);
  }

  protected abstract View findView(Object source, int id);

  public abstract Context getContext(Object source);
}

看到这里,我们知道了生成文件是怎么工作的,但是我们怎么调用到生成文件中的方法呢。这时候我们回到我们最开始看到的那个ButterKnife.bind方法。 ButterKnife.bind(this); 我们进去看看这个方法。

代码14

//哇,我们看到Finder的类型了。
//其实bind方法有很多重载,这只是其中一个,最终都会调用到bind(target, target, Finder);这个。所以我们看这个
public static Unbinder bind(@NonNull Activity target) {
    return bind(target, target, Finder.ACTIVITY);
  }
 static Unbinder bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder) {
    Class<?> targetClass = target.getClass();
    try {
    //哦?我们好像找到了。先看下findViewBinderForClass
    //看完代码15就很明白了。就是根据传进来的这个Activity或者其他对象的名字来构建了一个对应的ViewBinder实例,也就是我们根据不同的Activity或者Fragment或者其他声明注解的那些类手动写的类的实例。然后再调用他们各自的bind方法。这样target,source,Finder都有了,刚刚的也就都打通了。小总结一下,转小总结
      ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
      return viewBinder.bind(finder, target, source);
    } catch (Exception e) {
      throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
    }
  }

代码15

//从这个方法名字以及参数来看,大概是通过传进来的类,确定ViewBinder的一个实例。
private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
      throws IllegalAccessException, InstantiationException {
      //仍然是做了一个缓存
    ViewBinder<Object> viewBinder = BINDERS.get(cls);
    if (viewBinder != null) {
      if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
      return viewBinder;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return NOP_VIEW_BINDER;
    }
    try {
    //刚刚看到了,生成的文件的名字是(Activity或者Fragment或者其他注解所在类的名字 + $$ +ViewBinder)。所以将传进来的Activity的名字加上这个构造了一个类。这里使用反射生成的。对象生成之后就存到了Map里面,防止每次都用反射。
      Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
      //noinspection unchecked
      viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
      if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
    } catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      viewBinder = findViewBinderForClass(cls.getSuperclass());
    }
    BINDERS.put(cls, viewBinder);
    return viewBinder;
  }

小总结

ButterKnife的核心就是编译时注解,注解解析器(apt)会解析这些注解,然后手动生成对应的class文件,每个类都会实现ViewBinder这个接口,重写bind这个方法。 findViewById是在枚举类Finder中完成的,里面包含了三个枚举对象,分别是ACTIVITY,VIEW,DIALOG。 当我们在Activity中调用ButterKnife.bind方法的时候,首先通过反射生成对应类的实例。然后会调用对应类的bind方法。在这个方法中传入了Finder,target,source等参数。而根据传入的不同的枚举值,我们就知道调用哪个findViewById。也就不需要自己手动写了。 注意,生成的文件跟原来的文件在一个package下面,所以,被注解标记的View或者方法,都不能是private或者protected修饰的,因为那样生成类中的方法就访问不到了。