Butterknife源码解析

前言

Jake Wharton大神的Butterknife可谓是造福广大Android开发者, 再也不用重复写findViewById和setOnClickListener了.但是用了这么久的Butterknife, 一直以为用的是反射, 直到看了源码...(路漫漫其修远兮,吾将上下而求索)

Butterknife的使用

Git地址: https://github.com/JakeWharton/butterknife, 里面有具体的引用及使用说明, 这里不再介绍.

Butterknife的原理

谈到Butterknife的原理, 不得不先提一下运行时注解和编译时注解的区别:

  • 编译时注解: (代码生成)在编译时扫描所有注解,对注解进行处理,在项目中生成一份代码,然后在项目运行时直接调用;
  • 运行时注解: (代码注入)在代码中通过注解进行标记,在运行时通过反射在原代码的基础上完成注入;

Butterknife就是用的编译时注解(Annotation Processing Tool),简称APT技术. 关于APT, 可以戳这里,里面有demo,介绍了编译时自动生成代码的整个过程. 看完这篇文章再看Butterknife就有章可循了.

Butterknife源码解析

了解APT之后就知道了最重要的有两步,一编译时自动生成代码,二运行时进行代码绑定.下面以Butterknife最新版本8.8.1的源码进行说明;

编译时自动生成代码

首先来看ButterKnifeProcessor类,继承AbstractProcessor,需要复写的方法:
init(): 主要初始化一些工具类

@Override 
public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    ... //获取sdk和是否debug
    elementUtils = env.getElementUtils();//获取元素相关信息的工具类
    typeUtils = env.getTypeUtils();//处理TypeMirror的工具类
    filer = env.getFiler();//生成java文件的工具类
    try {
      trees = Trees.instance(processingEnv);
    } catch (IllegalArgumentException ignored) {
    }
  }

getSupportedAnnotationTypes(): 返回支持的注解类型,包括BindAnim,BindArray等注解,以及OnClick,OnCheckedChanged等监听.

private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

    annotations.add(BindAnim.class);
    annotations.add(BindArray.class);
    annotations.add(BindBitmap.class);
    annotations.add(BindBool.class);
    annotations.add(BindColor.class);
    annotations.add(BindDimen.class);
    annotations.add(BindDrawable.class);
    annotations.add(BindFloat.class);
    annotations.add(BindFont.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
  }

process():生成代码的核心部分,分两步,扫描处理代码中所有的注解,然后通过javapoet生成java文件. 返回值表示这组注解是否被这个 Processor 接受,如果接受(返回true)后续子的 processor 不会再对这个注解进行处理.

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //扫描代码中所有的注解,存到map中
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
     //生成java文件
      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;
  }

RoundEnvironment表示当前或之前的运行环境,可以通过该对象查找注解.
TypeElement代表源代码中的元素类型, 如包,类,属性,方法, 源代码中每一部分都是一种类型.包为PackageElement,类为TypeElement,属性为VariableElement,方法为ExecuteableElement ,都是Element的子类.
然后看下findAndParseTargets方法是如何扫描所有注解的.

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
    //建立view与id的关系
    scanForRClasses(env);

   // Process each @BindView element.
   //env.getElementsAnnotatedWith(BindView.class))获取所有注解是BindView的元素
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      // we don't SuperficialValidation.validateElement(element)
      // so that an unresolved View type can be generated by later processing rounds
      try {
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }    return bindingMap;
  }

首先看scanForRClasses()如何建立view与id的关系

private void scanForRClasses(RoundEnvironment env) {
    if (trees == null) return;
    //R文件扫描器,扫描代码中所有的R文件
    RClassScanner scanner = new RClassScanner();

    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
      for (Element element : env.getElementsAnnotatedWith(annotation)) {
        JCTree tree = (JCTree) trees.getTree(element, getMirror(element, annotation));
        if (tree != null) { // tree can be null if the references are compiled types and not source
          //获取R文件的包名
          String respectivePackageName =
              elementUtils.getPackageOf(element).getQualifiedName().toString();
          scanner.setCurrentPackageName(respectivePackageName);
          tree.accept(scanner);
        }
      }
    }

    for (Map.Entry<String, Set<String>> packageNameToRClassSet : scanner.getRClasses().entrySet()) {
      String respectivePackageName = packageNameToRClassSet.getKey();
      for (String rClass : packageNameToRClassSet.getValue()) {
        //解析R文件
        parseRClass(respectivePackageName, rClass);
      }
    }
  }

parseRClass()利用IdScanner寻找R文件内部类,如array,attr,string等

private void parseRClass(String respectivePackageName, String rClass) {
    Element element;

    try {
      element = elementUtils.getTypeElement(rClass);
    } catch (MirroredTypeException mte) {
      element = typeUtils.asElement(mte.getTypeMirror());
    }

    JCTree tree = (JCTree) trees.getTree(element);
    if (tree != null) { // tree can be null if the references are compiled types and not source
      //利用IdScanner寻找R文件内部类,如array,attr,string等
      IdScanner idScanner = new IdScanner(symbols, elementUtils.getPackageOf(element)
          .getQualifiedName().toString(), respectivePackageName);
      tree.accept(idScanner);
    } else {
      parseCompiledR(respectivePackageName, (TypeElement) element);
    }
  }

在IdScanner类内部利用VarScanner扫描R文件内部类(id,string等)的属性(键值对:资源名和id)

private static class IdScanner extends TreeScanner {
    private final Map<QualifiedId, Id> ids;
    private final String rPackageName;
    private final String respectivePackageName;

    IdScanner(Map<QualifiedId, Id> ids, String rPackageName, String respectivePackageName) {
      this.ids = ids;
      this.rPackageName = rPackageName;
      this.respectivePackageName = respectivePackageName;
    }

    @Override public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
      for (JCTree tree : jcClassDecl.defs) {
        if (tree instanceof ClassTree) {
          ClassTree classTree = (ClassTree) tree;
          String className = classTree.getSimpleName().toString();
          if (SUPPORTED_TYPES.contains(className)) {
            ClassName rClassName = ClassName.get(rPackageName, "R", className);
            VarScanner scanner = new VarScanner(ids, rClassName, respectivePackageName);
            ((JCTree) classTree).accept(scanner);
          }
        }
      }
    }
  }

在VarScanner类内部记录资源名称及id

private static class VarScanner extends TreeScanner {
    private final Map<QualifiedId, Id> ids;
    private final ClassName className;
    private final String respectivePackageName;

    private VarScanner(Map<QualifiedId, Id> ids, ClassName className,
        String respectivePackageName) {
      this.ids = ids;
      this.className = className;
      this.respectivePackageName = respectivePackageName;
    }

    @Override public void visitVarDef(JCTree.JCVariableDecl jcVariableDecl) {
      if ("int".equals(jcVariableDecl.getType().toString())) {
        int id = Integer.valueOf(jcVariableDecl.getInitializer().toString());
        String resourceName = jcVariableDecl.getName().toString();
        QualifiedId qualifiedId = new QualifiedId(respectivePackageName, id);
        ids.put(qualifiedId, new Id(id, className, resourceName));
      }
    }
  }

到此就建立了所有view与id的关系,然后看如何处理注解, 以BindView为例(其他类似). 将每一个BindView注解的元素存放到被绑定类的成员变量中.

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
    //获取BindView注解元素的父元素, 如MainActivity
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Start by verifying common generated code restrictions.
    //isInaccessibleViaGeneratedCode检查父元素是否是类且非private
    //isBindingInWrongPackage检查父元素是否是系统相关的类
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);

    // Verify that the target type extends from View.
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      TypeVariable typeVariable = (TypeVariable) elementType;
      elementType = typeVariable.getUpperBound();
    }
    Name qualifiedName = enclosingElement.getQualifiedName();
    Name simpleName = element.getSimpleName();
    //判断BindView注解的元素是view的子类或接口,否则有错return
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
      if (elementType.getKind() == TypeKind.ERROR) {
        note(element, "@%s field with unresolved type (%s) "
                + "must elsewhere be generated as a View or interface. (%s.%s)",
            BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
      } else {
        error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
            BindView.class.getSimpleName(), qualifiedName, simpleName);
        hasError = true;
      }
    }

    if (hasError) {
      return;
    }

    // Assemble information on the field.
    //获取BindView注解的value,及id
    int id = element.getAnnotation(BindView.class).value();
    //builderMap缓存所有被绑定类的信息, 键是父元素(如MainActivity),值是键对应的被绑定类的信息(如MainActivity_ViewBinding)
    //获取BindingSet.Builder, 有则使用缓存,无则创建
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    if (builder != null) {
      //从被绑定类的成员变量中查找这个id,不为空,说明有多个view绑定了同一个id, 抛异常
      String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
      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 = simpleName.toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);
    //加入到成员变量集合中
    builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

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

再看如何创建被绑定类BindingSet

private BindingSet.Builder getOrCreateBindingBuilder(
      Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder == null) {
      builder = BindingSet.newBuilder(enclosingElement);
      builderMap.put(enclosingElement, builder);
    }
    return builder;
  }
static Builder newBuilder(TypeElement enclosingElement) {
    //获取注解父元素的类型,View/Activity/Dialog
    TypeMirror typeMirror = enclosingElement.asType();

    boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
    boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
    boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE);

    TypeName targetType = TypeName.get(typeMirror);
    if (targetType instanceof ParameterizedTypeName) {
      targetType = ((ParameterizedTypeName) targetType).rawType;
    }
    //获取父元素的包名和类名
    String packageName = getPackage(enclosingElement).getQualifiedName().toString();
    String className = enclosingElement.getQualifiedName().toString().substring(
        packageName.length() + 1).replace('.', '$');
    //获取绑定的类名,加后缀_ViewBinding(后面介绍生成加后缀_ViewBinding的java文件)
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

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

到此完成了扫描所有注解的工作,再看如何生成java文件.就是process()中的binding.brewJava(sdk, debuggable);

JavaFile brewJava(int sdk, boolean debuggable) {
    //bindingClassName就是加了后缀_ViewBinding的类
    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) {
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    if (isFinal) {
      result.addModifiers(FINAL);
    }

    if (parentBinding != null) {
      result.superclass(parentBinding.bindingClassName);
    } else {
      result.addSuperinterface(UNBINDER);
    }

    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, debuggable));

    if (hasViewBindings() || parentBinding == null) {
      result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
  }

这里用到了javapoet,感兴趣的可以戳这里, 通过TypeSpec生成类,并一步步地添加构造方法等.
编译之后就会在build目录下会产生原Activity.class以及原Activity_ViewBinding.class两份代码,.来看下_ViewBinding代码的结构,以官方SimpleActivity_ViewBinding为例.在构造函数中生成控件以及事件监听,在unbind对所有控件,事件以及原Activity的引用置空,等待GC回收.

public class SimpleActivity_ViewBinding<T extends SimpleActivity> implements Unbinder {
  protected T target;

  private View view2130968578;

  private View view2130968579;

  @UiThread
  public SimpleActivity_ViewBinding(final T target, View source) {
    this.target = target;

    View view;
    //target就是SimpleActivity,所以SimpleActivity中的title,subtitle等被BindView注解的元素都不能是private的.
    target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
    target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
    view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
    target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);
    view2130968578 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.sayHello();
      }
    });
    view.setOnLongClickListener(new View.OnLongClickListener() {
      @Override
      public boolean onLongClick(View p0) {
        return target.sayGetOffMe();
      }
    });
    view = Utils.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'");
    target.listOfThings = Utils.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class);
    view2130968579 = view;
    ((AdapterView<?>) view).setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) {
        target.onItemClick(p2);
      }
    });
    target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);
    target.headerViews = Utils.listOf(
        Utils.findRequiredView(source, R.id.title, "field 'headerViews'"), 
        Utils.findRequiredView(source, R.id.subtitle, "field 'headerViews'"), 
        Utils.findRequiredView(source, R.id.hello, "field 'headerViews'"));
  }

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

    target.title = null;
    target.subtitle = null;
    target.hello = null;
    target.listOfThings = null;
    target.footer = null;
    target.headerViews = null;

    view2130968578.setOnClickListener(null);
    view2130968578.setOnLongClickListener(null);
    view2130968578 = null;
    ((AdapterView<?>) view2130968579).setOnItemClickListener(null);
    view2130968579 = null;

    this.target = null;
  }
}

编译时自动生成代码讲完了, 然后来看代码的绑定.

代码绑定

用过Butterknife的朋友都知道是通过ButterKnife.bind(this)进行绑定(以Activity为例).

@NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) {
    //获取跟布局view
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  }
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());
    //找到被绑定类(如SimpleActivity_ViewBinding)的构造函数
    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);
    }
  }

到此就完成了原Activity与Activity_ViewBinding的绑定,即在ButterKnife.bind(this)时就完成原Activity注解控件及事件的初始化,原Activity就可以直接调用了.

如理解有误,欢迎指正.最后附上相关技术的文章:
APT----《android-apt》
Javapoet----《javapoet——让你从重复无聊的代码中解放出来》

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

推荐阅读更多精彩内容

  • 本文主要介绍Android之神JakeWharton的一个注解框架,听说现在面试官现在面试都会问知不知道JakeW...
    Zeit丶阅读 964评论 4 6
  • 参考1参考2参考3参考4 一:基本原理 编译时注解+APT编译时注解:Rentention为CLASS的直接。保留...
    shuixingge阅读 513评论 0 3
  • 使用Butterknife的主要目的是消除关于view实例化的样板代码,这是一个专为View类型的依赖注入框架。D...
    9bc96fd72f8e阅读 328评论 0 5
  • 按照惯例,随笔又偷懒了。 今天收到上海方面确认,被收了,现在是到了签订三方协议的阶段,今天打算去离三方协议,虽然遇...
    夏十里阅读 85评论 0 0
  • 今天回家了,家里杏花已经落完,青杏藏在满树绿叶中,丝毫不起眼。清明的雨已经下完,滋润了整片大地,回升的气温使得万...
    b2d8dedfdf11阅读 208评论 0 0