千家信息网

怎样浅析ButterKnife

发表于:2024-09-21 作者:千家信息网编辑
千家信息网最后更新 2024年09月21日,怎样浅析ButterKnife,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。不管是Android开发的老司机也好,新司机也罢,想必大
千家信息网最后更新 2024年09月21日怎样浅析ButterKnife

怎样浅析ButterKnife,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。

不管是Android开发的老司机也好,新司机也罢,想必大家都对findViewById这种样板代码感到了厌倦,特别是进行复杂的UI界面开发的时候,这种代码就会显的非常的臃肿,既影响开发时的效率,又影响美观。
俗话说,不想偷懒的程序猿不叫工程师,那有什么方法可以让我们写这样的代码更加的有效率呢?

使用依赖注入框架

如果你不想写那些无聊的样板代码,那么你可以尝试一下现有的依赖注入库。ButterKnife作为Jake Wharton大神写的开源框架,号称在编译期间就可以实现依赖注入,没有用到反射,不会降低程序性能等。那么问题来了,它到底是怎么做到的呢?

初探ButterKnife

ButterKnife是Jake Wharton写的开源依赖注入框架,它和Android Annotations比较类似,都是用到了Java Annotation Tool来在编译期间生成辅助代码来达到View注入的目的。

注解处理器是Java1.5引入的工具,它提供了在程序编译期间扫描和处理注解的能力。它的原理就是在编译期间读取Java代码,解析注解,然后动态生成Java代码。下图是Java编译代码的流程,可以看到,我们的注解处理器的工作在Annotation Processing阶段,最终通过注解处理器生成的代码会和源代码一起被编译成Java字节码。不过比较遗憾的是你不能修改已经存在的Java文件,比如在已经存在的类中添加新的方法,所以通过Java Annotation Tool只能通过辅助类的方式来实现View的依赖注入,这样会略微增加项目的方法数和类数,不过只要控制好,不会对项目有太大的影响


ButterKnife在业务层的使用我就不介绍了,各位老司机肯定是轻车熟路。假如是我们自己写类似于ButterKnife这样的框架,那么我们的思路是这样:定义注解,扫描注解,生成代码。同时,我们需要用到以下这几个工具:JavaPoet(你当然可以直接用Java Annotation Tool,然后直接通过字符串拼接的方式去生成java源码,如果你生无可恋的话),Java Annotation Tool以及APT插件。为了后续更好的阅读ButterKnife的源码,我们先来介绍一下JavaPoet的基础知识。

JavaPoet生成代码

JavaPoet是一个可以生成.java源代码的开源项目,也是出自JakeWharton之手,我们可以结合注解处理器在程序编译阶段动态生成我们需要的代码。先介绍一个使用JavaPoet最基本的例子:

其中:

  • MethodSpec:代表一个构造函数或者方法声明

  • TypeSpec:代表一个类、接口或者枚举声明

  • FieldSpec:代表一个成员变量声明

  • JavaFile:代表一个顶级的JAVA文件

运行结果:

是不是很神奇?我们的例子只是把生成的代码写到了输出台,ButterKnife通过Java Annotation Tool的Filer可以帮助我们以文件的形式输出JAVA源码。问:那如果我要生成下面这段代码,我们会怎么写?


很简单嘛,依葫芦画瓢,只要把MethodSpec替换成下面这段:

然后代码华丽的生成了:

唉,等等,好像哪里不对啊,生成代码的格式怎么这么奇怪!难道我要这样写嘛:

这样写肯定是能达到我们的要求,但是未免也太麻烦了一点。其实JavaPoet提供了一个addStatement接口,可以自动帮我们换行以及添加分号,那么我们的代码就可以写成这个样子:

生成的代码:

好吧,其实格式也不是那么好看对不对?而且还要addStatement还需要夹杂addCode一起使用。为什么写个for循环都这么难(哭泣脸)。其实JavaPoet早考虑到这个问题,它提供了beginControlFlow() + endControlFlow()两个接口提供换行和缩进,再结合负责分号和换行的addStatement(),我们的代码就可以写成这样子:

生成的代码相当的顺眼:

其实JavaPoet还提供了很多有用的接口来帮我们更方便的生成代码。更加详细的用法请访问https://github.com/square/javapoet,这里我就不赘述了。

Java Annotation Tool

那么ButterKnife又是怎么通过Java Annotation Tool来生成我们的辅助代码呢?让我们以ButterKnife最新版本8.4.0的源代码为例。假如是我们自己写ButterKnife这样的框架,那么第一步肯定得先定义自己的注解。在ButterKnife源码的butterknife-annotations包中,我们可以看到ButterKnife自定义的所有的注解,如下图所示。

有了自定义注解,那我们的下一步就是实现自己的注解处理器了。我们结合ButterKnifeButterKnifeProcessor类来学习一下注解处理器的相关知识。为了实现自定义注解处理器,必须先继承AbstractProcessor类。ButterKnifeProcessor通过继承AbstractProcessor,实现了四个方法,如下图所示:


  • init(ProcessingEnvironment env)
    通过输入ProcessingEnvironment参数,你可以在得到很多有用的工具类,比如ElementsTypesFiler等。
    Elements是可以用来处理Element的工具类,可以理解为Java Annotation Tool扫描过程中扫描到的所有的元素,比如包(PackageElement)、类(TypeElement)、方法(ExecuteableElement)等
    Types是可以用来处理TypeMirror的工具类,它代表在JAVA语言中的一种类型,我们可以通过TypeMirror配合Elements来判断某个元素是否是我们想要的类型
    Filer是生成JAVA源代码的工具类,能不能生成java源码就靠它啦

  • getSupportedAnnotationTypes()
    代表注解处理器可以支持的注解类型,由前面的分析可以知道,ButterKnife支持的注解有BindViewOnClick等。

  • getSupportedSourceVersion()
    支持的JDK版本,一般使用SourceVersion.latestSupported(),这里使用Collections.singleton(OPTION_SDK_INT)也是可以的。

  • process(Set elements, RoundEnvironment env)
    process是整个注解处理器的重头戏,你所有扫描和处理注解的代码以及生成Java源文件的代码都写在这里面,这个也是我们将要重点分析的方法。

ButterKnifeProcessorprocess方法看起来很简单,实际上做了很多事情,大致可以分为两个部分:

  1. 扫描所有的ButterKnife注解,并且生成以TypeElement为Key,BindingSet为键值的HashMap。TypeElement我们在前面知道属于类或者接口,BindingSet用来记录我们使用JavaPoet生成代码时的一些参数,最终把该HashMap返回。这些逻辑对应于源码中的findAndParseTargets(RoundEnvironment env)方法

  2. 生成辅助类。辅助类以_ViewBinding为后缀,比如在MainActivity中使用了ButterKnife注解,那么最终会生成MainActivity_ViewBinding辅助类。MainActivity_ViewBinding类中最终会生成对应于@BindView的findViewById等代码。
    第一步,我们先来分析findAndParseTargets(RoundEnvironment env)源码。由于方法太长,而且做的事情都差不多,我们只需要分析一小段即可

private Map findAndParseTargets(RoundEnvironment env) {    Map builderMap = new LinkedHashMap<>();    Set 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 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;  }

遍历找到被注解的Element之后,通过parseBindView(Element element, Map builderMap,Set erasedTargetNames)方法去解析各个Element。在parseBindView方法中,首先会去检测被注解的元素是不是View或者Interface,如果满足条件则去获取被注解元素的注解的值,如果相应的的BindingSet.Builder没有被绑定过,那么通过getOrCreateBindingBuilder方法生成或者直接从targetClassMap中获取(为了提高效率,生成的BindingSet.Builder会被存储在targetClassMap中)。getOrCreateBindingBuilder方法比较简单,我就不贴代码了,生成的BindingSet.Builder会记录一个值binderClassNameButterKnife最终会根据binderClassName作为辅助类的类名。

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

parseBindView以及findAndParseTargets的解析工作完成后,所有的解析结果都会存放在targetClassMap中作为结果返回。我们现在来看process第二步的处理过程:遍历targetClassMap中所有的builder,并且通过Filer生成JAVA源文件。

---代码省略--- for (Map.Entry 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());      }    }

那么生成的代码都长什么样子呢?让我们打开BindingSetbrewJava(int sdk)方法一探究竟。

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


纳尼,竟然这么简单?我们观察到JavaFile的静态方法builder(String packageName, TypeSpec typeSpec)第二个参数为TypeSpec,前面提到过TypeSpec是JavaPoet提供的用来生成类的接口,打开createType(int sdk),霍霍,原来控制将要生成的代码的逻辑在这里:

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();  }

接下来让我们看看核心语句createBindingConstructor*_ViewBinding方法内到底干了什么:

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 nameViews;会走这段逻辑       */      for (FieldCollectionViewBinding binding : collectionBindings) {        constructor.addStatement("$L", binding.render());      }      if (!resourceBindings.isEmpty()) {        constructor.addCode("\n");      }    }---省略一些绑定resource资源的代码---}

addViewBindings我们简单看看就好。需要注意的是:

  • 因为生成代码时确实要根据不同条件来生成不同代码,所以使用了CodeBlock.Builder接口。CodeBlock.Builder也是JavaPoet提供的,该接口提供了类似字符串拼接的能力

  • 生成了类似于target.fieldBinding.getName() = .findViewById(bindings.getId().code)或者target.fieldBinding.getName() = .findRequiredView(bindings.getId().code)之类的代码,我们可以清楚的看到,这里没有用到反射,所以被@BindView注解的变量的修饰符不能为private。

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 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);}

addMethodBindings(result, bindings)实现了监听事件的绑定,也通过MethodSpec.Builder来生成相应的方法,由于源码太长,这里就不贴源码了。

小结:createType方法到底做了什么?

  • 生成类名为className_ViewBing的类

  • className_ViewBing实现了Unbinder接口(如果有父类的话,那么会调用父类的构造函数,不需要实现Unbinder接口)

  • 根据条件生成className_ViewBing构造函数(实现了成员变量、方法的绑定)以及unbind方法(解除绑定)等

如果简单使用ButterKnife,比如我们的MainActivity长这样

那么生成的最终MainActivity_ViewBinding类的代码就长下面这样子,和我们分析源码时预估的样子差不多。

需要注意的是,Utils.findRequiredViewAsTypeUtils.findRequiredViewUtils.castView的区别。其实Utils.findRequiredViewAsType就是Utils.findRequiredView(相当于findViewById)+Utils.castView(强制转型,class类接口)。

  public static  T findRequiredViewAsType(View source, @IdRes int id, String who,Class cls) {    View view = findRequiredView(source, id, who);    return castView(view, id, who, cls);  }

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)),这两个开源库的原理基本差不多,哪种方法比较好看个人喜好去选择吧。
言归正传,辅助类生成后,最终的调用过程一般是ButterKnife.bind(this)开始,查看ButterKnife.bind(this)源码,最终会走到createBinding以及findBindingConstructorForClass这个方法中,源码如下图所示,这个方法就是根据你传入的类名找到对应的辅助类,最终通过调用constructor.newInstance(target, source)来实现View以及其他资源的绑定工作。这里需要注意的是在findBindingConstructorForClass使用辅助类的时候,其实是有用到反射的,这样第一次使用的时候会稍微降低程序性能,但是ButterKnife会把通过反射生成的实例保存到HashMap中,下一次直接从HashMap中取上次生成的实例,这样就极大的降低了反射导致的性能问题。当然ButterKnife.bind方法还允许传入其他不同的参数,原理基本差不多,最终都会用到我们生成的辅助类,这里就不赘述了。


执行注解处理器

注解处理器已经有了,比如ButterKnifeProcessor,那么怎么执行它呢?这个时候就需要用到android-apt这个插件了,使用它有两个目的:

  1. 允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library

  2. 设置源路径,使注解处理器生成的代码能被Android Studio正确的引用

这里把使用ButterKnifeandroid-apt的配置作为例子,在工程的build.gradle中添加android-apt插件

buildscript {  repositories {    mavenCentral()   }  dependencies {    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'  }}

在项目的build.gradle中添加

apply plugin: 'android-apt'android {  ...}dependencies {  compile 'com.jakewharton:butterknife:8.4.0'  apt 'com.jakewharton:butterknife-compiler:8.4.0'}

ButterKnife作为一个被广泛使用的依赖注入库,有很多优点:

  • 没有使用反射,而是通过Java Annotation Tool动态生成辅助代码实现了View的依赖注入,提升了程序的性能

  • 提高开发效率,减少代码量

当然也有一些不太友好的地方:

  • 会额外生成新的类和方法数,主要是会加速触及65535方法数,当然,如果App已经有分dex了可以不用考虑

  • 也不是完全没有用到反射,比如第一次调用ButterKnife.bind(this)语句使用辅助类的时候就用到了,会稍微影响程序的性能(但是也仅仅是第一次)

关于怎样浅析ButterKnife问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注行业资讯频道了解更多相关知识。

生成 代码 方法 注解 处理 接口 辅助 处理器 源码 编译 参数 时候 程序 反射 代表 变量 工具 语句 问题 分析 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 华宝智投自动交易服务器延时 宝塔如何设置数据库连接 专家解释网络安全 软件开发学习编程 代理服务器技术的概念 一图读懂国家网络安全宣传 网络安全学院落地武汉 天津名优网络技术服务机构 网络安全的第二个时代 学校机房服务器怎么安装 呼玛软件开发有限公司 东莞电商平台软件开发 金蝶专业版怎样附加数据库 日照安卓软件开发哪家好 举例说明事务对数据库的作用 数据库打印表格 数据库查两个基因相关性 软件开发外包那个比较好 网络安全和计算机科学的区别 腾讯云服务器怎么存储文件 华为服务器收不到邮件 中学学生网络安全测试题 上海服务器机柜尺寸 大学生网络安全防范主题论文 数据库的等待时间是什么 软件开发新入职培训计划 海南高配置服务器云主机 计算机网络技术有哪些重要的知识 虚拟服务器怎么安装安全产品 网络安全整治严查
0