欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ButterKnife,你真的了解吗?

程序员文章站 2022-03-16 08:54:31
...

源自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<? 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;
  }
######################################

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> 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<? extends Annotation> 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<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) {
        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<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);

    // 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<? 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);
    } 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<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> 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<? extends Unbinder>) 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, 如有疑问,请留言,谢谢!!!

上一篇: Linux系统启动

下一篇: C++内联函数