Mark Xu 的博客

记录精彩的程序人生

ButterKnife 源码分析

ButterKnife 框架是我们日常使用最频繁的框架之一,本文对 ButterKnife 8.8.1 版本进行源码分析。

整体流程

  • 编写代码时对目标元素进行注解
  • 编译时注解处理器会扫描注解,并通过 JavaPoet 自动生成 Java 文件
  • 调用 ButterKnife.bind() 方法时,会通过反射拿到之前为目标类生成的 TargetClassName_ViewBinding 类,并调用其构造方法,完成绑定

具体分析

源码组成

ButterKnife 的源码主要的组成部分包括三个:butterknife、butterknife-annotations、butterknife-compiler。其中 butterknife-annotations 声明了所有的注解;butterknife-compiler 为注解处理器,在编译阶段对注解进行处理;butterknife 则是我们调用的接口。接下来我们一一来看。

butterknife-annotations

ButterKnife 提供了如下注解:

  • 绑定类型的注解:BindAnim、BindArray、BindBitmap、BindBool、BindColor、BindDimen、BindDrawable、BindFloat、BindFont、BindInt、BindString、BindView、BindViews
  • 监听类型注解:OnCheckedChanged、OnClick、OnEditorAction、OnFocusChange、OnItemClick、OnItemLongClick、OnItemSelected、OnLongClick、OnPageChange、OnTextChanged、OnTouch、Optional

butterknife-compiler

复习下在 EventBus 源码分析时用到的注解处理器:

注解处理器用来在编译时扫描和处理注解,会自动生成 .java 文件。

每一个注解处理器都继承于 AbstractProcessor,

package com.example;
public class MyProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment env){ }
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
    @Override
    public Set<String> getSupportedAnnotationTypes() { }
    @Override
    public SourceVersion getSupportedSourceVersion() { }
}

其中必需的几个方法如下:

  • init(ProcessingEnvironment env):会被外部的工具调用,传入 ProcessingEnvironment 参数,我们可以从其中获取到一些工具类
  • process():在这里扫描、处理注解以及生成代码
  • getSupportedAnnotationsTypes():在这里定义本注解处理器会注册到哪些注解上
  • getSupportedSourceVersion():指定 Java 版本

那么首先来看看在 butterknife-compiler 中的关键类:ButterknifeProcessor。

public final class ButterKnifeProcessor extends AbstractProcessor {
	// 初始化工具
	@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<>();
	    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(BindColor.class);
	    annotations.add(BindView.class);
	    ...
	    annotations.addAll(LISTENERS);

	    return annotations;
    }

    // 对注解进行处理
    @Override 
    public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {}
}

process 方法

接下来我们重点看看 process 方法,也就是注解处理器是如何对注解进行操作的。

// 对注解进行处理
@Override 
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
// 扫描注解并解析,得到 Map
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

// 生成 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;
}

通过参数 RoundEmviroment 我们可以拿到所有包含特定注解的被注解元素,生成一个 Key 为 TypeElement,Value 为 BindingSet 的 Map;接着遍历 Map 生成相应的 Java 文件。

先来看看扫描注解并解析的源码:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    ...
    // Process each @BindAnim element.
    // 遍历所有被注解的元素
    for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
        try {
          parseResourceAnimation(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
          ...
        }
    }
    ...

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

    // 把之前的 builderMap 转换为 bindingMap
    ...
    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;
}

中间省略了一些注解的处理,每种注解处理方式基本一致,以 BindAnim 为例,其中的关键方法为 parseResourceAnimation,其他注解的该方法分别对应着(parseBindView、parseResourceString 等)

private void parseResourceAnimation(Element element,
    ...
    // Assemble information on the field.
    BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    builder.addResource(new FieldAnimationBinding(getId(qualifiedId), name));

    erasedTargetNames.add(enclosingElement);
}

parseResourceAnimation 的作用为生成被注解元素所对应类的 Builder,并得到 builderMap。

接下来看看生成 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());
    }
}

这里使用 JavaPoet 生成了代码。

源码分析未完待续!

来看看 JavaPoet 为我们生成的 Java 文件(示例中 MainActivity 中有两个点击事件绑定):

public class MainActivity_ViewBinding implements Unbinder {
    private MainActivity target;

    private View view2131230756;

    private View view2131230758;

    @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.btn_alipay, "method 'onViewClicked'");
        view2131230756 = view;
        view.setOnClickListener(new DebouncingOnClickListener() {
            @Override
            public void doClick(View p0) {
                target.onViewClicked(p0);
            }
        });
        view = Utils.findRequiredView(source, R.id.btn_custom_view, "method 'onViewClicked'");
        view2131230758 = view;
        view.setOnClickListener(new DebouncingOnClickListener() {
            @Override
            public void doClick(View p0) {
                target.onViewClicked(p0);
            }
        });
    }

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

        view2131230756.setOnClickListener(null);
        view2131230756 = null;
        view2131230758.setOnClickListener(null);
        view2131230758 = null;
    }
}

在构造方法中,可以看到我们熟悉的点击事件,View 的绑定同理。

butterknife

在 ButterKnife 的标准用法中,会在 Activity 或 Fragment 的 onCreate 方法中调用 ButterKnife.bind(this); 进行绑定,我们就首先从 bind 方法入手。

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

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

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
        return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
        ...
    }
}

可以看到,通过反射首先拿到 target 的类名,然后找到其对应的构造器,接着创建相应的对象。其中的 findBindingConstructorForClass 方法源码如下:

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) {
        return bindingCtor;
    }
    String clsName = cls.getName();
    try {
        Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
        //noinspection unchecked
        bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    } catch (ClassNotFoundException e) {
    } catch (NoSuchMethodException e) {
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

可以看到,根据类名拿到 targetClassName_ViewBinding 类的构造器,并调用其构造方法,进行绑定

参考文档

ButterKnife 源码分析

留下你的脚步