黄油刀法:ButterKnife

661 阅读7分钟

1. 前言

他来了,他来了,他带着黄油刀走来了。

谁说不想学习的?(狗头)

2. 介绍

来自官网的介绍:

Field and method binding for Android views

也就是说 这的库是 关于Android视图中方法和变量的绑定。

不过在Github中也出现了一句提示:

Development on this tool is winding down. Please consider switching to view binding in the coming months

大概意思就是 使用这个工具开发的人在变少,建议使用数据绑定。

不过我们还是要来学习一个他的源码。

3. 正文

3.1 bind方法

简单使用案例:

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...
  }}

让我们直接进入到bind方法中去:

@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
  Class<?> targetClass = target.getClass();
 
  Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

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

  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() 方法去拿到构造器constructorconstructor会通过反射的方式拿到实例,现在让我们来具体的看看 findBindingConstructorForClass() 这个方法:

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
  Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
  if (bindingCtor != null || BINDINGS.containsKey(cls)) {
  
    return bindingCtor;
  }
  String clsName = cls.getName();
  // 如果是 java文件、androidx、android的文件就不进行处理
  if (clsName.startsWith("android.") || clsName.startsWith("java.")
      || clsName.startsWith("androidx.")) {
   
    return null;
  }
  try {
    // 在这里会加载一个 clsName_ViewBinding类的class对象
    Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
    // 在这里获取到clsName_ViewBinding类的构造器对象
    bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    
  } 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;
}

可以看到我们会先从BINDINGS中获取Constructor,如果存在的话,就直接返回。 那么BINDINGS是什么呢?

static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

其实也就是维持了一个LinkedHashMap的缓存,因为反射相对的来说还是会比较消耗性能,所以维持一个缓存来降低反射的调用。

可以看到我们最后会去找一个 clsName_ViewBinding类的实例,那么这个是什么呢?

我们可以反编译打开一个apk看看:

会发现在它的目录下会生成一个 clsName_ViewBindingJava类:

public class MainActivity_ViewBinding implements Unbinder {
    private MainActivity target;
    private View view7f070023;
    private View view7f070025;


    public MainActivity_ViewBinding(MainActivity mainActivity) {
        this(mainActivity, mainActivity.getWindow().getDecorView());
    }


    public MainActivity_ViewBinding(final MainActivity mainActivity, View view) {
        this.target = mainActivity;
        View findRequiredView = Utils.findRequiredView(view, R.id.button_click, "field 'buttonClick' and method 'onClick'");
        mainActivity.buttonClick = (Button) Utils.castView(findRequiredView, R.id.button_click, "field 'buttonClick'", Button.class);
        this.view7f070025 = findRequiredView;
        findRequiredView.setOnClickListener(new DebouncingOnClickListener() {
            public void doClick(View view) {
                mainActivity.onClick(view);
            }
        });
        View findRequiredView2 = Utils.findRequiredView(view, R.id.button_2, "method 'onClick'");
        this.view7f070023 = findRequiredView2;
        findRequiredView2.setOnClickListener(new DebouncingOnClickListener() {
            public void doClick(View view) {
                mainActivity.onClick(view);
            }
        });
    }


    public void unbind() {
        MainActivity mainActivity = this.target;
        if (mainActivity != null) {
            this.target = null;
            mainActivity.buttonClick = null;
            this.view7f070025.setOnClickListener((View.OnClickListener) null);
            this.view7f070025 = null;
            this.view7f070023.setOnClickListener((View.OnClickListener) null);
            this.view7f070023 = null;
            return;
        }
        throw new IllegalStateException("Bindings already cleared.");
    }
}

我们可以看到在ButterKnife.bind()中返回的构造对象应该就是这个 MainActivity_ViewBinding的对象。

我们进入到Utils#findRequiredView()的方法中:

public static View findRequiredView(View view, int i, String str) {
        View findViewById = view.findViewById(i);
        if (findViewById != null) {
            return findViewById;
        }
        String resourceEntryName = getResourceEntryName(view, i);
        throw new IllegalStateException("Required view '" + resourceEntryName + "' with ID " + i + " for " + str + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional' (methods) annotation.");
    }

可以看到内部也是使用findViewById来找寻对象的。

那么这个类是如何生成的呢?

3.2 ButterKnifeProcessor注解处理器

其实是ButterKnife 会通过自定义的注解处理器 ButterKnifeProcessor来对扫描到的注解进行处理, 然后通过javapoet来动态的生成代码,最后运行的时候,就可以通过bind方法来完成绑定了。

我们先来看看ButterKnifeProcessorinit()方法:

@Override public synchronized void init(ProcessingEnvironment env) {
  super.init(env);
//.....
  // 处理TypeMirror的工具类,用于取类信息
  typeUtils = env.getTypeUtils();
  // filer 可以用来创建文件
  filer = env.getFiler();
  //....
}

继续来看重写的方法getSupportedAnnotationTypes()

@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(BindAnim.class);
  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(BindFont.class);
  annotations.add(BindInt.class);
  annotations.add(BindString.class);
  annotations.add(BindView.class);
  annotations.add(BindViews.class);
  annotations.addAll(LISTENERS);


  return annotations;
}

这些就是处理器能够处理的注解。

现在让我们进入到主要方法process方法中:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
  // 这里是拿到所有的注解信息
  Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
  // 通过循环的方式用javapoet来生成Java代码
  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;
}

让我们再次进入到 findAndParseTargets() 中:

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

  //...... 这里是一系列的对BindXXX的处理
  // 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);
  }
}
// ......

}

对注解一系列的处理后都放入到 builderMap.

我们可以来看看parseBindView() 的处理:

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

  // 这里判断的是被注解的是否是 private 和 static
  // 判断是否被注解在错误的包上
  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();
  // 判断 是否是View 或者 接口
  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();
  // 通过类元素判断build是否存在
  BindingSet.Builder builder = builderMap.get(enclosingElement);
  Id resourceId = elementToId(element, BindView.class, id);
  if (builder != null) {
    // 判断是否已经被绑定过了
    String existingBindingName = builder.findExistingBindingName(resourceId);
    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 {
    // 创建一个新的build
    builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
  }

  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);
}

让我们再回到process方法中,通过binding.brewJava去生成一个Java类:

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

进入到createType() 中:

private TypeSpec createType(int sdk, boolean debuggable) {
  TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
      .addModifiers(PUBLIC)
      .addOriginatingElement(enclosingElement);
  if (isFinal) {
    result.addModifiers(FINAL);
  }

  if (parentBinding != null) {
    result.superclass(parentBinding.getBindingClassName());
  } 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) {
    // 这里就是增加unbind方法
    result.addMethod(createBindingUnbindMethod(result));
  }

  return result.build();
}

这里的一系列操作其实是为了javapoet生成java源文件做准备的。

然后在 javaFile.writeTo(filer) 中去生成了源文件。

3.2.1 总结

1. 可以看到butterKnife会在编译的时候扫描注解,然后通过自定义的ButterKnifeProcessor对注解的对象进行处理,最后通过javapoet生成java代码。
2. 然后调用ButterKnife的bind()方法,会找到生成的代码完成绑定,在其中完成一系列的操作。

3.3 Library中的R2文件

我们来看看butterknife源码中的sample中的library中的案例:

可以看到其中绑定的时候 R文件变成了R2,这是为什么呢?

我们先来看看主项目中的R文件和module中的R文件的区别:

  1. 主项目中创建一个资源文件,会在R.java中添加一个静态常量
  2. 而在子项目中创建资源文件后,R.java中自动添加的是静态变量而不是静态常量,这就导致需要需要用的常量的地方无法使用R.xxx的方式。

那么在butterknife中注解是需要用到R文件的静态变量怎么办呢?

butterknife是这样做的,创建一个R2文件,这个R2R文件的复制,只是在所有的值上都添加了final,让所有的变量都变成了常量。然后在生成ViewBinding类的时候再替换回来。

而这个R2文件是作者通过gradle插件来完成的,也就是butterknife-gradle-plugin

plugin中的 ButterKnifePlugin

我们进入到 R2Generator中:

task执行的时候,在这里javapoet会去生成代码。

关于R2的信息可以看看这个文章:

AOP 最后一块拼图 | AST 抽象语法树 —— 最轻量级的AOP方法

参考:

butterknife源码

butterknife 源码分析

AOP 最后一块拼图 | AST 抽象语法树 —— 最轻量级的AOP方法