您好,登錄后才能下訂單哦!
怎樣淺析ButterKnife,針對這個問題,這篇文章詳細(xì)介紹了相對應(yīng)的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
不管是Android開發(fā)的老司機(jī)也好,新司機(jī)也罷,想必大家都對findViewById
這種樣板代碼感到了厭倦,特別是進(jìn)行復(fù)雜的UI界面開發(fā)的時候,這種代碼就會顯的非常的臃腫,既影響開發(fā)時的效率,又影響美觀。
俗話說,不想偷懶的程序猿不叫工程師,那有什么方法可以讓我們寫這樣的代碼更加的有效率呢?
如果你不想寫那些無聊的樣板代碼,那么你可以嘗試一下現(xiàn)有的依賴注入庫。ButterKnife
作為Jake Wharton大神寫的開源框架,號稱在編譯期間就可以實現(xiàn)依賴注入,沒有用到反射,不會降低程序性能等。那么問題來了,它到底是怎么做到的呢?
ButterKnife
是Jake Wharton寫的開源依賴注入框架,它和Android Annotations
比較類似,都是用到了Java Annotation Tool來在編譯期間生成輔助代碼來達(dá)到View注入的目的。
注解處理器是Java1.5引入的工具,它提供了在程序編譯期間掃描和處理注解的能力。它的原理就是在編譯期間讀取Java代碼,解析注解,然后動態(tài)生成Java代碼。下圖是Java編譯代碼的流程,可以看到,我們的注解處理器的工作在Annotation Processing階段,最終通過注解處理器生成的代碼會和源代碼一起被編譯成Java字節(jié)碼。不過比較遺憾的是你不能修改已經(jīng)存在的Java文件,比如在已經(jīng)存在的類中添加新的方法,所以通過Java Annotation Tool只能通過輔助類的方式來實現(xiàn)View的依賴注入,這樣會略微增加項目的方法數(shù)和類數(shù),不過只要控制好,不會對項目有太大的影響
ButterKnife
在業(yè)務(wù)層的使用我就不介紹了,各位老司機(jī)肯定是輕車熟路。假如是我們自己寫類似于ButterKnife
這樣的框架,那么我們的思路是這樣:定義注解,掃描注解,生成代碼。同時,我們需要用到以下這幾個工具:JavaPoet(你當(dāng)然可以直接用Java Annotation Tool,然后直接通過字符串拼接的方式去生成java源碼,如果你生無可戀的話),Java Annotation Tool以及APT插件。為了后續(xù)更好的閱讀ButterKnife
的源碼,我們先來介紹一下JavaPoet的基礎(chǔ)知識。
JavaPoet是一個可以生成.java
源代碼的開源項目,也是出自JakeWharton之手,我們可以結(jié)合注解處理器在程序編譯階段動態(tài)生成我們需要的代碼。先介紹一個使用JavaPoet最基本的例子:
其中:
MethodSpec:代表一個構(gòu)造函數(shù)或者方法聲明
TypeSpec:代表一個類、接口或者枚舉聲明
FieldSpec:代表一個成員變量聲明
JavaFile:代表一個頂級的JAVA文件
運(yùn)行結(jié)果:
是不是很神奇?我們的例子只是把生成的代碼寫到了輸出臺,ButterKnife
通過Java Annotation Tool的Filer
可以幫助我們以文件的形式輸出JAVA源碼。問:那如果我要生成下面這段代碼,我們會怎么寫?
很簡單嘛,依葫蘆畫瓢,只要把MethodSpec
替換成下面這段:
然后代碼華麗的生成了:
唉,等等,好像哪里不對啊,生成代碼的格式怎么這么奇怪!難道我要這樣寫嘛:
這樣寫肯定是能達(dá)到我們的要求,但是未免也太麻煩了一點(diǎn)。其實JavaPoet提供了一個addStatement
接口,可以自動幫我們換行以及添加分號,那么我們的代碼就可以寫成這個樣子:
生成的代碼:
好吧,其實格式也不是那么好看對不對?而且還要addStatement
還需要夾雜addCode
一起使用。為什么寫個for循環(huán)都這么難(哭泣臉)。其實JavaPoet早考慮到這個問題,它提供了beginControlFlow()
+ endControlFlow()
兩個接口提供換行和縮進(jìn),再結(jié)合負(fù)責(zé)分號和換行的addStatement()
,我們的代碼就可以寫成這樣子:
生成的代碼相當(dāng)?shù)捻樠郏?br/>
其實JavaPoet還提供了很多有用的接口來幫我們更方便的生成代碼。更加詳細(xì)的用法請訪問https://github.com/square/javapoet,這里我就不贅述了。
那么ButterKnife
又是怎么通過Java Annotation Tool來生成我們的輔助代碼呢?讓我們以ButterKnife
最新版本8.4.0的源代碼為例。假如是我們自己寫ButterKnife
這樣的框架,那么第一步肯定得先定義自己的注解。在ButterKnife
源碼的butterknife-annotations包中,我們可以看到ButterKnife
自定義的所有的注解,如下圖所示。
有了自定義注解,那我們的下一步就是實現(xiàn)自己的注解處理器了。我們結(jié)合ButterKnife
的ButterKnifeProcessor
類來學(xué)習(xí)一下注解處理器的相關(guān)知識。為了實現(xiàn)自定義注解處理器,必須先繼承AbstractProcessor類。ButterKnifeProcessor
通過繼承AbstractProcessor
,實現(xiàn)了四個方法,如下圖所示:
init(ProcessingEnvironment env)
通過輸入ProcessingEnvironment
參數(shù),你可以在得到很多有用的工具類,比如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源文件的代碼都寫在這里面,這個也是我們將要重點(diǎn)分析的方法。
ButterKnifeProcessor
的process
方法看起來很簡單,實際上做了很多事情,大致可以分為兩個部分:
掃描所有的ButterKnife
注解,并且生成以TypeElement
為Key,BindingSet
為鍵值的HashMap。TypeElement
我們在前面知道屬于類或者接口,BindingSet
用來記錄我們使用JavaPoet生成代碼時的一些參數(shù),最終把該HashMap返回。這些邏輯對應(yīng)于源碼中的findAndParseTargets(RoundEnvironment env)
方法
生成輔助類。輔助類以_ViewBinding
為后綴,比如在MainActivity
中使用了ButterKnife
注解,那么最終會生成MainActivity_ViewBinding
輔助類。MainActivity_ViewBinding
類中最終會生成對應(yīng)于@BindView的findViewById
等代碼。
第一步,我們先來分析findAndParseTargets(RoundEnvironment env)
源碼。由于方法太長,而且做的事情都差不多,我們只需要分析一小段即可
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; }
遍歷找到被注解的Element
之后,通過parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,Set<TypeElement> erasedTargetNames)
方法去解析各個Element
。在parseBindView
方法中,首先會去檢測被注解的元素是不是View或者Interface
,如果滿足條件則去獲取被注解元素的注解的值,如果相應(yīng)的的BindingSet.Builder
沒有被綁定過,那么通過getOrCreateBindingBuilder
方法生成或者直接從targetClassMap
中獲取(為了提高效率,生成的BindingSet.Builder
會被存儲在targetClassMap
中)。getOrCreateBindingBuilder
方法比較簡單,我就不貼代碼了,生成的BindingSet.Builder
會記錄一個值binderClassName
,ButterKnife
最終會根據(jù)binderClassName
作為輔助類的類名。
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 { //如果沒有綁定過,那么通過該方法獲得對應(yīng)的builder并且返回。這里的targetClassMap會存儲已經(jīng)生成的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
的解析工作完成后,所有的解析結(jié)果都會存放在targetClassMap
中作為結(jié)果返回。我們現(xiàn)在來看process
第二步的處理過程:遍歷targetClassMap
中所有的builder
,并且通過Filer
生成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()); } }
那么生成的代碼都長什么樣子呢?讓我們打開BindingSet
的brewJava(int sdk)
方法一探究竟。
JavaFile brewJava(int sdk) { return JavaFile.builder(bindingClassName.packageName(), createType(sdk)) .addFileComment("Generated code from Butter Knife. Do not modify!") .build(); }
納尼,竟然這么簡單?我們觀察到JavaFile
的靜態(tài)方法builder(String packageName, TypeSpec typeSpec)
第二個參數(shù)為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 { //如果沒有父類,那么實現(xiàn)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方法內(nèi)到底干了什么:
private MethodSpec createBindingConstructor(TypeName targetType, int sdk) { //方法修飾符為PUBLIC,并且添加注解為UiThread MethodSpec.Builder constructor = MethodSpec.constructorBuilder() .addAnnotation(UI_THREAD) .addModifiers(PUBLIC); if (hasMethodBindings()) { //如果有OnClick注解,那么添加方法參數(shù)為targetType final target constructor.addParameter(targetType, "target", FINAL); } else { //如果沒有OnClick注解,那么添加方法參數(shù)為targetType target constructor.addParameter(targetType, "target"); } if (constructorNeedsView()) { //如果有注解的View控件,那么添加View source參數(shù) constructor.addParameter(VIEW, "source"); } else { //否則添加Context source參數(shù) constructor.addParameter(CONTEXT, "context"); } if (hasUnqualifiedResourceBindings()) { constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType") .build()); } //如果有父類,那么會根據(jù)不同情況調(diào)用不同的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綁定的最常用,也是最關(guān)鍵的語句,生成類似于findViewById之類的代碼 addViewBindings(constructor, bindings); } /** * 如果將多個view組成一個List或數(shù)組,然后進(jìn)行綁定, * 比如@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資源的代碼--- }
addViewBindings
我們簡單看看就好。需要注意的是:
因為生成代碼時確實要根據(jù)不同條件來生成不同代碼,所以使用了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<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); // 監(jiān)聽事件綁定 addMethodBindings(result, bindings); }
addMethodBindings(result, bindings)
實現(xiàn)了監(jiān)聽事件的綁定,也通過MethodSpec.Builder
來生成相應(yīng)的方法,由于源碼太長,這里就不貼源碼了。
小結(jié):createType
方法到底做了什么?
生成類名為className_ViewBing
的類
className_ViewBing
實現(xiàn)了Unbinder
接口(如果有父類的話,那么會調(diào)用父類的構(gòu)造函數(shù),不需要實現(xiàn)Unbinder接口)
根據(jù)條件生成className_ViewBing
構(gòu)造函數(shù)(實現(xiàn)了成員變量、方法的綁定)以及unbind
方法(解除綁定)等
如果簡單使用ButterKnife
,比如我們的MainActivity
長這樣
那么生成的最終MainActivity_ViewBinding
類的代碼就長下面這樣子,和我們分析源碼時預(yù)估的樣子差不多。
需要注意的是,Utils.findRequiredViewAsType
、Utils.findRequiredView
、Utils.castView
的區(qū)別。其實Utils.findRequiredViewAsType
就是Utils.findRequiredView
(相當(dāng)于findViewById
)+Utils.castView
(強(qiáng)制轉(zhuǎn)型,class類接口)。
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); }
MainActivity_ViewBinding
類的調(diào)用過程就比較簡單了。MainActivity
一般會調(diào)用ButterKnife.bind(this)
來實現(xiàn)View的依賴注入,這個也是ButterKnife
和Google親兒子AndroidAnnotations
的區(qū)別:AndroidAnnotations
不需要自己手動調(diào)用ButterKnife.bind(this)
等類似的方法就可以實現(xiàn)View的依賴注入,但是讓人蛋疼的是編譯的時候會生成一個子類,這個子類是使用了AndroidAnnotations
類后面加了一個_
,比如MainActivity
你就要使用MainActivity_
來代替,比如Activity
的跳轉(zhuǎn)就必須這樣寫:startActivity(new Intent(this,MyActivity_.class))
,這兩個開源庫的原理基本差不多,哪種方法比較好看個人喜好去選擇吧。
言歸正傳,輔助類生成后,最終的調(diào)用過程一般是ButterKnife.bind(this)
開始,查看ButterKnife.bind(this)
源碼,最終會走到createBinding
以及findBindingConstructorForClass
這個方法中,源碼如下圖所示,這個方法就是根據(jù)你傳入的類名找到對應(yīng)的輔助類,最終通過調(diào)用constructor.newInstance(target, source)
來實現(xiàn)View以及其他資源的綁定工作。這里需要注意的是在findBindingConstructorForClass
使用輔助類的時候,其實是有用到反射的,這樣第一次使用的時候會稍微降低程序性能,但是ButterKnife
會把通過反射生成的實例保存到HashMap中,下一次直接從HashMap中取上次生成的實例,這樣就極大的降低了反射導(dǎo)致的性能問題。當(dāng)然ButterKnife.bind
方法還允許傳入其他不同的參數(shù),原理基本差不多,最終都會用到我們生成的輔助類,這里就不贅述了。
注解處理器已經(jīng)有了,比如ButterKnifeProcessor
,那么怎么執(zhí)行它呢?這個時候就需要用到android-apt
這個插件了,使用它有兩個目的:
允許配置只在編譯時作為注解處理器的依賴,而不添加到最后的APK或library
設(shè)置源路徑,使注解處理器生成的代碼能被Android Studio正確的引用
這里把使用ButterKnife
時android-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
作為一個被廣泛使用的依賴注入庫,有很多優(yōu)點(diǎn):
沒有使用反射,而是通過Java Annotation Tool動態(tài)生成輔助代碼實現(xiàn)了View的依賴注入,提升了程序的性能
提高開發(fā)效率,減少代碼量
當(dāng)然也有一些不太友好的地方:
會額外生成新的類和方法數(shù),主要是會加速觸及65535方法數(shù),當(dāng)然,如果App已經(jīng)有分dex了可以不用考慮
也不是完全沒有用到反射,比如第一次調(diào)用ButterKnife.bind(this)
語句使用輔助類的時候就用到了,會稍微影響程序的性能(但是也僅僅是第一次)
關(guān)于怎樣淺析ButterKnife問題的解答就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注億速云行業(yè)資訊頻道了解更多相關(guān)知識。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。