ButterKnife源码解析(1)

作者: fyales | 来源:发表于2016-05-03 19:51 被阅读156次

ButterKnife是通过注解进行动态注入的,如果你对注解还不太了解的话,可以先看上篇有关注解的文章注解

ButterKnife的工作原理一般分为两步:

  • 通过@Bind(R.id.),@OnClick(R.id.)等注解在编译的时候动态生成java文件,生成java文件编译器会将它们编译成对应的class文件
  • 通过ButterKnife.bind(this)等类似方法将ID与对应的上下文绑定在一起。

首先来看最基本的注解类:

@Retention(CLASS) @Target(FIELD)
public @interface Bind {
 /** View ID to which the field will be bound. */
int[] value();
}

这个就是最常用的Bind注解,一般用它来标识要绑定的view,在这里可以清楚的看到,这个view是在编译的时候起作用的,并且它只能用来标识成员变量。这就表示我们可以在编译的时候解析它,从而生成java源代码。

ButterKnife的注解解析器为ButterknifeProcessor,它的最主要方法如下:

//初始化
  @Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    elementUtils = env.getElementUtils();
    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
  }

//在这里定义支持的注解类
  @Override public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<String>();

    types.add(Bind.class.getCanonicalName());

    for (Class<? extends Annotation> listener : LISTENERS) {
      types.add(listener.getCanonicalName());
    }

    types.add(BindBool.class.getCanonicalName());
    types.add(BindColor.class.getCanonicalName());
    types.add(BindDimen.class.getCanonicalName());
    types.add(BindDrawable.class.getCanonicalName());
    types.add(BindInt.class.getCanonicalName());
    types.add(BindString.class.getCanonicalName());

    return types;
  }

//对注解进行解析
  @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingClass bindingClass = entry.getValue();

      try {
        JavaFileObject jfo = filer.createSourceFile(bindingClass.getFqcn(), typeElement);
        Writer writer = jfo.openWriter();
        writer.write(bindingClass.brewJava());
        writer.flush();
        writer.close();
      } catch (IOException e) {
        error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
            e.getMessage());
      }
    }

    return true;
  }

所有通过ButterKnife注解的元素都会走到process方法,在process方法里,首先通过findAndParseTargets(env)方法将所有具有注解的类的信息挨个放在BindingClass里面,最后形成以TypeElement为键,BindingClass为值的键值对。

接下来,就只需要循环遍历这个键值对,然后根据TypeElement和BindingClass里面的信息生成对应的java类。例如MainActivity生成的类即为MainActivity$$ViewBinder类。

下面来看看findAndParseTargets(env)方法里面:

 private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<TypeElement, BindingClass>();
    Set<String> erasedTargetNames = new LinkedHashSet<String>();

    // Process each @Bind element.
    for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
      try {
        parseBind(element, targetClassMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, Bind.class, e);
      }
    }

    // Process each annotation that corresponds to a listener.
    for (Class<? extends Annotation> listener : LISTENERS) {
      findAndParseListener(env, listener, targetClassMap, erasedTargetNames);
    }

    //....

    // Try to find a parent binder for each.
    for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
      String parentClassFqcn = findParentFqcn(entry.getKey(), erasedTargetNames);
      if (parentClassFqcn != null) {
        entry.getValue().setParentViewBinder(parentClassFqcn + SUFFIX);
      }
    }

    return targetClassMap;
  }

这个方法做的主要工作就是将各注解进行分拆遍历,并且进行解析,最后将解析的结果放入targetClassMap,这里重点看Bind注解的解析:

private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap,
      Set<String> erasedTargetNames) {
    // Verify common generated code restrictions.
    if (isInaccessibleViaGeneratedCode(Bind.class, "fields", element)
        || isBindingInWrongPackage(Bind.class, element)) {
      return;
    }

    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.ARRAY) {
      parseBindMany(element, targetClassMap, erasedTargetNames);
    } else if (LIST_TYPE.equals(doubleErasure(elementType))) {
      parseBindMany(element, targetClassMap, erasedTargetNames);
    } else if (isSubtypeOfType(elementType, ITERABLE_TYPE)) {
      error(element, "@%s must be a List or array. (%s.%s)", Bind.class.getSimpleName(),
          ((TypeElement) element.getEnclosingElement()).getQualifiedName(),
          element.getSimpleName());
    } else {
      parseBindOne(element, targetClassMap, erasedTargetNames);
    }
  }

在这里可以看到ButterKnife会根据值的不同采取不同的解析方式,在这里我们看下最通用的解析方式,即parseBindOne(element,targetClassMap,erasedTargetNames),这也是解析器最核心的地方

private void parseBindOne(Element element, Map<TypeElement, BindingClass> targetClassMap,
      Set<String> erasedTargetNames) {
    boolean hasError = false;
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Verify that the target type extends from View.
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      TypeVariable typeVariable = (TypeVariable) elementType;
      elementType = typeVariable.getUpperBound();
    }
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
      error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
          Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName());
      hasError = true;
    }

    // Assemble information on the field.
    int[] ids = element.getAnnotation(Bind.class).value();
    if (ids.length != 1) {
      error(element, "@%s for a view must only specify one ID. Found: %s. (%s.%s)",
          Bind.class.getSimpleName(), Arrays.toString(ids), enclosingElement.getQualifiedName(),
          element.getSimpleName());
      hasError = true;
    }

    if (hasError) {
      return;
    }

    int id = ids[0];
    BindingClass bindingClass = targetClassMap.get(enclosingElement);
    if (bindingClass != null) {
      ViewBindings viewBindings = bindingClass.getViewBinding(id);
      if (viewBindings != null) {
        Iterator<FieldViewBinding> iterator = viewBindings.getFieldBindings().iterator();
        if (iterator.hasNext()) {
          FieldViewBinding existingBinding = iterator.next();
          error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
              Bind.class.getSimpleName(), id, existingBinding.getName(),
              enclosingElement.getQualifiedName(), element.getSimpleName());
          return;
        }
      }
    } else {
      bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
    }

    String name = element.getSimpleName().toString();
    String type = elementType.toString();
    boolean required = isRequiredBinding(element);

    FieldViewBinding binding = new FieldViewBinding(name, type, required);
    bindingClass.addField(id, binding);

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

在这个方法里面,enclosingElement包含使用Bind注解类的信息,通过下面的方法获取注解标注的值。

    int[] ids = element.getAnnotation(Bind.class).value();

接下来的任务就是如果没有创建BindingClass了(如果没有的话),创建BindingClass类也很简单,它的构造函数也只需要包名,类名等基本信息:

  private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap,
      TypeElement enclosingElement) {
    BindingClass bindingClass = targetClassMap.get(enclosingElement);
    if (bindingClass == null) {
      String targetType = enclosingElement.getQualifiedName().toString();
      String classPackage = getPackageName(enclosingElement);
      String className = getClassName(enclosingElement, classPackage) + SUFFIX;

      bindingClass = new BindingClass(classPackage, className, targetType);
      targetClassMap.put(enclosingElement, bindingClass);
    }
    return bindingClass;
  }

之后,就将view的信息绑定在FieldViewBinding类的实例中,最后添加到对应的BindingClass实例中,这样使用注解的实例中所有有关注解的信息及这个实体类本身的信息都会在这个BindingClass类的实体类中,ButterKnife接着会利用这个类来生成对应的java文件。

接下来绑定的步骤与上文讲解的类似,基本上都是这个原理,在这里不做赘述,当解析完成之后,所有使用注解的实例的相关信息都会存储在BindingClass的集合内(targetClassMap)。接下来做的工作就是循环遍历这个集合生成文件。

生成的字符串是由bindingClass.brewJava()实现的,该方法生成了java文件里面的所有字符串

String brewJava() {
    StringBuilder builder = new StringBuilder();
    builder.append("// Generated code from Butter Knife. Do not modify!\n");
    builder.append("package ").append(classPackage).append(";\n\n");

    if (!resourceBindings.isEmpty()) {
      builder.append("import android.content.res.Resources;\n");
    }
    if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
      builder.append("import android.view.View;\n");
    }
    builder.append("import butterknife.ButterKnife.Finder;\n");
    if (parentViewBinder == null) {
      builder.append("import butterknife.ButterKnife.ViewBinder;\n");
    }
    builder.append('\n');

    builder.append("public class ").append(className);
    builder.append("<T extends ").append(targetClass).append(">");

    if (parentViewBinder != null) {
      builder.append(" extends ").append(parentViewBinder).append("<T>");
    } else {
      builder.append(" implements ViewBinder<T>");
    }
    builder.append(" {\n");

    emitBindMethod(builder);
    builder.append('\n');
    emitUnbindMethod(builder);

    builder.append("}\n");
    return builder.toString();
  }

这个方法首先生成包名,然后引入各个类,接下来生成类的主题,通过emitBindMethod(builder)方法生成类的bind(builder)方法,通过emitUnbindMethod(builder)方法生成类的unbind(builder)方法。

  private void emitBindMethod(StringBuilder builder) {
  builder.append("  @Override ")
      .append("public void bind(final Finder finder, final T target, Object source) {\n");

  // Emit a call to the superclass binder, if any.
  if (parentViewBinder != null) {
    builder.append("    super.bind(finder, target, source);\n\n");
  }

  if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
    // Local variable in which all views will be temporarily stored.
    builder.append("    View view;\n");

    // Loop over each view bindings and emit it.
    for (ViewBindings bindings : viewIdMap.values()) {
      emitViewBindings(builder, bindings);
    }

    // Loop over each collection binding and emit it.
    for (Map.Entry<FieldCollectionViewBinding, int[]> entry : collectionBindings.entrySet()) {
      emitCollectionBinding(builder, entry.getKey(), entry.getValue());
    }
  }

  if (!resourceBindings.isEmpty()) {
    builder.append("    Resources res = finder.getContext(source).getResources();\n");

    for (FieldResourceBinding binding : resourceBindings) {
      builder.append("    target.")
          .append(binding.getName())
          .append(" = res.")
          .append(binding.getMethod())
          .append('(')
          .append(binding.getId())
          .append(");\n");
    }
  }

  builder.append("  }\n");
}


 private void emitViewBindings(StringBuilder builder, ViewBindings bindings) {
  builder.append("    view = ");

  List<ViewBinding> requiredViewBindings = bindings.getRequiredBindings();
  if (requiredViewBindings.isEmpty()) {
    builder.append("finder.findOptionalView(source, ")
        .append(bindings.getId())
        .append(", null);\n");
  } else {
    if (bindings.getId() == View.NO_ID) {
      builder.append("target;\n");
    } else {
      builder.append("finder.findRequiredView(source, ")
          .append(bindings.getId())
          .append(", \"");
      emitHumanDescription(builder, requiredViewBindings);
      builder.append("\");\n");
    }
  }

  emitFieldBindings(builder, bindings);
  emitMethodBindings(builder, bindings);
}

可以看到这里先读取每个绑定view的配置(ViewBindings),接下来绑定view,首先找到activity里面对应的view,然后通过emitFieldBindings(builder,bindings)将成员变量的view注入到activity中,emitMethodBindings(builder.bindings)则用来绑定事件。这里会在下一篇文章详细解说。

当这一切都结束之后,就可以生成文件了,在编译阶段生成的java文件也会生成class文件,在这里为了方便读者理解,给出一个最基本的MainActivity类和生成的MainActivity&&ViewBindler类的源代码:

//MainActivity.java
package com.fyales.butterknifedemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.text_tv)
    TextView textTv;
    @Bind(R.id.click_btn)
    Button clickBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
    }

    @OnClick(R.id.text_tv)
    void textTvClick(){
        Toast.makeText(this,"hello world",Toast.LENGTH_SHORT).show();
    }
}

//MainActivity$$ViewBinder
// Generated code from Butter Knife. Do not modify!
package com.fyales.butterknifedemo;

import android.view.View;
import butterknife.ButterKnife.Finder;
import butterknife.ButterKnife.ViewBinder;

public class MainActivity$$ViewBinder<T extends com.fyales.butterknifedemo.MainActivity> implements ViewBinder<T> {
  @Override public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131492943, "field 'textTv' and method 'textTvClick'");
    target.textTv = finder.castView(view, 2131492943, "field 'textTv'");
    view.setOnClickListener(
      new butterknife.internal.DebouncingOnClickListener() {
        @Override public void doClick(
          android.view.View p0
        ) {
          target.textTvClick();
        }
      });
    view = finder.findRequiredView(source, 2131492944, "field 'clickBtn'");
    target.clickBtn = finder.castView(view, 2131492944, "field 'clickBtn'");
  }

  @Override public void unbind(T target) {
    target.textTv = null;
    target.clickBtn = null;
  }
}

到这里,我们就将ButterKnife编译器生成文件部分的代码分析完毕了,更多的细节还请读者自己阅读ButterKnife源码,如果有什么疏漏,还望指出,下一次我们来分析ButterKnife与上下文绑定。

相关文章

网友评论

    本文标题:ButterKnife源码解析(1)

    本文链接:https://www.haomeiwen.com/subject/zphrrttx.html