手把手教你吃透Android Butterknife源码(3)

47 阅读8分钟

作为一名Android开发者,相信你一定使用过Butterknife这个库。它极大地简化了我们在Android开发中对视图(View)的绑定以及事件处理的代码量,让原本冗长繁琐的findViewById和setOnClickListener代码变得简洁优雅。但在享受便利的同时,你是否好奇过它背后的实现原理?今天,我就带着大家一起深入Android Butterknife的源码,揭开它神秘的面纱,从最底层的代码逻辑出发,一步步搞懂它是如何工作的。

1. Butterknife库的整体架构概览

在深入源码之前,我们先来宏观地了解一下Butterknife库的整体架构。Butterknife主要通过编译时注解处理器(Annotation Processor)运行时代码两大部分来实现其功能。编译时注解处理器负责在编译阶段扫描代码中的注解,并生成对应的绑定代码;而运行时代码则负责在运行时加载并执行这些生成的绑定代码,完成视图绑定和事件处理。

1.1 核心类介绍

在Butterknife库中,有几个核心类起着至关重要的作用:

  • ButterKnife类:这是我们在使用Butterknife时最常接触到的类,它提供了静态方法来完成视图绑定和生命周期管理等操作。
public final class ButterKnife {
    // 私有化构造函数,防止外部实例化
    private ButterKnife() {
        throw new AssertionError("No instances.");
    }

    // 绑定Activity的方法
    @NonNull
    public static Unbinder bind(@NonNull Activity target) {
        return bind(target, target);
    }

    // 绑定View的方法
    @NonNull
    public static Unbinder bind(@NonNull View target) {
        return bind(target, findBinding(target));
    }

    // 绑定Fragment的方法
    @NonNull
    public static Unbinder bind(@NonNull Fragment target, @NonNull View source) {
        return bind(target, source, findBinding(source));
    }
    // 省略其他绑定方法...
}
  • Unbinder接口:它用于解除视图绑定,在不再需要绑定的视图时,通过调用Unbinder的unbind方法来释放资源,防止内存泄漏。
public interface Unbinder {
    // 解除绑定的方法
    void unbind();
}
  • ButterKnifeProcessor类:这是编译时注解处理器的核心类,它继承自AbstractProcessor,负责扫描和处理代码中的Butterknife相关注解,并生成对应的绑定代码。

2. 编译时注解处理器的工作原理

编译时注解处理器是Butterknife实现的关键部分,它在编译阶段就开始工作,通过扫描代码中的注解,生成额外的Java代码来完成视图绑定和事件处理逻辑。接下来,我们详细分析ButterknifeProcessor类的源码。

2.1 初始化注解处理器

ButterknifeProcessor类继承自AbstractProcessor,需要重写一些方法来完成初始化和处理逻辑。

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
    // 用于记录日志的对象
    private Messager messager;
    // 用于处理元素的工具类
    private Elements elementUtils;
    // 用于处理类型的工具类
    private Types typeUtils;
    // 用于生成代码的工具类
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        messager = processingEnvironment.getMessager();
        elementUtils = processingEnvironment.getElementUtils();
        typeUtils = processingEnvironment.getTypeUtils();
        filer = processingEnvironment.getFiler();
    }
    // 省略其他方法...
}

在init方法中,获取了编译时所需的一些工具类,如Messager用于记录日志、Elements用于处理元素、Types用于处理类型、Filer用于生成代码等。

2.2 支持的注解和类型

ButterknifeProcessor需要指定它支持的注解和Java版本等信息。

@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    // 添加支持的注解类型
    types.add(BindView.class.getCanonicalName());
    types.add(OnClick.class.getCanonicalName());
    // 省略其他支持的注解...
    return types;
}

@Override
public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
}

在getSupportedAnnotationTypes方法中,添加了Butterknife支持的各种注解,如BindView用于视图绑定,OnClick用于点击事件绑定等。getSupportedSourceVersion方法则指定了支持的Java版本。

2.3 处理注解

process方法是注解处理器的核心方法,它在编译时被调用,用于处理扫描到的注解。

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
    Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
    // 处理BindView注解
    processBindViews(bindingMap, roundEnvironment);
    // 处理OnClick注解
    processOnClick(bindingMap, roundEnvironment);
    // 省略其他注解的处理...

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        BindingSet binding = entry.getValue();
        try {
            JavaFile javaFile = JavaFile.builder(
                    elementUtils.getPackageOf(typeElement).getQualifiedName().toString(),
                    binding.brewJava(typeUtils, elementUtils))
                  .build();
            javaFile.writeTo(filer);
        } catch (IOException e) {
            error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
        }
    }
    return true;
}

在process方法中,首先创建一个用于存储绑定信息的Map。然后分别调用processBindViews、processOnClick等方法来处理不同类型的注解。处理完注解后,遍历绑定信息Map,生成对应的Java代码文件并写入磁盘。

2.3.1 处理BindView注解

private void processBindViews(Map<TypeElement, BindingSet> bindingMap, RoundEnvironment roundEnvironment) {
    for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
        try {
            // 检查元素是否是字段
            if (!isField(element)) {
                error(element, "@%s must be set on field.", BindView.class.getSimpleName());
                continue;
            }
            // 获取字段元素
            VariableElement field = (VariableElement) element;
            // 获取字段所属的类元素
            TypeElement enclosingElement = (TypeElement) field.getEnclosingElement();
            // 获取BindView注解
            BindView bindView = field.getAnnotation(BindView.class);
            // 获取视图ID
            int id = bindView.value();
            // 如果视图ID无效,报错
            if (id < 0) {
                error(field, "id %s in @%s for field %s is not valid. (should be a valid R.id)", id,
                        BindView.class.getSimpleName(), field.getSimpleName());
                continue;
            }
            // 获取绑定集合,如果不存在则创建一个新的
            BindingSet binding = bindingMap.get(enclosingElement);
            if (binding == null) {
                binding = new BindingSet(typeUtils, elementUtils, enclosingElement);
                bindingMap.put(enclosingElement, binding);
            }
            // 将视图绑定信息添加到绑定集合中
            binding.addField(id, field);
        } catch (Exception e) {
            logError(e, element, "Unable to process @%s for %s", BindView.class.getSimpleName(), element);
        }
    }
}

在processBindViews方法中,遍历所有被BindView注解标注的元素。检查元素是否是字段,如果不是则报错。获取字段所属的类元素、BindView注解中的视图ID等信息,将视图绑定信息添加到对应的BindingSet中。

2.3.2 处理OnClick注解

private void processOnClick(Map<TypeElement, BindingSet> bindingMap, RoundEnvironment roundEnvironment) {
    for (Element element : roundEnvironment.getElementsAnnotatedWith(OnClick.class)) {
        try {
            // 检查元素是否是方法
            if (!isMethod(element)) {
                error(element, "@%s must be set on method.", OnClick.class.getSimpleName());
                continue;
            }
            // 获取方法元素
            ExecutableElement method = (ExecutableElement) element;
            // 获取方法所属的类元素
            TypeElement enclosingElement = (TypeElement) method.getEnclosingElement();
            // 获取OnClick注解
            OnClick onClick = method.getAnnotation(OnClick.class);
            // 获取视图ID数组
            int[] ids = onClick.value();
            // 如果视图ID数组为空,报错
            if (ids.length == 0) {
                error(method, "No view id specified for @%s annotation.", OnClick.class.getSimpleName());
                continue;
            }
            // 获取绑定集合,如果不存在则创建一个新的
            BindingSet binding = bindingMap.get(enclosingElement);
            if (binding == null) {
                binding = new BindingSet(typeUtils, elementUtils, enclosingElement);
                bindingMap.put(enclosingElement, binding);
            }
            // 将点击事件绑定信息添加到绑定集合中
            for (int id : ids) {
                binding.addMethod(id, method);
            }
        } catch (Exception e) {
            logError(e, element, "Unable to process @%s for %s", OnClick.class.getSimpleName(), element);
        }
    }
}

在processOnClick方法中,遍历所有被OnClick注解标注的元素。检查元素是否是方法,如果不是则报错。获取方法所属的类元素、OnClick注解中的视图ID数组等信息,将点击事件绑定信息添加到对应的BindingSet中。

2.4 生成绑定代码

BindingSet类负责生成最终的绑定代码,它的brewJava方法会根据之前收集到的绑定信息,生成对应的Java代码。

public JavaFileObject brewJava(Types typeUtils, Elements elementUtils) {
    // 生成包名
    String packageName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
    // 生成类名
    String simpleName = typeElement.getSimpleName() + "$$ViewBinder";
    // 创建Java文件生成器
    JavaFileObject.Builder builder = JavaFileObject.builder(packageName, simpleName);
    // 添加导入语句
    builder.addImport("android.view.View");
    builder.addImport("butterknife.Unbinder");
    // 省略其他导入语句...

    // 生成类声明
    builder.addType(TypeSpec.classBuilder(simpleName)
          .addModifiers(PUBLIC, FINAL)
          .addSuperinterface(Unbinder.class)
          .addJavadoc("Generated code from Butter Knife. Do not modify!")
          .addMethod(generateConstructor())
          .addMethod(generateUnbindMethod())
          .build());
    return builder.build();
}

在brewJava方法中,首先确定生成代码的包名和类名,然后创建Java文件生成器。添加必要的导入语句后,通过addMethod方法分别生成构造函数和解除绑定的方法等,最终构建并返回生成的Java文件对象。

3. 运行时代码的工作流程

编译阶段生成的绑定代码在运行时是如何被调用和执行的呢?接下来,我们分析Butterknife库运行时代码的工作流程。

3.1 视图绑定

当我们在Activity、Fragment或View中调用Butterknife的bind方法时,就开始了视图绑定的过程。以Activity为例:

public final class ButterKnife {
    @NonNull
    public static Unbinder bind(@NonNull Activity target) {
        return bind(target, target);
    }

    @NonNull
    static Unbinder bind(@NonNull Object target, @NonNull Object source) {
        Class<?> targetClass = target.getClass();
        try {
            // 根据目标类名获取对应的绑定类
            Constructor<? extends Unbinder> constructor = findBindingClass(targetClass).getConstructor(target.getClass(), source.getClass());
            // 实例化绑定类
            return constructor.newInstance(target, source);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Unable to find binding constructor for " + targetClass.getName(), e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to access binding constructor for " + targetClass.getName(), e);
        } catch (InstantiationException e) {
            throw new RuntimeException("Unable to instantiate binding for " + targetClass.getName(), e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Unable to create binding instance for " + targetClass.getName(), e.getCause());
        }
    }

    @Nullable
    private static Class<? extends Unbinder> findBindingClass(Class<?> cls) {
        // 根据类名生成绑定类名
        String clsName = cls.getName();
        if (clsName.startsWith("android.")) {
            throw new RuntimeException("Unable to bind views for android framework classes.");
        }
        try {
            // 尝试加载绑定类
            return (Class<? extends Unbinder>) Class.forName(clsName + "$$ViewBinder");
        } catch (ClassNotFoundException e) {
            return null;
        }
    }
}

在bind方法中,首先获取目标类(如Activity)的Class对象。然后通过findBindingClass方法根据目标类名生成并尝试加载对应的绑定类(即编译时生成的类)。如果找到绑定类,通过反射获取其构造函数并实例化,返回一个Unbinder对象,该对象包含了视图绑定和事件处理的逻辑。

3.2 解除绑定

当不再需要绑定的视图时,我们可以通过调用Unbinder的unbind方法来解除绑定,释放资源。

public interface Unbinder {
    void unbind();
}

// 编译时生成的绑定类实现Unbinder接口
public final class MainActivity$$ViewBinder implements Unbinder {
    private MainActivity target;

    public MainActivity$$ViewBinder(MainActivity target, View source) {
        this.target = target;
        // 进行视图绑定和事件处理
        target.button = source.findViewById(R.id.button);
        target.button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                target.onButtonClick();
            }
        });
    }

    @Override
    public void unbind() {
        MainActivity target = this.target;
        if (target == null) {
            throw new IllegalStateException("Bindings already cleared.");
        }
        // 置空视图引用,防止内存泄漏
        target.button = null;
        this.target = null;
    }
}

在编译时生成的绑定类中,实现了Unbinder接口的unbind方法。在unbind方法中,将绑定的视图引用置空,这样在垃圾回收时就可以回收相关资源,防止内存泄漏。

4. Butterknife源码中的细节处理

除了上述核心功能的实现,Butterknife源码中还有一些细节处理也值得我们关注。

4.1 错误处理

在编译时注解处理器和运行时代码中,都有完善的错误处理机制。在编译时,通过Messager记录错误信息:

private void error(Element element, String message, Object... args) {
    messager.printMessage(Diagnostic.Kind.ERROR, String.format(message, args), element);
}

在运行时,如果找不到绑定类或实例化失败等情况,会抛出相应的运行时异常,方便开发者定位问题。

4.2 性能优化

Butterknife在性能方面也做了一些优化。例如,在编译时生成绑定代码,避免了运行时大量的反射操作,提高了视图绑定和事件处理的效率。同时,在解除绑定时,及时释放资源,防止内存泄漏,保证应用的性能和稳定性。

4.3 兼容性处理

Butterknife库考虑了不同版本Android系统和Java版本的兼容性。在编译时注解处理器中,通过指定支持的Java版本和处理不同的注解类型,确保在各种环境下都能正常工作。在运行时代码中,也进行了一些兼容性检查,如在findBindingClass方法中,对Android框架类进行了特殊处理,避免出现不必要的错误。

通过这次对Android Butterknife源码的深入分析,相信大家对它的工作原理有了更清晰的认识。希望这些分析能帮助你在以后的开发中更好地使用和理解这个强大的库。如果你在阅读过程中有任何疑问,欢迎随时和我交流!