ButterKnife源码初探

Butterknife是一个Android View注入框架,配合Android ButterKnife Zelezny插件能够快速的完成View的初始化操作。相比较之前findViewById()方式来获取view的方式,它不仅简洁了代码,提高了编码效率,而且还避免了大量的强转。本文基于ButterKnife7.0.1版本进行分析。

1,ButterKnife的引入和使用

build.gradle文件下添加butterknife的依赖

dependencies {
    implementation'com.jakewharton:butterknife:7.0.1'
    annotationProcessor 'com.jakewharton:butterknife:7.0.1'
}

需要注意的是我使用的是Android Studio 3.0.1版本,如果你是3.0以下依赖写法不一致,你只需要使用compile'com.jakewharton:butterknife:7.0.1'来添加依赖。

添加完依赖之后,我们来看看ButterKnife的使用

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.show_tip_txt)
    TextView showTipTxt;
    @Bind(R.id.butter_knife_btn)
    Button butterKnifeBtn;

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

    @OnClick({R.id.butter_knife_btn})
    void butterBtn(){
        Toast.makeText(this,"点击了我",Toast.LENGTH_LONG).show();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        ButterKnife.unbind(this);
    }
}

在这里我们看到,ButterKnife.bind(this)需要在setContentView()方法后面,并且@Bind的参数不能用private和static修饰,具体原因后面有分析,ButterKnife是通过@Bind是用来绑定View的Id的,此外它还提供了更多的绑定方法提供给我们,如:BindBool,BindColor,BindDimen,BindDrawable,BindInt,BindString,OnClick等。在这里就不一一的介绍了,读者感兴趣的话,可自行实验。
上面的绑定View在Activity中的使用,其实ButterKnife不仅仅可以在Activity中使用,还可以在View,Dialog中使用,我们看看ButterKnife文件中的定义。

public enum Finder {
    VIEW {
      @Override protected View findView(Object source, int id) {
        return ((View) source).findViewById(id);
      }

      @Override public Context getContext(Object source) {
        return ((View) source).getContext();
      }
    },
    ACTIVITY {
      @Override protected View findView(Object source, int id) {
        return ((Activity) source).findViewById(id);
      }

      @Override public Context getContext(Object source) {
        return (Activity) source;
      }
    },
    DIALOG {
      @Override protected View findView(Object source, int id) {
        return ((Dialog) source).findViewById(id);
      }

      @Override public Context getContext(Object source) {
        return ((Dialog) source).getContext();
      }
    };
    private static  T[] filterNull(T[] views) {
      int end = 0;
      for (int i = 0; i < views.length; i++) {
        T view = views[i];
        if (view != null) {
          views[end++] = view;
        }
      }
      return Arrays.copyOfRange(views, 0, end);
    }

当然在我们不需要的时候,我们还需要解绑操作

 @Override
    protected void onDestroy() {
        super.onDestroy();
        ButterKnife.unbind(this);
    }

2,ButterKnife执行原理

通过上面我们了解到ButterKnife是通过@Bind来绑定View的,先来看看Bind的定义

/**
 * Bind a field to the view for the specified ID. The view will automatically be cast to the field
 * type.
 * 

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

Bind是一个是自定义的annotation文件,需要注意一下它和接口文件的区别:一个@interface ,另外一个interface。
我们先看看Bind上的注解@Rentention,@Target:
@Retention:对Annotation的“生命周期”限制,代表有效期
RetentionPolicy.SOURCE,在源文件中有效
RetentionPolicy.CLASS,在Class文件中有效
RetentionPolicy.RUNTIME,在运行时有效

@Target:规定自定义Annotation所修饰的对象范围
ElementType.CONSTRUCTOR,构造器声明
ElementType.FIELD,成员变量、对象、属性(包括enum实例)
ElementType.LOCAL_VARIABLE,局部变量声明
ElementType.METHOD,方法声明
ElementType.PACKAGE,包声明
ElementType.PARAMETER,参数声明
ElementType.TYPE,类、接口(包括注解类型)或enum声明
默认不设置@Target表示修饰所有。

当你在Build项目工程的时候,
ButterKnife使用了Annotion Processing(注解处理),因此在编译的时候,Annotation会由APT(Annotation Processing Tool)解析。ButterKnife工程中ButterKnifeProcessor类的process()方法会执行以下操作:
A,扫描Java代码中所有的ButterKnife注解@Bind,@OnClick等;
B,当有扫描到为ButterKnife注解的时候,ButterKnifeProcessor会自动生成一个实现了ViewBinder接口的XX$$ViewBinder的java文件,
C,在生成的ViewBinder中,包含了该类所有注解对于的实现,如@Bind会处理成findViewById,@OnClick处理成setOnClickListener等;

在运行代码的时候,遇到ButterKnife.bind()的时候,Butterknife会加载在编译期生成的ViewBinder类,并调用对应的注解如,@Bind等;
在取消注解的时候,会调用到ViewBinder的unbind方法。

public class MainActivity$$ViewBinder implements ViewBinder {
  @Override public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131165287, "field 'showTipTxt'");
    target.showTipTxt = finder.castView(view, 2131165287, "field 'showTipTxt'");
    view = finder.findRequiredView(source, 2131165219, "field 'butterKnifeBtn' and method 'butterBtn'");
    target.butterKnifeBtn = finder.castView(view, 2131165219, "field 'butterKnifeBtn'");
    view.setOnClickListener(
      new butterknife.internal.DebouncingOnClickListener() {
        @Override public void doClick(
          android.view.View p0
        ) {
          target.butterBtn();
        }
      });
  }

  @Override public void unbind(T target) {
    target.showTipTxt = null;
    target.butterKnifeBtn = null;
  }
}

3,ButterKnife 源码解析

既然View是通过ButterKnife.bind(this)方法来加载ViewBinder类,并调用对应注解的,那我们先看bind(this)方法:

 public static void bind(Activity target) {
    bind(target, target, Finder.ACTIVITY);
  }
static void bind(Object target, Object source, Finder finder) {
    Class targetClass = target.getClass();
    try {
      if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
      ViewBinder viewBinder = findViewBinderForClass(targetClass);
      if (viewBinder != null) {
        viewBinder.bind(finder, target, source);
      }
    } catch (Exception e) {
      throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
    }
  }
 
 

我们看到这个方法主要是通过传入的参数,然后去找到对应的viewBinder,并绑定注解,这如我们之前所说的一样。那我们在看看它是怎么找到viewBinder的,现在我们进入findViewBinderForClass(targetClass)方法看看:

private static ViewBinder findViewBinderForClass(Class cls)
      throws IllegalAccessException, InstantiationException {
    ViewBinder viewBinder = BINDERS.get(cls);
    if (viewBinder != null) {
      if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
      return viewBinder;
    }
    String clsName = cls.getName();
    //判断是不是系统文件
    if (clsName.startsWith(ANDROID_PREFIX) || clsName.startsWith(JAVA_PREFIX)) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return NOP_VIEW_BINDER;
    }
    try {
      //加载自动生成的ViewBinder文件
      Class viewBindingClass = Class.forName(clsName + ButterKnifeProcessor.SUFFIX);
      //noinspection unchecked
      viewBinder = (ViewBinder) viewBindingClass.newInstance();
      if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
    } catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      //如果没有找到,就继续查找它父类
      viewBinder = findViewBinderForClass(cls.getSuperclass());
    }
    //放入缓存中
    BINDERS.put(cls, viewBinder);
    return viewBinder;
  }
 
 

通过上面的代码,我们看到先会在BINDERS这个Map里面去查找是否有ViewBinder的缓存,如果找到就直接返回viewBinder,如果没有找到就继续判断类名是不是android,或者java开头,接下来会加载自动生成的ViewBinder文件,如果出现ClassNotFoundException,则会继续查找cls.getSuperclass(),最后将viewBinder放入缓存,并返回viewBinder。

if (viewBinder != null) {
        viewBinder.bind(finder, target, source);
}

bind()方法中拿到了viewBinder就会去调用viewBinder.bind()方法,也就是自动生成的XX$$ViewBinder中的bind方法。

另外,解绑的时候会最终调用

  @Override public void unbind(T target) {
    target.showTipTxt = null;
    target.butterKnifeBtn = null;
  }

分析到这里,基本的绑定/解绑也就搞明白了,现在我们再来看看是如果生成XX$$ViewBinder的方法的。

4,ButterKnife生成XX$$ViewBinder文件

ButterKnife中ButterKnifeProcessor去继承了AbstractProcessor

public final class ButterKnifeProcessor extends AbstractProcessor {}

Annotaion process tool会在编译期自动的查找所有继承了AbstractProcessor类的文件,然后调用它们的process方法去生成java文件。我们先看看,ButterKnife工程中ButterKnifeProcessor类的process()的方法:

  @Override public boolean process(Set elements, RoundEnvironment env) {
    //查找并解析注解
    Map targetClassMap = findAndParseTargets(env);

    for (Map.Entry 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;
  }

这个方法中,先查找并解析注解,然后再写入文件,那我们先看看findAndParseTargets()方法:

  private Map findAndParseTargets(RoundEnvironment env) {
    //TypeElement 代表等待注入的类元素,BindingClass 该类包含待注入元素的集合
    Map targetClassMap = new LinkedHashMap();
    Set erasedTargetNames = new LinkedHashSet();

    // Process each @Bind element.处理@Bind
    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 listener : LISTENERS) {
      findAndParseListener(env, listener, targetClassMap, erasedTargetNames);
    }

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

    //...省略其他代码

    // Try to find a parent binder for each.尝试去找到父类的绑定
    for (Map.Entry entry : targetClassMap.entrySet()) {
      String parentClassFqcn = findParentFqcn(entry.getKey(), erasedTargetNames);
      if (parentClassFqcn != null) {
        entry.getValue().setParentViewBinder(parentClassFqcn + SUFFIX);
      }
    }

    return targetClassMap;
  }

这个方法,主要去处理不同类型的绑定,以及他们父类的绑定。在这里我们看看,parseBind():

private void parseBind(Element element, Map targetClassMap,
      Set 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);
    }
  }

这个方法里面有两个很重要的判断isInaccessibleViaGeneratedCode(),该方法判断了
1、修饰符不能为private或static;
2、不能用于非Class类;
3、当前类修饰符不能为private

private boolean isInaccessibleViaGeneratedCode(Class annotationClass,
      String targetThing, Element element) {
    boolean hasError = false;
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Verify method modifiers.
    Set modifiers = element.getModifiers();
    //修饰符不能为private或static
    if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {
      error(element, "@%s %s must not be private or static. (%s.%s)",
          annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
          element.getSimpleName());
      hasError = true;
    }

    // 不能用于非Class类
    if (enclosingElement.getKind() != CLASS) {
      error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",
          annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
          element.getSimpleName());
      hasError = true;
    }

    // Verify containing class visibility is not private.
    //当前类修饰符不能为private
    if (enclosingElement.getModifiers().contains(PRIVATE)) {
      error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)",
          annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
          element.getSimpleName());
      hasError = true;
    }

    return hasError;
  }

在isBindingInWrongPackage()方法中校验类不能以“android.”或者“"java.”开头;

 public static final String ANDROID_PREFIX = "android.";
  public static final String JAVA_PREFIX = "java.";
private boolean isBindingInWrongPackage(Class annotationClass,
      Element element) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    String qualifiedName = enclosingElement.getQualifiedName().toString();

    if (qualifiedName.startsWith(ANDROID_PREFIX)) {
      error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
          annotationClass.getSimpleName(), qualifiedName);
      return true;
    }
    if (qualifiedName.startsWith(JAVA_PREFIX)) {
      error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
          annotationClass.getSimpleName(), qualifiedName);
      return true;
    }
    return false;
  }

回过头去再看看parseBind()方法,里面有parseBindMany()这是绑定的List或者Array,还有一个parseBindOne(),我们来看看:

  private void parseBindOne(Element element, Map targetClassMap,
      Set 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 bindingClass = targetClassMap.get(enclosingElement);
    if (bindingClass != null) {
      ViewBindings viewBindings = bindingClass.getViewBinding(id);
      if (viewBindings != null) {
        Iterator 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);
    // 再将带有@Bind注解的元素信息添加进BindingClass
    bindingClass.addField(id, binding);

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

先获取BindingClass,然后再将带有@Bind注解的元素添加进BindingClass。
我们再来看看获取BindingClass的方法getOrCreateTargetClass

  private BindingClass getOrCreateTargetClass(Map 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);
      //process方法中一路传递过来targetClassMap,添加BindingClass
      targetClassMap.put(enclosingElement, bindingClass);
    }
    return bindingClass;
  }

这个方法内会再一次判断通过targetClassMap获取的BindingClass是否为空,为空的时候会从新new一个BindingClass,并再次添加到targetClassMap。这里的targetClassMap就是process方法传递过来的,最后返回process。到这里ButterKnife的大概流程就完结了。

总结:

ButterKnife使用了注解的方式,在编译的时候,Annotaion process tool会在编译期自动的查找ButterKnifeProcessor文件,然后调用了它的process方法去生成XX$$ViewBinder.java文件。在执行ButterKnife.bind(this)操作的时候,会先去BINDERS(key为class,Value为ViewBinder)中查找,如果没有缓存,则会通过反射的方式去拿到在编译期生成的ViewBinder,真正执行的绑定的操作就是在ViewBinder中。ButterKnife相比与其他的View注入框架,它使用的是注解的方式,虽然在获取生成的ViewBinder类使用的是反射,但是有缓存,这使得ButterKnife执行效率较高。此外,@Bind()等注解不能使用private修饰,是它因为根据Id来获取View,如果使用private就只能通过反射拿到,这会很消耗性能。

你可能感兴趣的:(ButterKnife源码初探)