ButterKnife原理及源码浅析

知其然知其所以然

ButterKnife使用Java Annotation Processing技术,在Java代码编译成Java字节码的时候处理注解@BindView、@OnClick、@BindXXX(ButterKnife支持的注解:在Butterknife-annotations包下)生成对应的ViewBinding类,在这个类中进行资源和视图的绑定操作.


ButterKnife流程图

现在大致流程我们已经清楚了,那么源码中到底是如何实现的呢?
我们先看下源码工程的目录(ButterKnife版本:8.8.1):


ButterKnife工程目录
Module对应的职责如下:
  • butterknife: android library model 提供android使用的API
  • butterknife-annotations: java-model,使用时的注解
  • butterknife-compiler: java-model,编译时用到的注解的处理器
  • butterknife-gradle-plugin: 自定义的gradle插件,辅助生成有关代码
  • butterknife-integration-test: 项目的测试用例
  • butterknife-lint:项目的lint检查
首先我们先看下butterknife的构成

看起来真是简洁啊~ 我们来依次看下吧~

DebouncingOnClickListener

这是一个抽象类实现了View.OnClickListener并做了相应的处理来消除同一帧中发布的多个点击,当点击一个按钮将禁用该框架的所有按钮。

    public abstract class DebouncingOnClickListener implements View.OnClickListener {
      static boolean enabled = true;

      private static final Runnable ENABLE_AGAIN = new Runnable() {
        @Override public void run() {
          enabled = true;
        }
      };
    
      @Override public final void onClick(View v) {
        if (enabled) {
          enabled = false;
          v.post(ENABLE_AGAIN);
          doClick(v);
        }
      }
    
      public abstract void doClick(View v);
    }

ImmutableList

这是一个被final修饰继承AbstractList方法的不可变轻量集合类,因为在实际使用时需要的方法过少,所有没有必要去使用ArrayList,它的实现也很简单。

final class ImmutableList<T> extends AbstractList<T> implements RandomAccess {
  private final T[] views;

  ImmutableList(T[] views) {
    this.views = views;
  }

  @Override public T get(int index) {
    return views[index];
  }

  @Override public int size() {
    return views.length;
  }

  @Override public boolean contains(Object o) {
    for (T view : views) {
      if (view == o) {
        return true;
      }
    }
    return false;
  }
}

Utils

这个类看名字就知道是个工具类,它的作用主要就是获取资源和类型强转,代码就省略了,参考价值不大...

ButterKnife相关方法解析

  • ButterKnife.bind()方法

    • bind(Activity target):这个方法会先根据Activity取得decorView,然后再调用createBinding(target,decorView)方法。
    • bind(View target): 这个方法比较特殊,它会将视图及其子视图用作视图根,调用createBinding(target,target)方法。
    • bind(Dialog target):这个方法会先根据Dialog取得decorView,然后再调用createBinding(target,decorView)方法。
    • bind(Object target, Activity source):同bind(Activity target)。
    • bind(Object target, Dialog source):同bind(Dialog target)
    • bind(Object target, View source):直接调用createBinding(target,source)方法。

    参数中的target其实就是发生视图绑定的地方,也就是我们使用@BindXXX注解所在的类。所以在ViewHodler中调用的方法应该是最后一个

  • ButterKnife.createBinding()方法
    在前面的bind方法中最后都调用了这个方法来结束,那这个方法中到底有什么呢?

/**
 *
 * @param target
 * @param source target所在的根view或者自身(if target == view)
 * @ret urn
 */
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
  Class<?> targetClass = target.getClass();
  if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
  Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

  if (constructor == null) {
    return Unbinder.EMPTY;
  }

  //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
  try {
    return constructor.newInstance(target, source);
  } catch (IllegalAccessException e) {
    throw new RuntimeException("Unable to invoke " + constructor, e);
  } catch (InstantiationException e) {
    throw new RuntimeException("Unable to invoke " + constructor, e);
  } catch (InvocationTargetException e) {
    Throwable cause = e.getCause();
    if (cause instanceof RuntimeException) {
      throw (RuntimeException) cause;
    }
    if (cause instanceof Error) {
      throw (Error) cause;
    }
    throw new RuntimeException("Unable to create binding instance.", cause);
  }
}

@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
  //从缓存里寻找是否已经绑定过
  Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
  if (bindingCtor != null) {
    if (debug) Log.d(TAG, "HIT: Cached in binding map.");
    return bindingCtor;
  }

  //类名检测
  String clsName = cls.getName();
  if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
    if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
    return null;
  }

  try {
    //加载对应的Class_ViewBinding类文件
    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;
}

首先获取到target的class,然后根据class去缓存中寻找是否已经绑定过,绑定过就直接返回,否则取得类名并检查类名是否是android.或者java.开头的,如果是的话就直接返回null(ps:这里是禁止去接触framework层的代码),检查通过之后就会去寻找在编译以后生成的对应XX_ViewBinding类文件通过反射调用构造方法来进行绑定。如果没有找到对应的XX_ViewBinding类文件会在target的父类中继续寻找直到找到或者失败抛异常。最终如果绑定成功以后会加入缓存池中并返回。

接着我们先看下butterknife-annotations的构成
butterknife支持的全部注解

不得不说这些注解已经能够基本满足我们日常工作了。那我就挑几个大家不常用或者不熟悉的注解来说下怎么使用吧~

@OnCheckedChanged

这个注解的作用主要是在android.widget.CompoundButton上,但是当你去看源码时发现这只是个抽象类,那么它的实现类到底是谁?在官网上我们看到分别是:CheckBox,RadioButton,Switch,ToggleButton



这个注解对应的替代的方法就是setOnCheckedChangeListener,使用如下:

 @OnCheckedChanged(R.id.example) void onChecked(boolean checked) {
    Toast.makeText(this, checked ? "Checked!" : " Unchecked!",Toast.LENGTH_SHORT).show();  
  }
@OnTextChanged

这个注解是用来替代addTextChangedListener。以前我们在对EditText进行文本变化监听时不得不重写TextChangedListener的三个方法,但是很多时候我们只需要用到其中一个方法。这个时候使用这个注解就能满足我们的要求,注解内部使用枚举变量分别三个方法,我们在使用注解时传入对应的枚举变量对应的方法就会被回调,使用方法如下:

//因为在注解定义时默认返回TEXT_CHANGED 所以callback = TEXT_CHANGED时可以省略
@OnTextChanged(R.id.example) void onTextChanged(CharSequence text) {
   Toast.makeText(this, "Text changed: " + text, Toast.LENGTH_SHORT).show();
}
@OnTextChanged(value = R.id.example, callback = BEFORE_TEXT_CHANGED)
  void onBeforeTextChanged(CharSequence text) {
    Toast.makeText(this, "Before text changed: " + text,Toast.LENGTH_SHORT).show();
}

@OnTextChanged(value = R.id.example, callback = AFTER_TEXT_CHANGED)
  void onAfterTextChanged(CharSequence text) {
    Toast.makeText(this, "Before text changed: " + text,Toast.LENGTH_SHORT).show();
}

这里就不对所有注解怎么使用进行赘述了,大家只要在使用时去翻一翻源码即可.

然后我们先看下butterknife-compiler的构成

butterknife在编译过程中进行的操作全部在这里

因为处理注解使用的是Java Annotation Processing,所以入口肯定是ButterKnifeProcessor。
在看这个类的源码之前先说一点关于JavaPoet的知识

  • MethodSpec 代表一个构造函数或方法声明。
  • TypeSpec 代表一个类,接口,或者枚举声明。
  • FieldSpec 代表一个成员变量,一个字段声明。
  • JavaFile包含一个顶级类的Java文件。
举个小栗子🐒

这是一个HelloLianJia类

package com.lianjia.hello;

public final class HelloLianJia {
  public static void main(String[] args) {
    System.out.println("Hello, World!");
  }
}

使用JavaPoet来生成上面的HelloLianJia类

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, World!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloLianJia")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.lianjia.hello", helloWorld)
    .build();

javaFile.writeTo(System.out);

上述代码中我们先创建了一个MethodSpec来声明main方法,它配置了修饰符,返回类型,参数和代码语句。 然后我们使用TypeSpec创建了HelloLianJia类,它配置了修饰符之后将main方法添加到HelloLianJia类中,最后通过JavaFile将其添加到HelloWorld.java文件中。

在了解了JavaPoet相关知识后我们来看ButterKnifeProcessor的process方法
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //1.遍历和解析注解 结果存在Map中
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    //遍历Map 通过JavaPoet生成对应的ViewBinding文件
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
      //2.这里就是ViewBinding生成的过程
      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;
  }

解析的过程就是先将所有注解遍历解析一遍,然后使用JavaPoet根据解析结果生成对应的XXX_ViewBinding文件.

1. findAndParseTargets
 /**
   * 遍历和解析注解
   * @param env
   * @return 返回遍历的结果
   */
  private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    //注意这里创建的Map的value值为BindingSet.Builder 而不是方法定义的返回类型BindingSet
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    //将相关的View与R.id.XX建立对应的关系
    scanForRClasses(env);

    // Process each @BindAnim element.
    for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        //1-2.我们在这里只说解析@BindAnim的过程,其他的注解类似
        parseResourceAnimation(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindAnim.class, e);
      }
    }

    .....................省略其他注解的解析过程...........................
    

    // Associate superclass binders with their subclass binders. This is a queue-based tree walk
    // which starts at the roots (superclasses) and walks to the leafs (subclasses).

    // 这里的主要目的就是为了将Map.Entry<TypeElement, BindingSet.Builder>转化成Map<TypeElement, BindingSet>

    //第一步将Map.Entry<TypeElement, BindingSet.Builder>转换成Map.Entry<TypeElement, BindingSet.Builder>
    //这么做的主要原因是Map.Entry<K,V>可以直接调用getKey()和getValue()
    //那为什么要使用Deque呢? 看注释提醒需要将父类的绑定者和它们子类的绑定者关联起来,然后基于Deque从根到叶子的树遍历
    Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
      new ArrayDeque<>(builderMap.entrySet());
    Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
    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;
  }
1-2. parseResourceAnimation
  private void parseResourceAnimation(Element element,
    Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) {
    boolean hasError = false;
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Verify that the target type is Animation.检验类型是否是Animation类型
    if (!ANIMATION_TYPE.equals(element.asType().toString())) {
      error(element, "@%s field type must be 'Animation'. (%s.%s)", BindAnim.class.getSimpleName(),
        enclosingElement.getQualifiedName(), element.getSimpleName());
      hasError = true;
    }

    // Verify common generated code restrictions.检验通用生成的代码限制。
    hasError |= isInaccessibleViaGeneratedCode(BindAnim.class, "fields", element);
    //检验类的包名不能是android.或者java.开头的
    hasError |= isBindingInWrongPackage(BindAnim.class, element);

    if (hasError) {
      return;
    }

    // Assemble information on the field.
    String name = element.getSimpleName().toString();
    //获取id
    int id = element.getAnnotation(BindAnim.class).value();
    //将id和对应的packageName存储起来
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    //根据enclosingElement生成对应的BindingSet.Buidler
    BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    //然后对id和name进行封装以后添加到builder中
    builder.addResource(new FieldAnimationBinding(getId(qualifiedId), name));
    erasedTargetNames.add(enclosingElement);
  }
1-3. 关于BindingSet.Builder

BindingSet采用的是建造者设计模式来构建对应的实例,在内部类Buidler中会对注解的TypeName、bindingClassName、parentBinding和注解所在的Target是什么进行存储。

 static final class Builder {
    private final TypeName targetTypeName;
    private final ClassName bindingClassName;
    private final boolean isFinal;
    private final boolean isView;
    private final boolean isActivity;
    private final boolean isDialog;

    private BindingSet parentBinding;
   .........
 }
2. BindingSet.brewJava

这个方法里面处理了所有的注解,包含了ViewBinding类的生成过程

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

  private TypeSpec createType(int sdk, boolean debuggable) {
    //类名,修饰符类型为public
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    
    //final类型判断
    if (isFinal) {
      result.addModifiers(FINAL);
    }
    //父类的注解绑定判断
    if (parentBinding != null) {
      //继承父类
      result.superclass(parentBinding.bindingClassName);
    } else {
      //实现Unbinder接口
      result.addSuperinterface(UNBINDER);
    }

    //target字段的添加
    if (hasTargetField()) {
      result.addField(targetTypeName, "target", PRIVATE);
    }

    //创建构造方法 在最前面的bind方法中view、activity、dialog对应的绑定方法不一样,同理生成的构造方法也不一样
    //这里的构造方法其实没有做什么事情,只是相当于一个重载方法入口的匹配过程
    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
      result.addMethod(createBindingConstructorForDialog());
    }

    //绑定是否需要View
    if (!constructorNeedsView()) {
      // Add a delegating constructor with a target type + view signature for reflective use.
      // 添加具有目标类型和视图签名的代理构造方法以供反射使用
      result.addMethod(createBindingViewDelegateConstructor());
    }
  
    //真正做处理的构造方法在这里创建
    result.addMethod(createBindingConstructor(sdk, debuggable));

    //类型的绑定需要视图层次结构或者父类的注解绑定不存在
    if (hasViewBindings() || parentBinding == null) {
      //unbind方法的添加过程
      result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
  }

至此,ButterKnife的最核心的部分已经讲述完毕...

这个时候,可能会有人说:" 那R2是在什么地方生成的呢? "🐒

R2的出现起初是为了解决ButterKnife在Library中使用时无法使用R进行资源绑定,后来为了统一无论是在什么工程使用统一使用R2,而R2的配置其实是在 butterknife-gradle-plugin里通过gradle插件来实现的.


ButterKnifePlugin

这里就是R2的诞生之地~

butterknife-lint

作为项目检查,其内部存在一个核心类InvalidR2UsageDetector用来确保生成的R2在注释外部不被引用,感兴趣的朋友可以去看看源码时怎么实现。逃:)

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

推荐阅读更多精彩内容

  • 俗话说的好“不想偷懒的程序员,不是好程序员”,我们在日常开发android的过程中,在前端activity或者fr...
    蛋西阅读 4,939评论 0 14
  • 转载于:[http://blog.csdn.net/chenkai19920410/article/details...
    双鱼大猫阅读 484评论 0 5
  • 0X0 前言 做过Android开发的猿类很多都知道ButterKnife这么个东西。这个库可以大大的简化我们的代...
    knightingal阅读 722评论 1 10
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,112评论 18 139
  • 不要总用自己的价值观去决定别人的道路,每个人都有自己人生。
    杨宁哥哥阅读 136评论 0 0