ButterKnife,你真的了解吗?

源自JakeWharton的经典制作ButterKnife, 相信大多数人都用的很熟练, 它一定程度上简化了我们的findViewById以及OnClick等操作,给平时开发带来了很大的便利。此处对是否应该使用它,我不作评论,也不是本篇的目的。

这个使用了多年的框架, 你真的知道是什么原理吗?它的实现是怎样的,你有产生过好奇心吗?那么就让我们来解读一下。


解读ButterKnife之准备篇

反射

这是一个多数框架都会使用的技术,只要知道类或对象,我们可以解析出关于此类的一切信息,为所欲为。一提反射,很多人会觉得性能消耗不会很大吗?肯定你也带着同样的想法吧?那么我来声明一下,反射存在性能消耗是有的,但是没有想象中的夸张;如果结合缓存使用的话,可以极大的抵消带来的性能的消耗,从而更好的享受它所带来的便利。作为一个Java开发者,不会反射真的有些过分了..

AnnotationProcessor

大家都知道,ButterKnifed 便利来自于注解。那么既然存在注解,注解处理器技术的使用是必然的。现在的框架不同于以往;以前的框架类似XUtils的注解很大程度上是使用反射来解析的,上面说过,反射带来性能消耗还是有的;但是现在, 大多数的注解框架都是基于Apt或AnnotationProcessor的编译时解析实现的。试想一下,在程序编译时就完成了注解解析的工作,又会给性能带来什么影响呢?答案当然是没影响。

早期的注解处理器大多使用开源的Apt, 然而一是Apt已不再被作者所维护;二则Google推出了AnnotationProcessor来替代它,更是集成到了API中,所以怎么看,使用AnnotationProcessor都是一个很好的选择。

Javapoet

这是一个很神奇的技术,借助它,我们可以便捷的生成我们想要的代码。至少目前来看,GreenDao生成的Bean与Dao, ButterKnife生成的Binding以及ARouter生成的Router&&Group&&xx等,都是使用了Poet技术。

以上3项技术被大多数框架所使用,如果你还不会,你已经Out了。那么读完这篇文章,让你脱离Out的领域。


ButterKnife的使用

这里不着重说怎么用ButterKnife, 或者说作为一个如此简单的框架,压根就不用多说。这里引入它的使用,为ButterKnife的解析作为一个引导。

// 初始化
ButterKnife.bind(Object target, View source);

// 加入注解
@BindView(id)
Button 

以上就是ButterKnife的基本调用。初始化,然后添加各种注解,如BindView, OnClick, BindBitmap。 如果你看过它的源码,你就会知道,ButterKnife支持的注解主要分为3类: BindView之绑定View, BindBitmap/ BindColor等绑定资源, OnClick等绑定事件。至于为什么不添加绑定setContentView, 大神说只能替换一行代码而已,没意思。大神就是这么任性...


ButterKnife的实现思想

从注解, 从我列举的准备技术,我想聪明的你已经猜到了ButterKnife是如何实现了吧。是的,它所使用的核心技术可以称之为依赖注入。俗话说就是,我们不在Activity中初始化这些View, 这堆资源, 这系列的点击事件;我们通过生成自定义类来偷偷的初始化,偷偷的绑定监听。对,偷偷的...

1)添加注解

这个没什么多说的, ButterKnife的核心即是通过注解来简化操作。

2)解析注解,生成注入类

加注解的目的是什么?就是偷偷生成一个类,在这个类里面悄悄的消化你注解的内容,用我们最熟悉的方式。这样我们看起来只要加个注解就实现了,好厉害;如果你看了生成的Binding类,你就知道,切,这不是我们整天写的吗?

现在看一下生成的类

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131165245;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(final MainActivity target, View source) {
    this.target = target;

    View view;
    view = Utils.findRequiredView(source, R.id.id_button, "method 'doClick'");
    view2131165245 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.doClick(p0);
      }
    });
  }

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


    view2131165245.setOnClickListener(null);
    view2131165245 = null;
  }
}

上面的类就是ButterKnife自动生成的类。里面包含的@OnClick的处理,我来解析一下,你就明白了。

首先看类名, 类名的形式你一看就明白了,就是@注解的类 + 一个后缀ViewBinding。

其次,看一下变量, MainActivity不用解释了吧,你是在MainActivity的方法上添加的@OnClick; View即是你点击的控件。

再者看一下构造,构造传入了MainActivity, 然后findRequiredView,其实这里面就是findViewById。后面的实现看一眼就明白了,在生成类里面设置监听,然后调用你添加注解的Method。难以理解吗? 很简单的实现吧。这就是回调的使用吧,相信我们都经常使用,在View点击的时候,调用指定的方法。

最后看unbind()方法,在页面执行结束后,是会自动释放资源的。

再来看下BindView生成的Binding类

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;

    target.btn = Utils.findRequiredViewAsType(source, R.id.id_button, "field 'btn'", Button.class);
    target.tv = Utils.findRequiredViewAsType(source, R.id.id_textview, "field 'tv'", TextView.class);
  }

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

    target.btn = null;
    target.tv = null;
  }
}

和OnClick生成的类很相似,只是改成了findRequiredViewAsType。 然而这个方法的实现不过是findViewById加类型转换,也就是类似的

Button btn = (Button)findViewById(R.id.btn);

看到这里,或许有人就奇怪了,你是怎么生成的? 答:技术就是使用JavaPoet。 至于这些属性怎么得到的, 如类名, id值, Activity与View, 控件是什么, 绑定的onClick方法是什么? 那你得去问AnnotationProcessor了,就是这么神通广大。


我们来捋一下思路,

1 添加注解
2)在AnnotationProcessor中解析注解(至于怎么解析,下面说)
3)根据解析的信息,自动生成Binding类
4)在ButterKnife.bind时,一般是onCreate, 得到对应的Binding类,通过反射,生成Binding类的对象,执行Binding类的构造函数。在构造中偷偷的findViewById, setOnClickListener等。这时候在对应的Activity中添加注解的控件就有值了。添加OnClick的控件就设置了监听了。

看了以上的思路,是不是恍然大悟?所以我才说这是一个很简单的框架。那么接下来,解读一下注解处理的源码与生成类的源码

首先看配置

// 导入生成代码的库
 api deps.javapoet
// 导入自动注册的库
  compileOnly deps.auto.service

// 上面的库是什么
javapoet: 'com.squareup:javapoet:1.10.0',
'auto': [
         'service': 'com.google.auto.service:auto-service:1.0-rc4',
         'common': 'com.google.auto:auto-common:0.10',
      ],

然后看下注解处理器的代码, ButterKnifeProcessor, 其中process为解析的方法

@Override public boolean process(Set elements, RoundEnvironment env) {
    Map bindingMap = findAndParseTargets(env);

    for (Map.Entry 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;
  }
######################################

private Map findAndParseTargets(RoundEnvironment env) {
    Map builderMap = new LinkedHashMap<>();
    Set erasedTargetNames = new LinkedHashSet<>();

    scanForRClasses(env);

    // Process each @BindAnim element.
    for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceAnimation(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindAnim.class, e);
      }
    }

    // Process each @BindArray element.
    for (Element element : env.getElementsAnnotatedWith(BindArray.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceArray(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindArray.class, e);
      }
    }

    // Process each @BindBitmap element.
    for (Element element : env.getElementsAnnotatedWith(BindBitmap.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceBitmap(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindBitmap.class, e);
      }
    }

    // Process each @BindBool element.
    for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceBool(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindBool.class, e);
      }
    }

    // Process each @BindColor element.
    for (Element element : env.getElementsAnnotatedWith(BindColor.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceColor(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindColor.class, e);
      }
    }

    // Process each @BindDimen element.
    for (Element element : env.getElementsAnnotatedWith(BindDimen.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceDimen(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindDimen.class, e);
      }
    }

    // Process each @BindDrawable element.
    for (Element element : env.getElementsAnnotatedWith(BindDrawable.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceDrawable(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindDrawable.class, e);
      }
    }

    // Process each @BindFloat element.
    for (Element element : env.getElementsAnnotatedWith(BindFloat.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceFloat(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindFloat.class, e);
      }
    }

    // Process each @BindFont element.
    for (Element element : env.getElementsAnnotatedWith(BindFont.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceFont(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindFont.class, e);
      }
    }

    // Process each @BindInt element.
    for (Element element : env.getElementsAnnotatedWith(BindInt.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceInt(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindInt.class, e);
      }
    }

    // Process each @BindString element.
    for (Element element : env.getElementsAnnotatedWith(BindString.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceString(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindString.class, e);
      }
    }

    // Process each @BindView element.
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      // 遍历带有{@interface: BindView}的元素
      // 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);
      }
    }

    // Process each @BindViews element.
    for (Element element : env.getElementsAnnotatedWith(BindViews.class)) {
      // we don't SuperficialValidation.validateElement(element)
      // so that an unresolved View type can be generated by later processing rounds
      try {
        parseBindViews(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindViews.class, e);
      }
    }

    // Process each annotation that corresponds to a listener.
    for (Class listener : LISTENERS) {
      findAndParseListener(env, listener, builderMap, erasedTargetNames);
    }

    // Associate superclass binders with their subclass binders. This is a queue-based tree walk
    // which starts at the roots (superclasses) and walks to the leafs (subclasses).
    Deque> entries =
        new ArrayDeque<>(builderMap.entrySet());
    Map bindingMap = new LinkedHashMap<>();
    while (!entries.isEmpty()) {
      Map.Entry entry = entries.removeFirst();

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

      TypeElement parentType = findParentType(type, erasedTargetNames);
      if (parentType == null) {
        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;
  }

############################################

private void parseBindView(Element element, Map builderMap,
      Set erasedTargetNames) {
    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.
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      TypeVariable typeVariable = (TypeVariable) elementType;
      elementType = typeVariable.getUpperBound();
    }
    Name qualifiedName = enclosingElement.getQualifiedName();
    Name simpleName = element.getSimpleName();
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
      if (elementType.getKind() == TypeKind.ERROR) {
        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.
    int id = element.getAnnotation(BindView.class).value();

    BindingSet.Builder builder = builderMap.get(enclosingElement);
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    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 = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }

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

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

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

以上代码片段分为3部分。

部分1为process执行的部分,从代码可以得知,主要有两部实现: 1) 找到所有的注解标签与信息 2)生成代码

部分2为查找所有的注解信息,如下以BindView为例

  // Process each @BindView element.
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      // 遍历带有{@interface: BindView}的元素
      // 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);
      }
    }

遍历所有带BindView的标签,然后解析注解信息。

部分3为解析注解信息, 我们可以得到注解的类的包名,类名, 注解的值是什么等等。


得到以上信息, 我们有了生成Binding类的所有条件。那么看一下怎么生成类的

JavaFile brewJava(int sdk, boolean debuggable) {
    return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable))
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }

  private TypeSpec createType(int sdk, boolean debuggable) {
    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, debuggable));

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

    return result.build();
  }

 private MethodSpec createBindingConstructorForView() {
    MethodSpec.Builder builder = MethodSpec.constructorBuilder()
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC)
        .addParameter(targetTypeName, "target");
    if (constructorNeedsView()) {
      builder.addStatement("this(target, target)");
    } else {
      builder.addStatement("this(target, target.getContext())");
    }
    return builder.build();
  }

看了吗? 根据MethodSpec可以生成任意的方法,根据TypeSpec可以生成任意的类;只要我们取得生成需要的相关信息。


整个流程都顺畅了,那么怎么调用的Binding类实现注入的呢? 答案是在ButterKnife.bind时。
注意:在编译的时候已经生成对应的Binding类, 在bind时只需要查找对应的类
看一下源码

 @NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull Dialog source) {
    View sourceView = source.getWindow().getDecorView();
    return createBinding(target, sourceView);
  }

核心方法是在createBinding中,继续往下看

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

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

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InstantiationException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      }
      if (cause instanceof Error) {
        throw (Error) cause;
      }
      throw new RuntimeException("Unable to create binding instance.", cause);
    }
  }

你看到了什么? findBindingConstructorForClass很明显是根据targetClass(MainActivity)得到构造函数。然后 return constructor.newInstance(target, source); 生成实例,这时候就调用了构造函数。而在构造函数中,它执行了findViewById与setOnClickListener, 并完成了赋值。这时候, 添加注解的Button 、View等就完成findViewById的赋值了。Button.setOnClickListener也完成了。

如果你不甘心,继续看一下如何得到构造函数的吧,这就用到了反射了。

private static Constructor findBindingConstructorForClass(Class cls) {
    Constructor bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      Class bindingClass = cls.getClassLoader().(clsName + "_ViewBinding");
      //noinspection unchecked
      bindingCtor = (Constructor) bindingClass.getConstructor(cls, View.class);
      if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
    } catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
    } catch (NoSuchMethodException e) {
      throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

没骗你吧, 加载Binding类,然后getConstructure,得到构造函数了。


通过以上的分析,你还迷茫吗?如果你把我列举的3个技术准备,那么你就可以完成自己的ButterKnife了。是不是很简单呢? 毕竟是最简单的框架之一了。好了不早了,拜拜了~!

还是那句话,如果你觉得这篇文章对你有帮助,请帮我打Call, 如有疑问,请留言,谢谢!!!

你可能感兴趣的:(ButterKnife,你真的了解吗?)