天天看點

【騰訊Bugly幹貨分享】深入了解 ButterKnife,讓你的程式學會寫代碼

話說我們做程式員的,都應該多少是個懶人,我們總是想辦法驅使我們的電腦幫我們幹活,是以我們學會了各式各樣的語言來告訴電腦該做什麼——盡管,他們有時候也會誤會我們的意思。

本文來自于騰訊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,這個架構雖然也采用了注解進行注入,不過人家可是編譯期生成代碼的方式,對運作時沒有任何副作用,果真見效快,療效好,隻是編譯期有一點點時間成本而已。

說句題外話,現如今做 Android 如果不知道 Jake Wharton,我覺得面試可以直接 Pass 掉了。。。哈哈,開玩笑啦

1.2 ButterKnife 怎麼用?

怎麼介紹一個東西,那真是一個折學問題。别老說我沒文化,我的意思是比較曲折嘛。

我們還是要先簡單介紹一些 ButterKnife 的基本用法,這些知識你在 ButterKnife 這裡也可以看到。

簡單來說,使用 ButterKnife 需要三步走:

  1. 配置編譯環境,由于 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'
     }
               
  2. 用注解标注需要注解的對象,比如 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);
     }
               
  3. 在初始化布局之後,調用

    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$$ViewBinder<T extends SimpleActivity> implements ViewBinder<T> {
    @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<Object> viewBinder = findViewBinderForClass(targetClass);
       viewBinder.bind(finder, target, source);
     } catch (Exception e) {
       //省略異常處理
     }
    }
               
    我們知道參數

    target

    source

    在這裡都是咱們的

    Activity

    的執行個體,那麼找到的

    viewBinder

    又是什麼鬼呢?
    private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
       throws IllegalAccessException, InstantiationException {
     ViewBinder<Object> viewBinder = BINDERS.get(cls);
     //先找緩存
     if (viewBinder != null) {
       return viewBinder;
     }
     //檢查下是否支援這個類
     String clsName = cls.getName();
     if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
       return NOP_VIEW_BINDER;
     }
     try {
       //找到類名為 Activity 的類名加 "$$ViewBinder" 的類,執行個體化,并傳回
       Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
       //noinspection unchecked
       viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
     } catch (ClassNotFoundException e) {
       //注意這裡支援了繼承關系
       viewBinder = findViewBinderForClass(cls.getSuperclass());
     }
     //緩存 viewBinder
     BINDERS.put(cls, viewBinder);
     return 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) {
     //下面這一句解析注解
     Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
    
     //解析完成以後,需要生成的代碼結構已經都有了,它們都存在于每一個 BindingClass 當中
     for (Map.Entry<TypeElement, BindingClass> 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 Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
     Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
     Set<String> 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, Map<TypeElement, BindingClass> targetClassMap,
       Set<String> 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, Map<TypeElement, BindingClass> targetClassMap,
       Set<String> 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.Entry<FieldCollectionViewBinding, int[]> entry : 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$$ViewBinder<T extends SimpleActivity> implements ViewBinder<T> {
   @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 Set<String> getSupportedAnnotationTypes() {
     Set<String> types = new LinkedHashSet<>();
     ...
     types.add(BindLayout.class.getCanonicalName());
     ...
     return types;
   }
           

第三步,注解處理器的解析環節要添加支援:

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
     Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
     Set<String> 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, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) {
     /*與其他注解解析不同,BindLayout 标注的類型就是 TYPE,是以這裡直接強轉為 
      TypeElement,其實就是對應于 Activity 的類型*/
     TypeElement typeElement = (TypeElement) element;
     Set<Modifier> 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 androidannotations

androidannotations 同樣是一個注入工具,如果你稍微接觸一下它,你就會發現它的原理與 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 2

Dagger 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的微信公衆賬号:

    騰訊 Bugly是一款專為移動開發者打造的品質監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合并功能幫助開發同學把每天上報的數千條 Crash 根據根因合并分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在釋出後快速的了解應用的品質情況,适配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!