浅析 ButterKnife

xzyyjy 7年前
   <p>不管是Android开发的老司机也好,新司机也罢,想必大家都对 findViewById 这种样板代码感到了厌倦,特别是进行复杂的UI界面开发的时候,这种代码就会显的非常的臃肿,既影响开发时的效率,又影响美观。</p>    <p>俗话说,不想偷懒的程序猿不叫工程师,那有什么方法可以让我们写这样的代码更加的有效率呢?</p>    <h2><strong>使用依赖注入框架</strong></h2>    <p>如果你不想写那些无聊的样板代码,那么你可以尝试一下现有的依赖注入库。 ButterKnife 作为Jake Wharton大神写的开源框架,号称在编译期间就可以实现依赖注入,没有用到反射,不会降低程序性能等。那么问题来了,它到底是怎么做到的呢?</p>    <h3><strong>初探ButterKnife</strong></h3>    <p>ButterKnife 是Jake Wharton写的开源依赖注入框架,它和 Android Annotations 比较类似,都是用到了Java Annotation Tool来在编译期间生成辅助代码来达到View注入的目的。</p>    <p>注解处理器是Java1.5引入的工具,它提供了在程序编译期间扫描和处理注解的能力。它的原理就是在编译期间读取Java代码,解析注解,然后动态生成Java代码。下图是Java编译代码的流程,可以看到,我们的注解处理器的工作在Annotation Processing阶段,最终通过注解处理器生成的代码会和源代码一起被编译成Java字节码。不过比较遗憾的是你不能修改已经存在的Java文件,比如在已经存在的类中添加新的方法,所以通过Java Annotation Tool只能通过辅助类的方式来实现View的依赖注入,这样会略微增加项目的方法数和类数,不过只要控制好,不会对项目有太大的影响</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b1f1eaf406c6f2f8701704b4045c6f38.png"></p>    <p>ButterKnife 在业务层的使用我就不介绍了,各位老司机肯定是轻车熟路。假如是我们自己写类似于 ButterKnife 这样的框架,那么我们的思路是这样:定义注解,扫描注解,生成代码。同时,我们需要用到以下这几个工具:JavaPoet(你当然可以直接用Java Annotation Tool,然后直接通过字符串拼接的方式去生成java源码,如果你生无可恋的话),Java Annotation Tool以及APT插件。为了后续更好的阅读 ButterKnife 的源码,我们先来介绍一下JavaPoet的基础知识。</p>    <p><strong>JavaPoet生成代码</strong></p>    <p>JavaPoet是一个可以生成 .java 源代码的开源项目,也是出自JakeWharton之手,我们可以结合注解处理器在程序编译阶段动态生成我们需要的代码。先介绍一个使用JavaPoet最基本的例子:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/244ca4cd9cb41d2ca33eb011f43a2682.jpg"></p>    <p>其中:</p>    <ul>     <li> <p>MethodSpec:代表一个构造函数或者方法声明</p> </li>     <li> <p>TypeSpec:代表一个类、接口或者枚举声明</p> </li>     <li> <p>FieldSpec:代表一个成员变量声明</p> </li>     <li> <p>JavaFile:代表一个顶级的JAVA文件</p> </li>    </ul>    <p>运行结果:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/98ce5508b764b890c2b76c89f55d5274.png"></p>    <p>是不是很神奇?我们的例子只是把生成的代码写到了输出台, ButterKnife 通过Java Annotation Tool的 Filer 可以帮助我们以文件的形式输出JAVA源码。问:那如果我要生成下面这段代码,我们会怎么写?</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/949c2f2fb67ff27229cf88fbb085a9c0.png"></p>    <p>很简单嘛,依葫芦画瓢,只要把 MethodSpec 替换成下面这段:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d443738756be4dc0fd048d4e4d339750.png"></p>    <p>然后代码华丽的生成了:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/02766804edcded2a8c6d835eec9a8ae6.png"></p>    <p>唉,等等,好像哪里不对啊,生成代码的格式怎么这么奇怪!难道我要这样写嘛:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/67aed37d1d55b0d2581418d915ec7758.png"></p>    <p>这样写肯定是能达到我们的要求,但是未免也太麻烦了一点。其实JavaPoet提供了一个 addStatement 接口,可以自动帮我们换行以及添加分号,那么我们的代码就可以写成这个样子:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/512c5366ac5c4d8cd19bbd1fafe94723.png"></p>    <p>生成的代码:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c3a91a727eb420dbe06a32e019692ff6.png"></p>    <p>好吧,其实格式也不是那么好看对不对?而且还要 addStatement 还需要夹杂 addCode 一起使用。为什么写个for循环都这么难(哭泣脸)。其实JavaPoet早考虑到这个问题,它提供了 beginControlFlow() + endControlFlow() 两个接口提供换行和缩进,再结合负责分号和换行的 addStatement() ,我们的代码就可以写成这样子:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/22129f403e431df9ffa633c0742b0b75.png"></p>    <p>生成的代码相当的顺眼:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/73277763f974b0181d66e48c0f93affe.png"></p>    <p>其实JavaPoet还提供了很多有用的接口来帮我们更方便的生成代码。</p>    <p><strong>Java Annotation Tool</strong></p>    <p>那么 ButterKnife 又是怎么通过Java Annotation Tool来生成我们的辅助代码呢?让我们以 ButterKnife 最新版本8.4.0的源代码为例。假如是我们自己写 ButterKnife 这样的框架,那么第一步肯定得先定义自己的注解。在 ButterKnife 源码的butterknife-annotations包中,我们可以看到 ButterKnife 自定义的所有的注解,如下图所示。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/08ba9e430fc0f1659bc8c98a67a2c81b.jpg"></p>    <p>有了自定义注解,那我们的下一步就是实现自己的注解处理器了。我们结合 ButterKnife 的 ButterKnifeProcessor 类来学习一下注解处理器的相关知识。为了实现自定义注解处理器,必须先继承AbstractProcessor类。 ButterKnifeProcessor 通过继承 AbstractProcessor ,实现了四个方法,如下图所示:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9f099d6db265592e61b25d348c41dae1.jpg"> <img src="https://simg.open-open.com/show/0482f085a2fbc7507072f3dff898199d.jpg"></p>    <ul>     <li> <p>init(ProcessingEnvironment env)</p> <p>通过输入 ProcessingEnvironment 参数,你可以在得到很多有用的工具类,比如 Elements , Types , Filer 等。</p> <p>Elements 是可以用来处理 Element 的工具类,可以理解为Java Annotation Tool扫描过程中扫描到的所有的元素,比如包(PackageElement)、类(TypeElement)、方法(ExecuteableElement)等</p> <p>Types 是可以用来处理 TypeMirror 的工具类,它代表在JAVA语言中的一种类型,我们可以通过 TypeMirror 配合 Elements 来判断某个元素是否是我们想要的类型</p> <p>Filer 是生成JAVA源代码的工具类,能不能生成java源码就靠它啦</p> </li>     <li> <p>getSupportedAnnotationTypes()<br> 代表注解处理器可以支持的注解类型,由前面的分析可以知道, ButterKnife 支持的注解有 BindView 、 OnClick 等。</p> </li>     <li> <p>getSupportedSourceVersion()<br> 支持的JDK版本,一般使用 SourceVersion.latestSupported() ,这里使用 Collections.singleton(OPTION_SDK_INT) 也是可以的。</p> </li>     <li> <p>process(Set<? extends TypeElement> elements, RoundEnvironment env)<br> process 是整个注解处理器的重头戏,你所有扫描和处理注解的代码以及生成Java源文件的代码都写在这里面,这个也是我们将要重点分析的方法。</p> </li>    </ul>    <p>ButterKnifeProcessor 的 process 方法看起来很简单,实际上做了很多事情,大致可以分为两个部分:</p>    <ol>     <li> <p>扫描所有的 ButterKnife 注解,并且生成以 TypeElement 为Key, BindingSet 为键值的HashMap。 TypeElement 我们在前面知道属于类或者接口, BindingSet 用来记录我们使用JavaPoet生成代码时的一些参数,最终把该HashMap返回。这些逻辑对应于源码中的 findAndParseTargets(RoundEnvironment env) 方法</p> </li>     <li> <p>生成辅助类。辅助类以 _ViewBinding 为后缀,比如在 MainActivity 中使用了 ButterKnife 注解,那么最终会生成 MainActivity_ViewBinding 辅助类。 MainActivity_ViewBinding 类中最终会生成对应于@BindView的 findViewById 等代码。</p> <p>第一步,我们先来分析 findAndParseTargets(RoundEnvironment env) 源码。由于方法太长,而且做的事情都差不多,我们只需要分析一小段即可。</p> </li>    </ol>    <pre>  <code class="language-java">private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {      Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();      Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();      --- 省略部分代码---        for (Element element : env.getElementsAnnotatedWith(BindView.class)) {        if (!SuperficialValidation.validateElement(element)) continue;        try {          //遍历所有被BindView注解的类          parseBindView(element, targetClassMap, erasedTargetNames);        } catch (Exception e) {          logParsingError(element, BindView.class, e);        }      }      --- 省略部分代码---         // Try to find a parent binder for each.      for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {        TypeElement parentType = findParentType(entry.getKey(), erasedTargetNames);        if (parentType != null) {          BindingClass bindingClass = entry.getValue();          BindingClass parentBindingClass = targetClassMap.get(parentType);          bindingClass.setParent(parentBindingClass);        }      }        return targetClassMap;    }</code></pre>    <p>遍历找到被注解的 Element 之后,通过 parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,Set<TypeElement> erasedTargetNames) 方法去解析各个 Element 。在 parseBindView 方法中,首先会去检测被注解的元素是不是View或者 Interface ,如果满足条件则去获取被注解元素的注解的值,如果相应的的 BindingSet.Builder 没有被绑定过,那么通过 getOrCreateBindingBuilder 方法生成或者直接从 targetClassMap 中获取(为了提高效率,生成的 BindingSet.Builder 会被存储在 targetClassMap 中)。 getOrCreateBindingBuilder 方法比较简单,我就不贴代码了,生成的 BindingSet.Builder 会记录一个值 binderClassName , ButterKnife 最终会根据 binderClassName 作为辅助类的类名。</p>    <pre>  <code class="language-java">private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,        Set<TypeElement> erasedTargetNames) {      TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();        // Start by verifying common generated code restrictions.      boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)          || isBindingInWrongPackage(BindView.class, element);        // Verify that the target type extends from View.      TypeMirror elementType = element.asType();      --- 省略类型校验逻辑的代码---         // 获取注解的值      int id = element.getAnnotation(BindView.class).value();        BindingSet.Builder builder = builderMap.get(enclosingElement);      if (builder != null) {        ViewBindings viewBindings = builder.getViewBinding(getId(id));        if (viewBindings != null && viewBindings.getFieldBinding() != null) {          FieldViewBinding existingBinding = viewBindings.getFieldBinding();          error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",              BindView.class.getSimpleName(), id, existingBinding.getName(),              enclosingElement.getQualifiedName(), element.getSimpleName());          return;        }      } else {         //如果没有绑定过,那么通过该方法获得对应的builder并且返回。这里的targetClassMap会存储已经生成的builder,必要的时候提高效率           builder = getOrCreateBindingBuilder(builderMap, enclosingElement);      }        String name = element.getSimpleName().toString();      TypeName type = TypeName.get(elementType);      boolean required = isFieldRequired(element);        builder.addField(getId(id), new FieldViewBinding(name, type, required));        erasedTargetNames.add(enclosingElement);    }</code></pre>    <p>parseBindView 以及 findAndParseTargets 的解析工作完成后,所有的解析结果都会存放在 targetClassMap 中作为结果返回。我们现在来看 process 第二步的处理过程:遍历 targetClassMap 中所有的 builder ,并且通过 Filer 生成JAVA源文件。</p>    <pre>  <code class="language-java">---代码省略---   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());        }      }</code></pre>    <p>那么生成的代码都长什么样子呢?让我们打开 BindingSet 的 brewJava(int sdk) 方法一探究竟。</p>    <pre>  <code class="language-java"> JavaFile brewJava(int sdk) {      return JavaFile.builder(bindingClassName.packageName(), createType(sdk))          .addFileComment("Generated code from Butter Knife. Do not modify!")          .build();    }</code></pre>    <p><img src="https://simg.open-open.com/show/0d3d2227e3ab27307f7054d85792062c.jpg"></p>    <p>纳尼,竟然这么简单?我们观察到 JavaFile 的静态方法 builder(String packageName, TypeSpec typeSpec) 第二个参数为 TypeSpec ,前面提到过 TypeSpec 是JavaPoet提供的用来生成类的接口,打开 createType(int sdk) ,霍霍,原来控制将要生成的代码的逻辑在这里:</p>    <pre>  <code class="language-java">private TypeSpec createType(int sdk) {       // 生成类名为bindingClassName的类      TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())          .addModifiers(PUBLIC);       //ButterKnife的BindingSet初始化都是通过BindingSet的build方法初始化的,所以isFinal一般被初始化为false      if (isFinal) {        result.addModifiers(FINAL);      }          if (parentBinding != null) {         //如果有父类的话,那么注入该子类的时候,也会顺带注入其父类        result.superclass(parentBinding.bindingClassName);      } else {         //如果没有父类,那么实现Unbinder接口(所以所有生成的辅助类都会继承Unbinder接口)        result.addSuperinterface(UNBINDER);      }       //增加一个变量名为target,类型为targetTypeName的成员变量      if (hasTargetField()) {        result.addField(targetTypeName, "target", PRIVATE);      }        if (!constructorNeedsView()) {        // Add a delegating constructor with a target type + view signature for reflective use.        result.addMethod(createBindingViewDelegateConstructor(targetTypeName));      }      //核心方法,生成***_ViewBinding方法,我们控件的绑定比如findViewById之类的方法都在这里生成      result.addMethod(createBindingConstructor(targetTypeName, sdk));        if (hasViewBindings() || parentBinding == null) {       //生成unBind方法        result.addMethod(createBindingUnbindMethod(result, targetTypeName));      }        return result.build();    }</code></pre>    <p>接下来让我们看看核心语句 createBindingConstructor 在 <em>*</em> _ViewBinding方法内到底干了什么:</p>    <pre>  <code class="language-java">private MethodSpec createBindingConstructor(TypeName targetType, int sdk) {      //方法修饰符为PUBLIC,并且添加注解为UiThread      MethodSpec.Builder constructor = MethodSpec.constructorBuilder()          .addAnnotation(UI_THREAD)          .addModifiers(PUBLIC);        if (hasMethodBindings()) {         //如果有OnClick注解,那么添加方法参数为targetType final target        constructor.addParameter(targetType, "target", FINAL);      } else {         //如果没有OnClick注解,那么添加方法参数为targetType target        constructor.addParameter(targetType, "target");      }        if (constructorNeedsView()) {         //如果有注解的View控件,那么添加View source参数        constructor.addParameter(VIEW, "source");      } else {        //否则添加Context source参数        constructor.addParameter(CONTEXT, "context");      }        if (hasUnqualifiedResourceBindings()) {        constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)            .addMember("value", "$S", "ResourceType")            .build());      }         //如果有父类,那么会根据不同情况调用不同的super语句      if (parentBinding != null) {        if (parentBinding.constructorNeedsView()) {          constructor.addStatement("super(target, source)");        } else if (constructorNeedsView()) {          constructor.addStatement("super(target, source.getContext())");        } else {          constructor.addStatement("super(target, context)");        }        constructor.addCode("\n");      }         //如果有绑定过Field(不一定是View),那么添加this.target = target语句      if (hasTargetField()) {        constructor.addStatement("this.target = target");        constructor.addCode("\n");      }        if (hasViewBindings()) {        if (hasViewLocal()) {            // Local variable in which all views will be temporarily stored.          constructor.addStatement("$T view", VIEW);        }        for (ViewBindings bindings : viewBindings) {          //View绑定的最常用,也是最关键的语句,生成类似于findViewById之类的代码          addViewBindings(constructor, bindings);        }        /**         * 如果将多个view组成一个List或数组,然后进行绑定,         * 比如@BindView({ R.id.first_name, R.id.middle_name, R.id.last_name })         * List<EditText> nameViews;会走这段逻辑         */        for (FieldCollectionViewBinding binding : collectionBindings) {          constructor.addStatement("$L", binding.render());        }          if (!resourceBindings.isEmpty()) {          constructor.addCode("\n");        }      }  ---省略一些绑定resource资源的代码---  }</code></pre>    <p>addViewBindings 我们简单看看就好。需要注意的是:</p>    <ul>     <li> <p>因为生成代码时确实要根据不同条件来生成不同代码,所以使用了 CodeBlock.Builder 接口。 CodeBlock.Builder 也是JavaPoet提供的,该接口提供了类似字符串拼接的能力</p> </li>     <li> <p>生成了类似于 target.fieldBinding.getName() = .findViewById(bindings.getId().code) 或者 target.fieldBinding.getName() = .findRequiredView(bindings.getId().code) 之类的代码,我们可以清楚的看到,这里没有用到反射,所以被@BindView注解的变量的修饰符不能为private。</p> <pre>  <code class="language-java">private void addViewBindings(MethodSpec.Builder result, ViewBindings bindings) {    if (bindings.isSingleFieldBinding()) {      // Optimize the common case where there's a single binding directly to a field.      FieldViewBinding fieldBinding = bindings.getFieldBinding();      /**       * 这里使用了CodeBlock接口,顾名思义,该接口提供了类似字符串拼接的接口       * 另外,从target.$L 这条语句来看,我们就知道为什么使用BindView注解的       * 变量不能为private了       */      CodeBlock.Builder builder = CodeBlock.builder()          .add("target.$L = ", fieldBinding.getName());        boolean requiresCast = requiresCast(fieldBinding.getType());      if (!requiresCast && !fieldBinding.isRequired()) {        builder.add("source.findViewById($L)", bindings.getId().code);      } else {        builder.add("$T.find", UTILS);        builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");        if (requiresCast) {          builder.add("AsType");        }        builder.add("(source, $L", bindings.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;    }      List<ViewBinding> requiredViewBindings = bindings.getRequiredBindings();    if (requiredViewBindings.isEmpty()) {      result.addStatement("view = source.findViewById($L)", bindings.getId().code);    } else if (!bindings.isBoundToRoot()) {      result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS,          bindings.getId().code, asHumanDescription(requiredViewBindings));    }      addFieldBindings(result, bindings);     // 监听事件绑定    addMethodBindings(result, bindings);  }</code></pre> <p>addMethodBindings(result, bindings) 实现了监听事件的绑定,也通过 MethodSpec.Builder 来生成相应的方法,由于源码太长,这里就不贴源码了。</p> </li>    </ul>    <p>小结: createType 方法到底做了什么?</p>    <ul>     <li> <p>生成类名为 className_ViewBing 的类</p> </li>     <li> <p>className_ViewBing 实现了 Unbinder 接口(如果有父类的话,那么会调用父类的构造函数,不需要实现Unbinder接口)</p> </li>     <li> <p>根据条件生成 className_ViewBing 构造函数(实现了成员变量、方法的绑定)以及 unbind 方法(解除绑定)等</p> </li>    </ul>    <p>如果简单使用 ButterKnife ,比如我们的 MainActivity 长这样</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0b59fdc72ab8b62285c327277065ebd6.jpg"></p>    <p>那么生成的最终 MainActivity_ViewBinding 类的代码就长下面这样子,和我们分析源码时预估的样子差不多。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b7a0055f48a6ed2d898912595b316ff9.jpg"></p>    <p>需要注意的是, Utils.findRequiredViewAsType 、 Utils.findRequiredView 、 Utils.castView 的区别。其实 Utils.findRequiredViewAsType 就是 Utils.findRequiredView (相当于 findViewById )+ Utils.castView (强制转型,class类接口)。</p>    <pre>  <code class="language-java"> public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,Class<T> cls) {      View view = findRequiredView(source, id, who);      return castView(view, id, who, cls);    }</code></pre>    <p>MainActivity_ViewBinding 类的调用过程就比较简单了。 MainActivity 一般会调用 ButterKnife.bind(this) 来实现View的依赖注入,这个也是 ButterKnife 和Google亲儿子 AndroidAnnotations 的区别: AndroidAnnotations 不需要自己手动调用 ButterKnife.bind(this) 等类似的方法就可以实现View的依赖注入,但是让人蛋疼的是编译的时候会生成一个子类,这个子类是使用了 AndroidAnnotations 类后面加了一个 _ ,比如 MainActivity 你就要使用 MainActivity_ 来代替,比如 Activity 的跳转就必须这样写: startActivity(new Intent(this,MyActivity_.class)) ,这两个开源库的原理基本差不多,哪种方法比较好看个人喜好去选择吧。</p>    <p>言归正传,辅助类生成后,最终的调用过程一般是 ButterKnife.bind(this) 开始,查看 ButterKnife.bind(this) 源码,最终会走到 createBinding 以及 findBindingConstructorForClass 这个方法中,源码如下图所示,这个方法就是根据你传入的类名找到对应的辅助类,最终通过调用 constructor.newInstance(target, source) 来实现View以及其他资源的绑定工作。这里需要注意的是在 findBindingConstructorForClass 使用辅助类的时候,其实是有用到反射的,这样第一次使用的时候会稍微降低程序性能,但是 ButterKnife 会把通过反射生成的实例保存到HashMap中,下一次直接从HashMap中取上次生成的实例,这样就极大的降低了反射导致的性能问题。当然 ButterKnife.bind 方法还允许传入其他不同的参数,原理基本差不多,最终都会用到我们生成的辅助类,这里就不赘述了。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f3632a18e8b1d4a829734991a4402ce1.jpg"> <img src="https://simg.open-open.com/show/b30e8fed685fbba8e2450ed113ece599.jpg"></p>    <p><strong>执行注解处理器</strong></p>    <p>注解处理器已经有了,比如 ButterKnifeProcessor ,那么怎么执行它呢?这个时候就需要用到 android-apt 这个插件了,使用它有两个目的:</p>    <ol>     <li> <p>允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library</p> </li>     <li> <p>设置源路径,使注解处理器生成的代码能被Android Studio正确的引用</p> </li>    </ol>    <p>这里把使用 ButterKnife 时 android-apt 的配置作为例子,在工程的 build.gradle 中添加 android-apt 插件</p>    <pre>  <code class="language-java">buildscript {    repositories {      mavenCentral()     }    dependencies {      classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'    }  }</code></pre>    <p>在项目的build.gradle中添加</p>    <pre>  <code class="language-java">apply plugin: 'android-apt'    android {    ...  }    dependencies {    compile 'com.jakewharton:butterknife:8.4.0'    apt 'com.jakewharton:butterknife-compiler:8.4.0'  }</code></pre>    <h3><strong>总结</strong></h3>    <p>ButterKnife 作为一个被广泛使用的依赖注入库,有很多优点:</p>    <ul>     <li> <p>没有使用反射,而是通过Java Annotation Tool动态生成辅助代码实现了View的依赖注入,提升了程序的性能</p> </li>     <li> <p>提高开发效率,减少代码量</p> </li>    </ul>    <p>当然也有一些不太友好的地方:</p>    <ul>     <li> <p>会额外生成新的类和方法数,主要是会加速触及65535方法数,当然,如果App已经有分dex了可以不用考虑</p> </li>     <li> <p>也不是完全没有用到反射,比如第一次调用 ButterKnife.bind(this) 语句使用辅助类的时候就用到了,会稍微影响程序的性能(但是也仅仅是第一次)</p> </li>    </ul>    <p>ButterKnife 之旅到这里就结束了,至于要不要在项目中使用大家可以根据实际情况来选择,毕竟适合自己的才是最好的。</p>    <p><strong>参考文档</strong></p>    <ol>     <li> <p>[初探JavaPoet]</p> <p>http://blog.ornithopter.me/post/android/javapoet</p> </li>     <li> <p>Generating Java Source Files with JavaPoet</p> <p>http://www.hascode.com/2015/02/generating-java-source-files-with-javapoet/</p> </li>     <li> <p>Java注解处理器</p> <p>http://www.race604.com/annotation-processing/</p> </li>     <li> <p>android-apt</p> <p>http://www.jianshu.com/p/2494825183c5</p> </li>     <li> <p>深入浅出ButterKnife</p> <p>http://km.oa.com/articles/show/286521?kmref=search&from_page=1&no=1&is_from_iso=1</p> </li>     <li> <p>ButterKnife框架原理</p> <p>https://bxbxbai.github.io/2016/03/12/how-butterknife-works/</p> </li>     <li> <p>浅析ButterKnife的实现</p> <p>http://blog.csdn.net/ta893115871/article/details/52497297</p> </li>     <li> <p>深入理解ButterKnife源码并掌握原理</p> <p>http://www.bcoder.cn/13581.html</p> </li>     <li> <p>ButterKnife源码剖析</p> <p>http://blog.csdn.net/chenkai19920410/article/details/51020151</p> </li>    </ol>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA==&mid=2651232205&idx=1&sn=6c24e6eef2b18f253284b9dd92ec7efb&chksm=f1d9eaaec6ae63b82fd84f72c66d3759c693f164ff578da5dde45d367f168aea0038bc3cc8e8&scene=0#wechat_redirect</p>    <p> </p>