博客主页

ButterKnife(黄油刀)是控件注入框架,可以帮助安卓开发者省去初始化控件的重复性工作,简单快捷的初始化布局文件中的控件,极大的提升开发效率。

ButterKnife框架有很多优化:

  • 强大的View绑定和Click事件处理功能,简化代码,提升开发效率
  • 方便处理Adapter中的ViewHolder绑定问题
  • 运行时不会影响App效率,使用配置方便
  • 代码清晰,可读性强

与IOC架构主要区别有:

  • 共同特点:都实现了解耦的目的
  • 核心技术:IOC框架运行时通过反射技术(reflect),ButterKnife框架通过注解处理器技术(APT)
  • 开发使用:两者几乎一样
  • 代码难易:IOC编码更具有挑战性
  • 程序稳定:两者都未发现致命的缺陷
  • 两者缺陷:reflect会消耗一定性能,APT会增加APK的大小
  • 开发追求:更偏向编译期的APT技术

ButterKnife源码地址

ButterKnife基本使用

ButterKnife详细使用地址

  1. 在build.gradle文件中添加依赖:
android {
  ...
  // Butterknife requires Java 8.
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
  implementation 'com.jakewharton:butterknife:10.2.0'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
}

如果是使用kotlin语言,将annotationProcessor替换为kapt

  1. 如果想在Android Library中使用ButterKnife,在项目根build.gradle中添加buildScript:
buildscript {
  repositories {
    mavenCentral()
    google()
   }
  dependencies {
    classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.0'
  }
}

然后在项目module中的build.gradle中apply

apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'

最后在ButterKnife注解中使用R2替换R

class ExampleActivity extends Activity {
  @BindView(R2.id.user) EditText username;
  @BindView(R2.id.pass) EditText password;
...
}
  1. 在字段上通过@BindView注解,代替findViewById。在Activity中setContentView方法后调用ButterKnife.bind(this)
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...
  }
}

ButterKnife源码原理分析

ButterKnife框架不是使用反射技术实现的,而是通过APT技术生成委托类代码,通过委托类查找试图中的View。以上例子生成的代码如下:

public class ExampleActivity_ViewBinding implements Unbinder {
  private ExampleActivity target;

  private View view7f0700af;

  @UiThread
  public ExampleActivity_ViewBinding(ExampleActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public ExampleActivity_ViewBinding(final ExampleActivity target, View source) {
    this.target = target;

    View view;
    view = Utils.findRequiredView(source, R.id.title, "field 'title' and method 'jumpTestActivity'");
    target.title = Utils.castView(view, R.id.title, "field 'title'", TextView.class);
    view7f0700af = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.jumpTestActivity();
      }
    });
    target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
    target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);
  }
}

然后调用ButterKnife.bind(this)加载上面生成的代码。接下来看下bind方法源码实现:

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

bind方法需要在setContentView方法之后调用,否则sourceView为null。

public static Unbinder bind(@NonNull Object target, @NonNull View source) {
  // 获取目标的Class类,如上面示例中:ExampleActivity
  Class<?> targetClass = target.getClass();
  if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
  // 找到目标类的构造,如:ExampleActivity_ViewBinding类的构造
  Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

  if (constructor == null) { // 如果没有找到,直接返回空实现的委托类
    return Unbinder.EMPTY;
  }

  //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
  try {
    // 通过反射创建目标类的实例,也是通过APT生成的ExampleActivity_ViewBinding类实例
    return constructor.newInstance(target, source);
  } catch (Exception e) {
     // ...
  }
}

首先通过绑定目标类的查找由APT生成的类的构造,然后创建目标类的实例对象。例如:创建生成的ExampleActivity_ViewBinding类

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
  // 先从缓存中获取,如果存在直接返回
  Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
  if (bindingCtor != null || BINDINGS.containsKey(cls)) {
    if (debug) Log.d(TAG, "HIT: Cached in binding map.");
    return bindingCtor;
  }
  
  // 如果目标类不是自己定义的,是JDK或者SDK中的类直接返回null
  String clsName = cls.getName();
  if (clsName.startsWith("android.") || clsName.startsWith("java.")
      || clsName.startsWith("androidx.")) {
    if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
    return null;
  }
  try {
    // 加载目标类的Class,如:ExampleActivity_ViewBinding类的Class
    Class<?> bindingClass = cls.getClassLoader().loadClass(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;
}

通过反射技术加载目标类的构造。先是从缓存中获取,如果找到直接返回,否则反射获取并放入到缓存中。

ButterKnife编译时注解

运行时注解由于性能问题被一些人所诟病,主要是通过反射技术实现,而编译时注解核心原理是通过APT(Annotation Processing Tool)实现的。

使用编译时注解的第三方框架有很多,如:ButterKnife、Dragger、Retrofit、ARouter等。而ButterKnife这个库是针对View,资源ID,部分事件等进行注解的开源库,它能够去除掉一些不怎么雅观的样板式代码,使得我们的代码更加简洁,易于维护,同时使用APT技术也使得它的效率得到保证。

这篇文章组件化已经详细介绍了APT技术,这里不在重复了。

ButterKnife框架Coding

新建一个java libary库,在build.gradle文件中添加依赖:

dependencies {
    implementation project(path: ':annotation')

    compileOnly 'com.google.auto.service:auto-service:1.0-rc6'
    implementation 'com.google.auto:auto-common:0.10'
    api 'com.squareup:javapoet:1.11.1'
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc6'
}

创建ButterKnifeProcessor类继承AbstractProcessor,并指定JDK编译版本、支持的注解类型等

// 用来生成 META-INF/services/javax.annotation.processing.Processor文件
@AutoService(Processor.class)
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_8)
// 允许/支持的注解类型,让注解处理器处理
@SupportedAnnotationTypes({"com.todo.butterknife.annotation.BindView", "com.todo.butterknife.annotation.OnClick"})
public class ButterKnifeProcessor extends AbstractProcessor { }

接下来初始化一些工具类:

// 操作Element工具类(类、函数、属性都是操作Element工具类)
private Elements elementUtils;

// Messager用来报告错误,警告和其他提示信息
private Messager messager;

// Types类信息工具类,包含操作TypeMirror的工具方法
private Types typeUtils;

// 文件生成器,Filer用来创建新的类文件,class文件及辅助文件
private Filer filer;

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    elementUtils = processingEnv.getElementUtils();
    messager = processingEnv.getMessager();
    typeUtils = processingEnv.getTypeUtils();
    filer = processingEnv.getFiler();
    messager.printMessage(Diagnostic.Kind.NOTE, "<<<<<ButterKnifeProcessor##INIT>>>>>");
}

接下来处理支持的注解

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    if (set.isEmpty()) return false;

    Set<? extends Element> elementsAnnotatedWithBindView = roundEnvironment.getElementsAnnotatedWith(BindView.class);

    Set<? extends Element> elementsAnnotatedWithOnClick = roundEnvironment.getElementsAnnotatedWith(OnClick.class);

    if ((elementsAnnotatedWithBindView != null && !elementsAnnotatedWithBindView.isEmpty())
            || (elementsAnnotatedWithOnClick != null && !elementsAnnotatedWithOnClick.isEmpty())) {
        // 缓存满足条件被注解的元素
        fillElementMap(elementsAnnotatedWithBindView, elementsAnnotatedWithOnClick);

        try {
            // 创建文件
            createFile();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return true;
    }
    return false;
}

缓存被@BindView和@OnClick注解的元素

private Map<TypeElement, ElementSet> cacheMap = new HashMap<>();

private void fillElementMap(
        Set<? extends Element> elementsAnnotatedWithBindView,
        Set<? extends Element> elementsAnnotatedWithOnClick
) {

    if (elementsAnnotatedWithBindView != null && !elementsAnnotatedWithBindView.isEmpty()) {

        for (Element element : elementsAnnotatedWithBindView) {
            messager.printMessage(Diagnostic.Kind.NOTE, "被@BindView注解的元素::" + element.getSimpleName());
            // 判断元素种类是否是字段
            if (element.getKind() == ElementKind.FIELD) {
                VariableElement fieldElement = (VariableElement) element;
                // 注解在属性上,属性节点的父节点是类节点
                TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

                // 如果缓存Map中包含指定的类节点,直接加入到缓存中
                if (cacheMap.containsKey(enclosingElement)) {
                    cacheMap.get(enclosingElement).variableElements.add(fieldElement);
                } else {
                    ElementSet set = new ElementSet();
                    set.variableElements.add(fieldElement);
                    cacheMap.put(enclosingElement, set);
                }
            }
        }
    }


    if (elementsAnnotatedWithOnClick != null && !elementsAnnotatedWithOnClick.isEmpty()) {
        for (Element element : elementsAnnotatedWithOnClick) {
            messager.printMessage(Diagnostic.Kind.NOTE, "被@OnClick注解的元素::" + element.getSimpleName());

            if (element.getKind() == ElementKind.METHOD) {
                ExecutableElement methodElement = (ExecutableElement) element;
                TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

                if (cacheMap.containsKey(enclosingElement)) {
                    cacheMap.get(enclosingElement).executableElements.add(methodElement);
                } else {
                    ElementSet set = new ElementSet();
                    set.executableElements.add(methodElement);
                    cacheMap.put(enclosingElement, set);
                }
            }
        }
    }
}

ElementSet的结构为:

public class ElementSet {
    // 被@BindView注解的属性集合
    public List<VariableElement> variableElements = new ArrayList<>();
    // 被@OnClick注解的方法集合
    public List<ExecutableElement> executableElements = new ArrayList<>();
}

最后一步就是创建文件

private void createFile() throws IOException {
    if (cacheMap.isEmpty()) return;

    TypeElement unBinderElement = elementUtils.getTypeElement("com.todo.butterknife.api.UnBinder");
    TypeElement debouncingOnClickListenerElement = elementUtils.getTypeElement("com.todo.butterknife.api.DebouncingOnClickListener");
    TypeElement viewElement = elementUtils.getTypeElement("android.view.View");

    for (Map.Entry<TypeElement, ElementSet> entry : cacheMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        ElementSet elementSet = entry.getValue();

        ParameterSpec targetParameterSpec = ParameterSpec.builder(
                ClassName.get(typeElement), "target", Modifier.FINAL
        ).build();

        // public void bind(ExampleActivity target)
        MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("bind")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(targetParameterSpec);

        if (!elementSet.variableElements.isEmpty()) {
            for (VariableElement element : elementSet.variableElements) {
                // target.title = target.findViewById(R.id.title);
                String fieldName = element.getSimpleName().toString();
                int resId = element.getAnnotation(BindView.class).value();
                String format = "$N." + fieldName + " = $N.findViewById($L)";
                bindMethodBuilder.addStatement(format, "target", "target", resId);
            }
        }

        if (!elementSet.executableElements.isEmpty()) {
            for (ExecutableElement element : elementSet.executableElements) {
//                        target.findViewById(R.id.title).setOnClickListener(new DebouncingOnClickListener() {
//                            @Override
//                            protected void doClick(View v) {
//                                target.jumpTestActivity();
//                            }
//                        });


                String methodName = element.getSimpleName().toString();

                int resId = element.getAnnotation(OnClick.class).value();

                bindMethodBuilder.beginControlFlow("$N.findViewById($L).setOnClickListener(new $T()",
                        "target", resId, ClassName.get(debouncingOnClickListenerElement))
                        .beginControlFlow("protected void doClick($T v)", ClassName.get(viewElement))
                        .addStatement("$N." + methodName + "()", "target")
                        .endControlFlow()
                        .endControlFlow(")");
            }
        }

        MethodSpec bindMethod = bindMethodBuilder.build();

        ClassName className = ClassName.get(typeElement);

        String packageName = className.packageName();

        // ExampleActivity_ViewBinder
        String finalClassName = className.simpleName() + "_ViewBinder";

        messager.printMessage(Diagnostic.Kind.NOTE, "最终生成的类文件:" + packageName + "." + finalClassName);

        ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(ClassName.get(unBinderElement), className);

        // public class ExampleActivity_ViewBinder implements UnBinder<ExampleActivity>
        TypeSpec finaClass = TypeSpec.classBuilder(finalClassName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addSuperinterface(parameterizedTypeName)
                .addMethod(bindMethod).build();

        JavaFile.builder(
                packageName, finaClass
        ).build().writeTo(filer);
    }
}

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)


小兵兵同学
56 声望23 粉丝

Android技术分享平台,每个工作日都有优质技术文章分享。从技术角度,分享生活工作的点滴。