怎样浅析ButterKnife

免费建站   2024年05月10日 10:26  

怎样浅析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自定义的所有的注解,如下图所示。有了自定义注解,那我们的下一步就是实现自己的注解处理器了。我们结合ButterKnife的ButterKnifeProcessor类来学习一下注解处理器的相关知识。为了实现自定义注解处理器,必须先继承AbstractProcessor类。ButterKnifeProcessor通过继承AbstractProcessor,实现了四个方法,如下图所示:

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

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

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

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

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

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

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

privateMap<TypeElement,BindingClass>findAndParseTargets(RoundEnvironmentenv){Map<TypeElement,BindingSet.Builder>builderMap=newLinkedHashMap<>();Set<TypeElement>erasedTargetNames=newLinkedHashSet<>();---省略部分代码---for(Elementelement:env.getElementsAnnotatedWith(BindView.class)){if(!SuperficialValidation.validateElement(element))continue;try{//遍历所有被BindView注解的类parseBindView(element,targetClassMap,erasedTargetNames);}catch(Exceptione){logParsingError(element,BindView.class,e);}}---省略部分代码---//Trytofindaparentbinderforeach.for(Map.Entry<TypeElement,BindingClass>entry:targetClassMap.entrySet()){TypeElementparentType=findParentType(entry.getKey(),erasedTargetNames);if(parentType!=null){BindingClassbindingClass=entry.getValue();BindingClassparentBindingClass=targetClassMap.get(parentType);bindingClass.setParent(parentBindingClass);}}returntargetClassMap;}

遍历找到被注解的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作为辅助类的类名。

privatevoidparseBindView(Elementelement,Map<TypeElement,BindingSet.Builder>builderMap,Set<TypeElement>erasedTargetNames){TypeElementenclosingElement=(TypeElement)element.getEnclosingElement();//Startbyverifyingcommongeneratedcoderestrictions.booleanhasError=isInaccessibleViaGeneratedCode(BindView.class,"fields",element)||isBindingInWrongPackage(BindView.class,element);//VerifythatthetargettypeextendsfromView.TypeMirrorelementType=element.asType();---省略类型校验逻辑的代码---//获取注解的值intid=element.getAnnotation(BindView.class).value();BindingSet.Builderbuilder=builderMap.get(enclosingElement);if(builder!=null){ViewBindingsviewBindings=builder.getViewBinding(getId(id));if(viewBindings!=null&&viewBindings.getFieldBinding()!=null){FieldViewBindingexistingBinding=viewBindings.getFieldBinding();error(element,"Attempttouse@%sforanalreadyboundID%don'%s'.(%s.%s)",BindView.class.getSimpleName(),id,existingBinding.getName(),enclosingElement.getQualifiedName(),element.getSimpleName());return;}}else{//如果没有绑定过,那么通过该方法获得对应的builder并且返回。这里的targetClassMap会存储已经生成的builder,必要的时候提高效率builder=getOrCreateBindingBuilder(builderMap,enclosingElement);}Stringname=element.getSimpleName().toString();TypeNametype=TypeName.get(elementType);booleanrequired=isFieldRequired(element);builder.addField(getId(id),newFieldViewBinding(name,type,required));erasedTargetNames.add(enclosingElement);}

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

---代码省略---for(Map.Entry<TypeElement,BindingSet>entry:bindingMap.entrySet()){TypeElementtypeElement=entry.getKey();BindingSetbinding=entry.getValue();JavaFilejavaFile=binding.brewJava(sdk);try{javaFile.writeTo(filer);}catch(IOExceptione){error(typeElement,"Unabletowritebindingfortype%s:%s",typeElement,e.getMessage());}}

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

JavaFilebrewJava(intsdk){returnJavaFile.builder(bindingClassName.packageName(),createType(sdk)).addFileComment("GeneratedcodefromButterKnife.Donotmodify!").build();}

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

privateTypeSpeccreateType(intsdk){//生成类名为bindingClassName的类TypeSpec.Builderresult=TypeSpec.classBuilder(bindingClassName.simpleName()).addModifiers(PUBLIC);//ButterKnife的BindingSet初始化都是通过BindingSet的build方法初始化的,所以isFinal一般被初始化为falseif(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()){//Addadelegatingconstructorwithatargettype+viewsignatureforreflectiveuse.result.addMethod(createBindingViewDelegateConstructor(targetTypeName));}//核心方法,生成***_ViewBinding方法,我们控件的绑定比如findViewById之类的方法都在这里生成result.addMethod(createBindingConstructor(targetTypeName,sdk));if(hasViewBindings()||parentBinding==null){//生成unBind方法result.addMethod(createBindingUnbindMethod(result,targetTypeName));}returnresult.build();}

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

privateMethodSpeccreateBindingConstructor(TypeNametargetType,intsdk){//方法修饰符为PUBLIC,并且添加注解为UiThreadMethodSpec.Builderconstructor=MethodSpec.constructorBuilder().addAnnotation(UI_THREAD).addModifiers(PUBLIC);if(hasMethodBindings()){//如果有OnClick注解,那么添加方法参数为targetTypefinaltargetconstructor.addParameter(targetType,"target",FINAL);}else{//如果没有OnClick注解,那么添加方法参数为targetTypetargetconstructor.addParameter(targetType,"target");}if(constructorNeedsView()){//如果有注解的View控件,那么添加Viewsource参数constructor.addParameter(VIEW,"source");}else{//否则添加Contextsource参数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)");}elseif(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()){//Localvariableinwhichallviewswillbetemporarilystored.constructor.addStatement("$Tview",VIEW);}for(ViewBindingsbindings: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(FieldCollectionViewBindingbinding: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。

privatevoidaddViewBindings(MethodSpec.Builderresult,ViewBindingsbindings){if(bindings.isSingleFieldBinding()){//Optimizethecommoncasewherethere'sasinglebindingdirectlytoafield.FieldViewBindingfieldBinding=bindings.getFieldBinding();/***这里使用了CodeBlock接口,顾名思义,该接口提供了类似字符串拼接的接口*另外,从target.$L这条语句来看,我们就知道为什么使用BindView注解的*变量不能为private了*/CodeBlock.Builderbuilder=CodeBlock.builder().add("target.$L=",fieldBinding.getName());booleanrequiresCast=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);}elseif(!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.findRequiredViewAsType、Utils.findRequiredView、Utils.castView的区别。其实Utils.findRequiredViewAsType就是Utils.findRequiredView(相当于findViewById)+Utils.castView(强制转型,class类接口)。

publicstatic<T>TfindRequiredViewAsType(Viewsource,@IdResintid,Stringwho,Class<T>cls){Viewview=findRequiredView(source,id,who);returncastView(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这个插件了,使用它有两个目的:

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

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

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

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

在项目的build.gradle中添加

applyplugin:'android-apt'android{...}dependencies{compile'com.jakewharton::8.4.0'apt'com.jakewharton:-compiler:8.4.0'}

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

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

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

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

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

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

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

域名注册
购买VPS主机

您或许对下面这些文章有兴趣:                    本月吐槽辛苦排行榜

看贴要回贴有N种理由!看帖不回贴的后果你懂得的!


评论内容 (*必填):
(Ctrl + Enter提交)   

部落快速搜索栏

各类专题梳理

网站导航栏

X
返回顶部