摘要:這似乎是一個很有意思的話題,如果你的程序足夠聰明,它就可以自己寫代碼那么這么說就是要給生成的代碼添加一個屬性咯不不不,是添加一組注入關系,后面生成代碼時,注解管理器就需要根據這些解析來的關系來組織生成的代碼。
本文來自于騰訊bugly開發者社區,非經作者同意,請勿轉載,原文地址:http://dev.qq.com/topic/578753c0c9da73584b025875
0、引子話說我們做程序員的,都應該多少是個懶人,我們總是想辦法驅使我們的電腦幫我們干活,所以我們學會了各式各樣的語言來告訴電腦該做什么——盡管,他們有時候也會誤會我們的意思。
突然有一天,我覺得有些代碼其實,可以按照某種規則生成,但你又不能不寫——不是所有的重復代碼都可以通過重構并采用高端技術比如泛型來消除的——比如我最痛恨的代碼:
TextView textView = (TextView) findViewById(R.id.text_view); Button button = (Button) findViewById(R.id.button);
這樣的代碼,你總不能不寫吧,真是讓人沮喪。突然想到以前背單詞的故事:正著背背不過 C,倒著背背不過 V。嗯,也許寫 Android app,也是寫不過 findViewById 的吧。。
我們今天要介紹的 ButterKnife 其實就是一個依托 Java 的注解機制來實現輔助代碼生成的框架,讀完本文,你將能夠了解到 Java 的注解處理器的強大之處,你也會對 dagger2 和 androidannotations 這樣類似的框架有一定的認識。
1、初識 ButterKnife 1.1 ButterKnife 簡介說真的,我一直對于 findViewById 這個的東西有意見,后來見到了 Afinal 這個框架,于是我們就可以直接通過注解的方式來注入,哇塞,終于可以跟 findViewById 說『Byte Byte』了,真是好開心。
什么?寨見不是介么寫么?
不過,畢竟是移動端,對于用反射實現注入的 Afinal 之類的框架,我們總是難免有一種發自內心的抵觸,于是。。。
別哭哈,不用反射也可以的~~
這個世界有家神奇的公司叫做 Square,里面有個大神叫 Jake Wharton,開源了一個神奇的框架叫做 ButterKnife,這個框架雖然也采用了注解進行注入,不過人家可是編譯期生成代碼的方式,對運行時沒有任何副作用,果真見效快,療效好,只是編譯期有一點點時間成本而已。
1.2 ButterKnife 怎么用?說句題外話,現如今做 Android 如果不知道 Jake Wharton,我覺得面試可以直接 Pass 掉了。。。哈哈,開玩笑啦
怎么介紹一個東西,那真是一個折學問題。別老說我沒文化,我的意思是比較曲折嘛。
我們還是要先簡單介紹一些 ButterKnife 的基本用法,這些知識你在 ButterKnife 這里也可以看到。
簡單來說,使用 ButterKnife 需要三步走:
配置編譯環境,由于 Butterknife 用到了注解處理器,所以,比起一般的框架,配置稍微多了些,不過也很簡單啦:
buildscript { repositories { mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:1.3.1" classpath "com.neenbedankt.gradle.plugins:android-apt:1.8" } } apply plugin: "com.neenbedankt.android-apt" ... dependencies { compile "com.jakewharton:butterknife:8.1.0" apt "com.jakewharton:butterknife-compiler:8.1.0" }
用注解標注需要注解的對象,比如 View,比如一些事件方法(用作 onClick 之類的),例:
@Bind(R.id.title) TextView title; @OnClick(R.id.hello) void sayHello() { Toast.makeText(this, "Hello, views!", LENGTH_SHORT).show(); ButterKnife.apply(headerViews, ALPHA_FADE); }
在初始化布局之后,調用 bind 方法:
setContentView(R.layout.activity_main); ButterKnife.bind(this);//一定要在 setContentView 之后哈,不然你就等著玩空指針吧
瞧,這時候你要是編譯一下,你的代碼就能歡快的跑起來啦,什么 findViewById,什么 setOnClickListener,我從來沒聽說過~
哈,不過你還是要小心一點兒,你要是有本事寫成這樣,ButterKnife 就說『信不信我報個錯給你看啊!』
@Bind(R.id.title) private TextView title; @OnClick(R.id.hello) private void sayHello() { Toast.makeText(this, "Hello, views!", LENGTH_SHORT).show(); ButterKnife.apply(headerViews, ALPHA_FADE); }
Error:(48, 22) error: @Bind fields must not be private or static. (com.example.butterknife.SimpleActivity.title) Error:(68, 18) error: @OnClick methods must not be private or static. (com.example.butterknife.SimpleActivity.sayHello)
這又是為神馬嘞?如果你知道 ButterKnife 的機制,那么這個問題就很清晰了,前面我們已經提到,ButterKnife 是通過注解處理器來生成輔助代碼進而達到自己的注入目的的,那么我們就有必要瞅瞅它究竟生成了什么鬼。
話說,生成的代碼就在 build/generated/source/apt 下面,我們就以 ButterKnife 的官方 sample 為例,它生成的代碼如下:
讓我們看一下 SimpleActivity$$ViewBinder:
public class SimpleActivity$$ViewBinderimplements ViewBinder { @Override public void bind(final Finder finder, final T target, Object source) { Unbinder unbinder = new Unbinder(target); View view; //注入 title,這里的 target 其實就是我們的 Activity view = finder.findRequiredView(source, 2130968576, "field "title""); target.title = finder.castView(view, 2130968576, "field "title""); //下面注入 hello 這個 Button,并為其設置 click 事件 view = finder.findRequiredView(source, 2130968578, "field "hello", method "sayHello", and method "sayGetOffMe""); target.hello = finder.castView(view, 2130968578, "field "hello""); unbinder.view2130968578 = view; view.setOnClickListener(new DebouncingOnClickListener() { @Override public void doClick(View p0) { target.sayHello(); } }); ... } ... }
我們看到這里面有個叫 bind 的方法,這個方法跟我們之前調用的 ButterKnife.bind的關系可想而知——其實,后者只是個引子,調用它就是為了調用生成的代碼。什么,不信?好吧,我就喜歡你們這些充滿好奇的娃。我們在調用 ButterKnife.bind 之后,會進入下面的方法:
static void bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder) { Class> targetClass = target.getClass(); try { ViewBinder
我們知道參數 target 和 source 在這里都是咱們的 Activity 的實例,那么找到的 viewBinder 又是什么鬼呢?
private static ViewBinder
簡單看下注釋就很容易理解了,如果我們的 Activity 名為 SimpleActivity,那么找到的 ViewBinder 應該就是 SimpleActivity$$ViewBinder。
還是回到我們前面的問題,如果需要注入的成員是 private,ButterKnife 會報錯,顯然,如果 title 是 private,生成的代碼中又寫到 target.title,這不就是在搞笑么?小樣兒,你以為你是生成的代碼, Java 虛擬機就會讓你看見不該看的東西么?
當然,對需要注入的成員的要求不止這些啦,我們稍后就會知道,其實對于靜態成員和某些特定包下的類的成員也是不支持注入的。
1.3 小結這個框架給我們的感覺就是,用起來炒雞簡單有木有。說話想當年,@ 給了我們上網沖浪的感覺,現在,我們仍然只需要在代碼里面 @ 幾下,就可以在后面各種浪了。
等等,這么簡單的表象后面,究竟隱藏著怎樣的秘密?它那光鮮的外表下面又有那些不可告人的故事?請看下回分解。
2、ButterKnife,給我上一盤蛋炒飯Jake 大神,我賭一個月好萊塢會員,你一定是一個吃貨。。
我們把生成代碼這個過程比作一次蛋炒飯,在炒的時候你要先準備炊具,接著準備用料,然后開炒,出鍋。
2.1 準備炊具蛋炒飯是在鍋里面炒出來的,那么我們的 ButterKnife 的"鍋"又是什么鬼?
閑話少敘,且說從我們配置好的注解,到最終生成的代碼,這是個怎樣的過程呢?
上圖很清晰嘛,雖然什么都沒說。額。。別動手。。
你看圖里面 ButterKnife 很厲害的樣子,其實丫是仗勢欺人。仗誰的勢呢?我們千呼萬喚始出來滴注解處理器,這時候就要登上歷史舞臺啦!
話說 Java 編譯器編譯代碼之前要先來個預處理,這時候編譯器會對 classpath 下面有下圖所示配置的注解處理器進行調用,那么這時候我們就可以干壞事兒了(怎么每到這個時候都會很興奮呢。。)
所以,如果你要自己寫注解處理器的話,首先要繼承 AbstractProcessor ,然后寫下類似的配置。不過稍等一下,讓我們看下 Butterknife 是怎么做的:
@AutoService(Processor.class) public final class ButterKnifeProcessor extends AbstractProcessor { ... }
AutoService 是干什么的呢?看看剛才的圖,有沒有注意到那個文件夾是紅色?是的,它是自動生成的,而負責生成這個配置的家伙就是 AutoService,這是 google 的一個開源組件,非常簡單,我就不多說了。
簡而言之:注解處理器為我們打開了一扇門,讓我們可以在 Java 編譯器編譯代碼之前,執行一段我們的代碼。當然這代碼也不一定就是要生成別的代碼了,你可以去檢查那些被注解標注的代碼的命名是否規范(周志明大神的 《深入理解 Java 虛擬機》一書當中有這個例子)。啊,你說你要去輸出一個 “Hello World”,~~(╯﹏╰)b 也可以。。吧。。
2.2 嘿蛋炒飯,最簡單又最困難既然知道了程序的入口,那么我們就要來看看 ButterKnife 究竟干了什么見不得人的事兒。在這里,所有的輸入就是我們在自己的代碼中配置的注解,所有的輸出,就是生成的用于注入對象的輔助代碼。
關于注解處理器的更多細節請大家參考相應的資料哈,我這里直接給出 ButterKnife 的核心代碼,在 ButterKnifeProcessor.process 當中:
@Override public boolean process(Set extends TypeElement> elements, RoundEnvironment env) { //下面這一句解析注解 MaptargetClassMap = findAndParseTargets(env); //解析完成以后,需要生成的代碼結構已經都有了,它們都存在于每一個 BindingClass 當中 for (Map.Entry entry : targetClassMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingClass bindingClass = entry.getValue(); try { //這一步完成真正的代碼生成 bindingClass.brewJava().writeTo(filer); } catch (IOException e) { error(typeElement, "Unable to write view binder for type %s: %s", typeElement, e.getMessage()); } } return true; }
我們知道,ButterKnife 對于需要注入對象的成員有要求的,在解析注解配置時,首先要對被標注的成員進行檢查,如果檢查失敗,直接拋異常。
在分析解析過程時,我們以 @Bind 為例,注解處理器找到用 @Bind 標注的成員,檢驗這些成員是否符合注入的條件(比如不能是 private,不能是 static 之類),之后將注解當中的值取出來,創建或者更新對應的 BindingClass。
private MapfindAndParseTargets(RoundEnvironment env) { Map targetClassMap = new LinkedHashMap<>(); Set erasedTargetNames = new LinkedHashSet<>(); // Process each @Bind element. for (Element element : env.getElementsAnnotatedWith(Bind.class)) { if (!SuperficialValidation.validateElement(element)) continue; try { parseBind(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, Bind.class, e); } } ... return targetClassMap; }
現在以前面提到的 title 為例,解析的時候拿到的 element 其實對應的就是 title 這個變量。
private void parseBind(Element element, MaptargetClassMap, Set erasedTargetNames) { ... 省略掉檢驗 element 是否符合條件的代碼 ... TypeMirror elementType = element.asType(); if (elementType.getKind() == TypeKind.ARRAY) { parseBindMany(element, targetClassMap, erasedTargetNames); } else if (LIST_TYPE.equals(doubleErasure(elementType))) { parseBindMany(element, targetClassMap, erasedTargetNames); } else if (isSubtypeOfType(elementType, ITERABLE_TYPE)) { // 顯然這里被注入的對象類型不能是 Iterable,List 除外~ error(element, "@%s must be a List or array. (%s.%s)", Bind.class.getSimpleName(), ((TypeElement) element.getEnclosingElement()).getQualifiedName(), element.getSimpleName()); } else { parseBindOne(element, targetClassMap, erasedTargetNames); } }
在注入 title 時,對應的要接著執行 parseBindOne 方法:
private void parseBindOne(Element element, MaptargetClassMap, Set erasedTargetNames) { ... 省略掉一些校驗代碼 ... if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) { ... 處理錯誤,顯然被注入的必須是 View 的子類 ... } // Assemble information on the field. int[] ids = element.getAnnotation(Bind.class).value(); if (ids.length != 1) { ... 以前已經確認是單值綁定,所以出現了參數為多個的情況就報錯... } ... 省略構建 BindingClass 對象的代碼 ... BindingClass bindingClass = targetClassMap.get(enclosingElement); String name = element.getSimpleName().toString(); TypeName type = TypeName.get(elementType); boolean required = isFieldRequired(element); // 根據注解信息來生成注入關系,并添加到 bindingClass 當中 FieldViewBinding binding = new FieldViewBinding(name, type, required); bindingClass.addField(id, binding); ... }
其實每一個注解的解析流程都是類似的,解析的最終目標就是在這個 bindingClass 里 addField,這意味著什么呢?
通過前面的分析,其實我們已經知道解析注解的最終目標是生成那些用于注入的代碼,這就好比我們讓注解管理器寫代碼。這似乎是一個很有意思的話題,如果你的程序足夠聰明,它就可以自己寫代碼~~
那么這么說 addField 就是要給生成的代碼添加一個屬性咯?不不不,是添加一組注入關系,后面生成代碼時,注解管理器就需要根據這些解析來的關系來組織生成的代碼。所以,要不要再看一下生成的代碼,看看還有沒有新的發現?
2.3、出鍋咯話說,注解配置已經解析完畢,我們已經知道我們要生成的代碼長啥樣了,那么下一個問題就是如何真正的生成代碼。這里用到了一個工具 JavaPoet,同樣出自 Square 的大神之手。JavaPoet 提供了非常強大的代碼生成功能,比如我們下面將給出生成輸出 HelloWorld 的 JavaDemo 的代碼:
MethodSpec main = MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) .build(); javaFile.writeTo(System.out);
這樣就可以生成下面的代碼了:
package com.example.helloworld; public final class HelloWorld { public static void main(String[] args) { System.out.println("Hello, JavaPoet!"); } }
其實我們自己寫個程序生成一些代碼并不難,不過導包這個事情卻非常的令人焦灼,別擔心,JavaPoet 可以把這些統統搞定。
有了個簡單的認識之后,我們要看下 ButterKnife 都用 JavaPoet 干了什么。還記得下面的代碼么:
bindingClass.brewJava().writeTo(filer);
這句代碼將 brew 出來的什么鬼東西寫到了 filer 當中,Filer 嘛發揮想象力就知道是類似于文件的東西,換句話說,這句代碼就是完成代碼生成到指定文件的過程。
Brew Java !!
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
[ ]P coffee! [ ]P
JavaFile brewJava() { TypeSpec.Builder result = TypeSpec.classBuilder(className) //添加修飾符為 public,生成的類是 public 的 .addModifiers(PUBLIC) .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass))); /*其實 Bind 過程也是有繼承關系的,我有一個 Activity A 有注入,另一個 B 繼承它,那么生成注入 B 的成員的代碼時,就要把 A 的注入一起捎上*/ if (parentViewBinder != null) { result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentViewBinder), TypeVariableName.get("T"))); } else { result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T"))); } if (hasUnbinder()) { result.addType(createUnbinderClass()); } //這一句很關鍵,我們的絕大多數注入用到的代碼都在這里了 result.addMethod(createBindMethod()); //輸出一個 JavaFile 對象(其實這里離生成最終的代碼已經很近了),完工 return JavaFile.builder(classPackage, result.build()) .addFileComment("Generated code from Butter Knife. Do not modify!") .build(); }
現在我們需要繼續看下 createBindMethod 方法,這個方法是生成代碼的關鍵~
private MethodSpec createBindMethod() { /*創建了一個叫做 bind 的方法,添加了 @Override 注解,方法可見性為 public 以及一些參數類型 */ MethodSpec.Builder result = MethodSpec.methodBuilder("bind") .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(FINDER, "finder", FINAL) .addParameter(TypeVariableName.get("T"), "target", FINAL) .addParameter(Object.class, "source"); if (hasResourceBindings()) { // Aapt can change IDs out from underneath us, just suppress since all will work at runtime. result.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType") .build()); } // Emit a call to the superclass binder, if any. if (parentViewBinder != null) { result.addStatement("super.bind(finder, target, source)"); } /* 關于 unbinder,我們一直都沒有提到過,如果我們有下面的注入配置: @Unbinder ButterKnife.Unbinder unbinder; * 那么這時候就會在生成的代碼中添加下面的代碼,這實際上就是構造 unbinder */ // If the caller requested an unbinder, we need to create an instance of it. if (hasUnbinder()) { result.addStatement("$T unbinder = new $T($N)", unbinderBinding.getUnbinderClassName(), unbinderBinding.getUnbinderClassName(), "target"); } /* * 這里就是注入 view了,addViewBindings 這個方法其實就生成功能上類似 TextView textView = (TextView) findViewById(...) 的代碼 */ if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) { // Local variable in which all views will be temporarily stored. result.addStatement("$T view", VIEW); // Loop over each view bindings and emit it. for (ViewBindings bindings : viewIdMap.values()) { addViewBindings(result, bindings); } // Loop over each collection binding and emit it. for (Map.Entryentry : collectionBindings.entrySet()) { emitCollectionBinding(result, entry.getKey(), entry.getValue()); } } /* * 注入 unbinder */ // Bind unbinder if was requested. if (hasUnbinder()) { result.addStatement("target.$L = unbinder", unbinderBinding.getUnbinderFieldName()); } /* ButterKnife 其實不止支持注入 View, 還支持注入 字符串,主題,圖片。。 * 所有資源里面你能想象到的東西 */ if (hasResourceBindings()) { //篇幅有限,我還是省略掉他們吧 ... } return result.build(); }
不知道為什么,這段代碼讓我想起了我寫代碼的樣子。。那分明就是 ButterKnife 在替我們寫代碼嘛。
當然,這只是生成的代碼中最重要的最核心的部分,為了方便理解,我把 demo 里面生成的這個方法列出來方便查看:
@Override public void bind(final Finder finder, final T target, Object source) { //構造 unbinder Unbinder unbinder = new Unbinder(target); //下面開始 注入 view View view; view = finder.findRequiredView(source, 2130968576, "field "title""); target.title = finder.castView(view, 2130968576, "field "title""); //... 省略掉其他成員的注入 ... //注入 unbinder target.unbinder = unbinder; }3、Hack 一下,定義我們自己的注解 BindLayout
我一直覺得,既然 View 都能注入了,咱能不能把 layout 也注入了呢?顯然這沒什么難度嘛,可為啥 Jake 大神沒有做這個功能呢?我覺得主要是因為。。。你想哈,你注入個 layout,大概要這么寫
@BindLayout(R.layout.main) public class AnyActivity extends Activity{...}
可我們平時怎么寫呢?
public class AnyActivity extends Activity{ @Override protected void onCreate(Bundle savedInstances){ super.onCreate(savedInstances); setContentView(R.layout.main); } }
你別說你不繼承 onCreate 方法啊,所以好像始終要寫一句,性價比不高?誰知道呢。。。
不過呢,咱們接下來就運用我們的神功,給 ButterKnife 添磚加瓦(這怎么感覺像校長說的呢。。嗯,他說的是社河會蟹主@義),讓 ButterKnife 可以 @BindLayout。先看效果:
//注入 layout @BindLayout(R.layout.simple_activity) public class SimpleActivity extends Activity { ... }
生成的代碼:
public class SimpleActivity$$ViewBinderimplements ViewBinder { @Override public void bind(final Finder finder, final T target, Object source) { //生成了這句代碼來注入 layout target.setContentView(2130837504); //下面省略掉的代碼我們已經見過啦,就是注入 unbinder,注入 view ... } ... }
那么我們要怎么做呢?一個字,順藤摸瓜~
第一步,當然是要定義注解 BindLayout
@Retention(CLASS) @Target(TYPE) public @interface BindLayout { @LayoutRes int value(); }
第二步,我們要去注解處理器里面添加對這個注解的支持:
@Override public SetgetSupportedAnnotationTypes() { Set types = new LinkedHashSet<>(); ... types.add(BindLayout.class.getCanonicalName()); ... return types; }
第三步,注解處理器的解析環節要添加支持:
private MapfindAndParseTargets(RoundEnvironment env) { Map targetClassMap = new LinkedHashMap<>(); Set erasedTargetNames = new LinkedHashSet<>(); // Process each @Bind element. for (Element element : env.getElementsAnnotatedWith(BindLayout.class)) { if (!SuperficialValidation.validateElement(element)) continue; try { parseBindLayout(element, targetClassMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindLayout.class, e); } } ... }
下面是 parseBindLayout 方法:
private void parseBindLayout(Element element, MaptargetClassMap, Set erasedTargetNames) { /*與其他注解解析不同,BindLayout 標注的類型就是 TYPE,所以這里直接強轉為 TypeElement,其實就是對應于 Activity 的類型*/ TypeElement typeElement = (TypeElement) element; Set modifiers = element.getModifiers(); // 只有 private 不可以訪問到,static 類型不影響,這也是與其他注解不同的地方 if (modifiers.contains(PRIVATE)) { error(element, "@%s %s must not be private. (%s.%s)", BindLayout.class.getSimpleName(), "types", typeElement.getQualifiedName(), element.getSimpleName()); return; } // 同樣的,對于 android 開頭的包內的類不予支持 String qualifiedName = typeElement.getQualifiedName().toString(); if (qualifiedName.startsWith("android.")) { error(element, "@%s-annotated class incorrectly in Android framework package. (%s)", BindLayout.class.getSimpleName(), qualifiedName); return; } // 同樣的,對于 java 開頭的包內的類不予支持 if (qualifiedName.startsWith("java.")) { error(element, "@%s-annotated class incorrectly in Java framework package. (%s)", BindLayout.class.getSimpleName(), qualifiedName); return; } /* 我們暫時只支持 Activity,如果你想支持 Fragment,需要區別對待哈, 因為二者初始化 View 的代碼不一樣 */ if(!isSubtypeOfType(typeElement.asType(), ACTIVITY_TYPE)){ error(element, "@%s fields must extend from View or be an interface. (%s.%s)", BindLayout.class.getSimpleName(), typeElement.getQualifiedName(), element.getSimpleName()); return; } // 拿到注解傳入的值,比如 R.layout.main int layoutId = typeElement.getAnnotation(BindLayout.class).value(); if(layoutId == 0){ error(element, "@%s for a Activity must specify one layout ID. Found: %s. (%s.%s)", BindLayout.class.getSimpleName(), layoutId, typeElement.getQualifiedName(), element.getSimpleName()); return; } BindingClass bindingClass = targetClassMap.get(typeElement); if (bindingClass == null) { bindingClass = getOrCreateTargetClass(targetClassMap, typeElement); } // 把這個布局的值塞給 bindingClass,這里我只是簡單的存了下這個值 bindingClass.setContentLayoutId(layoutId); log(element, "element:" + element + "; targetMap:" + targetClassMap + "; erasedNames: " + erasedTargetNames); }
第四步,添加相應的生成代碼的支持,這個在 BindingClass.createBindMethod 當中:
private MethodSpec createBindMethod() { MethodSpec.Builder result = MethodSpec.methodBuilder("bind") .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(FINDER, "finder", FINAL) .addParameter(TypeVariableName.get("T"), "target", FINAL) .addParameter(Object.class, "source"); if (hasResourceBindings()) { ... 省略之 ... } //如果 layoutId 不為 0 ,那說明有綁定,添加一句 setContentView 完事兒~~ //要注意的是,這句要比 view 注入在前面。。。你懂的,不然自己去玩空指針 if(layoutId != 0){ result.addStatement("target.setContentView($L)", layoutId); } ... }
這樣,我們就可以告別 setContentView 了,寫個注解,非常清爽,隨意打開個 Activity 一眼就看到了布局在哪里,哈哈哈哈哈
其實是說你胖。。
4、androidannotations 和 dagger2 4.1 androidannotationsandroidannotations 同樣是一個注入工具,如果你稍微接觸一下它,你就會發現它的原理與 ButterKnife 如出一轍。下面我們給出其中非常核心的代碼:
private void processThrowing(Set extends TypeElement> annotations, RoundEnvironment roundEnv) throws Exception { if (nothingToDo(annotations, roundEnv)) { return; } AnnotationElementsHolder extractedModel = extractAnnotations(annotations, roundEnv); AnnotationElementsHolder validatingHolder = extractedModel.validatingHolder(); androidAnnotationsEnv.setValidatedElements(validatingHolder); try { AndroidManifest androidManifest = extractAndroidManifest(); LOGGER.info("AndroidManifest.xml found: {}", androidManifest); IRClass rClass = findRClasses(androidManifest); androidAnnotationsEnv.setAndroidEnvironment(rClass, androidManifest); } catch (Exception e) { return; } AnnotationElements validatedModel = validateAnnotations(extractedModel, validatingHolder); ModelProcessor.ProcessResult processResult = processAnnotations(validatedModel); generateSources(processResult); }
我們就簡單看下,其實也是注解解析和代碼生成幾個步驟,當然,由于 androidannotations 支持的功能要復雜的多,不僅僅包含 UI 注入,還包含線程切換,網絡請求等等,因此它的注解解析邏輯也要復雜得多,閱讀它的源碼時,建議多多關注一下它的代碼結構設計,非常不錯。
從使用的角度來說,ButterKnife 只是針對 UI 進行注入,功能比較單一,而 androidannotations 真是有些龐大和強大,究竟使用哪一個框架,那要看具體需求了。
4.2 Dagger 2Dagger 2 算是超級富二代了,媽是 Square,爹是 Google—— Dagger 2 源自于 Square 的開源項目,目前已經由 Google 接管(怎么感覺 Google 喜當爹的節奏 →_→)。
Dagger 本是一把利刃,它也是用來注入成員的一個框架,不過相對于前面的兩個框架,它
顯得更基礎,因為它不針對具體業務
顯得更通用,因為它不依賴運行平臺
顯得更復雜,因為它更關注于對象間的依賴關系
用它的開發者說的一句話就是(大意):有一天,我們發現我們的構造方法居然需要 3000 行,這時候我們意識到是時候寫一個框架幫我們完成構造方法了。
換句話說,如果你的構造方法沒有那么長,其實也沒必要引入 Dagger 2,因為那樣會讓你的代碼顯得。。。不是那么的好懂。
當然,我們放到這里提一下 Dagger 2,是因為它 完全去反射,實現的思想與前面提到的兩個框架也是一毛一樣啊。所以你可以不假思索的說,Dagger 2 肯定至少有兩個模塊,一個是 compiler,里面有個注解處理器;還有一個是運行時需要依賴的模塊,主要提供 Dagger 2 的注解支持等等。
5、小結本文通過對 ButterKnife 的源碼的分析,我們了解到了 ButterKnife 這樣的注入框架的實現原理,同時我們也對 Java 的注解處理機制有了一定的認識;接著我們還對 ButterKnife 進行了擴充的簡單嘗試——總而言之,使用起來非常簡單的 ButterKnife 框架的實現實際上涉及了較多的知識點,這些知識點相對生僻,卻又非常的強大,我們可以利用這些特性來實現各種各樣個性化的需求,讓我們的工作效率進一步提高。
來吧,解放我們的雙手!
更多精彩內容歡迎關注bugly的微信公眾賬號:
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/66215.html
今年3月,Google 破天荒提前半年發布了 Android N 開發者預覽版。當然,作為一個不合格的谷粉并沒有第一時間體驗安裝,因為至今仍然能夠回憶起來去年今日此門中(霧)興沖沖刷了 Android M Preview 的時候發現各種 Crash 就連微信也(不出所料得)中招時自己一臉懵逼的心情。當然,為自己的機智而慶幸并沒有過多久,很快就有微信好友(當然也是純純的谷粉)反饋微信又雙叒叕在 An...
摘要:不努力不奮斗,可能就會在基層一輩子止步不前。不過,只一句,如果你還在做這一行,還是一名程序猿媛,想走上坡路的你,也許我這到手的十幾家一線互聯網公司性能優化項目實戰可能會對你有所幫助。 ...
閱讀 4751·2021-11-15 11:39
閱讀 2698·2021-11-11 16:55
閱讀 2206·2021-10-25 09:44
閱讀 3511·2021-09-22 16:02
閱讀 2441·2019-08-30 15:55
閱讀 3129·2019-08-30 13:46
閱讀 2670·2019-08-30 13:15
閱讀 1958·2019-08-30 11:12