ButterKnife源码分析

butterknife注解框架相信很多同学都在用,但是你真的了解它的实现原理吗?那么今天让我们来看看它到底是怎么实现的(注:本文是基于butterknife:8.5.1版本进行分析)。

前言

先来看看一些预备知识

java注解

java有三类注解,通过元注解@Retention来标识:

  • RetentionPolicy.SOURCE:源码级别解析,例如@Override,@SupportWarnngs,这里注解在编译成功后就不会再起作用,并且不会出现在.class中。

  • RetentionPolicy.CLASS:编译时解析,默认的解析方式,会保留在最终的class中,但无法再运行时获取。

  • RetentionPolicy.RUNTIME:运行时注解,会保留在最终的class中,这类注解可以用反射API中getAnnotations()获取到。

编译时注解

编译时注解是注解强大的地方之一,你可以用它来帮你生成java代码去处理一些逻辑,避免了用反射解析所带来的性能的开销。ButterKnife的核心思想正是用编译时注解。

ButterKnife解析

从Bind开始

众所周知,使用butterknife时首先需要在Activity#onCreate方法中添加这么一句代码

ButterKnife.bind(this);

那么我们就从此方法入手,以下是Butterknife.bind()的源码:

@NonNull @UiThread
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);

    try {
      return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
     //...... 省略
    }
}

@Nullable @CheckResult @UiThread
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 = Class.forName(clsName + "_ViewBinding");
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
      
    } catch (ClassNotFoundException e) {
      //...... 省略
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

首先,bind方法中调用了createBinding(...)方法,而在createBinding()方法中得到了一个Unbinder的构造器,并实例化此类。可以发现这个Unbinder的构造器是通过findBindingConstructorForClass这个方法获取的,那就进入此方法中瞧瞧。可以看到此方法中首先从BINDINGS中获取unbinder类构造器,如果有的话直接返回,没有则通过反射得到此类构造器,先在BINDINGS中存放一份,然后在将其返回。其实这里BINDINGS的作用是缓存通过反射得到的类。避免多次通过反射获取所带来的性能开销。

bind方法暂时就分析到这里,其实要是查看ButterKnife这个类会发现,bind方法有很多重载方法,其中有针对View的,有针对Dialog的等等作用都一样,就是要利用传入的target来实例化一个类名为target_ViewBinding类的对象。那么target_ViewBinding这个类是从哪里来的呢?作用有是什么呢?

@BindView(id)注解

使用butterknife的好处是不用程序员手动findViewById去找对应的View,而只需增加一个@BindView(id)即可,那么butterknife是怎么通过@BindView注解来将对应的View初始化的呢?

让我们先来看看此注解的代码:

@Retention(CLASS) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

通过以上代码可以看到,BindView注解是一个编译时注解(@Retention(CLASS)),并且只能作用在属性上(@Target(FIELD))。

如果你对java注解有一定的了解,那你就一定知道,编译时注解最终都是有AbstractProcessor的子类来处理的,那我们就找到对应的类为ButterKnifeProcessor类。

先来看看process方法:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

      JavaFile javaFile = binding.brewJava(sdk);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }

这个方法有两个作用,第一是解析所有注解findAndParseTargets(env),第二是生成java代码。

  1. 解析所有注解findAndParseTargets(env)
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
    //省略代码...
    // Process each @BindView element.
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      try {
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
   //省略代码...
    return bindingMap;
}

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

    //省略代码 验证 1、被注解的属性不能是private或static。2、验证被注解的属性必须是View的子类

    // Assemble information on the field.
    int id = element.getAnnotation(BindView.class).value();

    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder != null) {
      String existingBindingName = builder.findExistingBindingName(getId(id));
      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 {
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }

    String name = element.getSimpleName().toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    //将被注解的属性封装成FieldViewBinding存储在buildSet中
    builder.addField(getId(id), new FieldViewBinding(name, type, required));

    // 将绑定变量所在的类添加到待unBind序列中。
    erasedTargetNames.add(enclosingElement);
  }

这部分代码很容易懂,解析所有被@BindView所注解的Element(肯定是Field),并处理它。具体解析代码在parseBindVIew方法中。首先校验属性的合法性:

  • 被注解的属性不能是private或static
  • 被注解的属性必须是View的子类
  • 属性与id必须一一对应

其次是将属性封装成FieldViewBinding对象,存储在BindingSet中。那么BindingSet又是个什么玩意呢?

static final class Builder {
    private final TypeName targetTypeName;
    private final ClassName bindingClassName;
    private BindingSet parentBinding;
    private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>();
    //省略代码
    void addField(Id id, FieldViewBinding binding) {
      getOrCreateViewBindings(id).setFieldBinding(binding);
    }

    private ViewBinding.Builder getOrCreateViewBindings(Id id) {
      ViewBinding.Builder viewId = viewIdMap.get(id);
      if (viewId == null) {
        viewId = new ViewBinding.Builder(id);
        viewIdMap.put(id, viewId);
      }
      return viewId;
    }

    BindingSet build() {
      //省略代码      
      return new BindingSet(targetTypeName, bindingClassName, isFinal, isView, isActivity, isDialog,
          viewBindings.build(), collectionBindings.build(), resourceBindings.build(),
          parentBinding);
    }
  }

这里贴出来了BindingSet的构造器Builder,可以看出此类包装了被注解的类的信息以及注解的所有的属性,viewIdMap中存储了所有的注解字段,这些字段都被封装成ViewBinding(其实就是将id和属性名包装成一个对象)。最后由BindingSet负责生成java代码。

static Builder newBuilder(TypeElement enclosingElement) {
    //省略代码
    String packageName = getPackage(enclosingElement).getQualifiedName().toString();
    String className = enclosingElement.getQualifiedName().toString().substring(
        packageName.length() + 1).replace('.', '$');
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
}

可以看到构造Builder时会传入包名,类名,而这个类名 className_ViewBinding就是最终被生成的类的名称。还记得在bind方法中会通过反射去实例化一个target_ViewBinding类的对象吧,这个类其实就是这里通过注解来自动生成的。到这里butterknife的原理就分析完了,下面我们来看看如何生成java代码。

2.使用javapoet生成java代码

再次介绍一个大杀器 javapoet,square出品的生成.java文件的Java API。通过清晰的语法来生成一个文件结构,无敌。
那么我们回到process方法中查看。

JavaFile javaFile = binding.brewJava(sdk);
javaFile.writeTo(filer);

这里主要逻辑在binding.brewJava()方法中。

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

这里builder的参数第一个是包名。第二个是TypeSpec对象,就是要生成的类的信息。

private TypeSpec createType(int sdk) {

    //获取一个builder对象,将类的修饰符设置为public,如果允许是final时,设置为final
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    if (isFinal) {
      result.addModifiers(FINAL);
    }

    ......
    //添加属性
    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));

    //添加一个unbind方法
    if (hasViewBindings() || parentBinding == null) {
      result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
  }

在这个方法中可以看到最终生成的类的相关信息,最主要的给属性初始化的代码是在createBindingConstructor(sdk) 这个方法中。

private MethodSpec createBindingConstructor(int sdk) {
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC);
    ......
    //绑定View属性
    if (hasViewBindings()) {
      ......
      for (ViewBinding binding : viewBindings) {
        addViewBinding(constructor, binding);
      }
      ......
    }
    ......
    return constructor.build();
  }
  
  private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
    if (binding.isSingleFieldBinding()) {
      // Optimize the common case where there's a single binding directly to a field.
      FieldViewBinding fieldBinding = binding.getFieldBinding();
      CodeBlock.Builder builder = CodeBlock.builder()
          .add("target.$L = ", fieldBinding.getName());

      boolean requiresCast = requiresCast(fieldBinding.getType());
      if (!requiresCast && !fieldBinding.isRequired()) {
        builder.add("source.findViewById($L)", binding.getId().code);
      } else {
        builder.add("$T.find", UTILS);
        builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
        if (requiresCast) {
          builder.add("AsType");
        }
        builder.add("(source, $L", binding.getId().code);
        if (fieldBinding.isRequired() || requiresCast) {
          builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
        }
        if (requiresCast) {
          builder.add(", $T.class", fieldBinding.getRawType());
        }
        builder.add(")");
      }
      result.addStatement("$L", builder.build());
      return;
    }

    ......

    addFieldBinding(result, binding);
    addMethodBindings(result, binding);
  }

这两个方法主要就是生成 通过findViewById方法去找到对应的View 的代码。可以看到注解库最终还是调用的findViewById来查找View的。

到这里ButterKnife注解库就分析完了。下面贴一段最终生成的代码瞧瞧:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131427417;

  @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;
    target.txt1 = Utils.findRequiredViewAsType(source, R.id.id1, "field 'txt1'", TextView.class);
    view = Utils.findRequiredView(source, R.id.btn, "method 'start'");
    view2131427417 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.start();
      }
    });
  }

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

    target.txt1 = null;
    
    view2131427417.setOnClickListener(null);
    view2131427417 = null;
  }
}

总结:首先通过注解来获取对应类中所有要初始化的属性,通过processor编译生成对应的.java的类文件。这个类文件中会在构造器中通过调用View的findViewById方法去初始化所有的属性(注意:到这里只是生成了.java类文件,并没有和目标类绑定)。这个编译时生成的类的构造器中需要传入对应的目标类作为参数。因此在目标类初始化时需要调用ButterKnife.bind(this)方法类进行绑定。在这个bind方法中会通过反射得到编译时才生成的类的对象,这样就和目标类进行了绑定,也就是初始化目标类中的被注解的属性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 博文出处:ButterKnife源码分析,欢迎大家关注我的博客,谢谢! 0x01 前言 在程序开发的过程中,总会有...
    俞其荣阅读 2,023评论 1 18
  • 主目录见:Android高级进阶知识(这是总目录索引) 前面我们已经讲完[编译期注解的使用例子]大家应该对这个流程...
    ZJ_Rocky阅读 1,481评论 0 8
  • 引言 在Android开发中我们会用到ButterKnife框架来简化绑定layout中的视图。这里我们主要分析B...
    伍零一阅读 296评论 0 1
  • butterknife是一个Android View和Callback注入框架,相信很多人都在使用,可以减少很多代...
    不二先生的世界阅读 274评论 0 1
  • 此刻他的思想,已经悄悄地溜到了双重思想的迷幻世界中去了:明明知道,却佯装不知;本来对事实心知肚明,却偏要费劲心机去...
    木卯丁阅读 283评论 0 1