【騰訊Bugly乾貨分享】深刻理解 ButterKnife,讓你的程序學會寫代碼

本文來自於騰訊bugly開發者社區,非經做者贊成,請勿轉載,原文地址:http://dev.qq.com/topic/578753c0c9da73584b025875java

0、引子

話說咱們作程序員的,都應該多少是個懶人,咱們老是想辦法驅使咱們的電腦幫咱們幹活,因此咱們學會了各式各樣的語言來告訴電腦該作什麼——儘管,他們有時候也會誤會咱們的意思。android

忽然有一天,我以爲有些代碼其實,能夠按照某種規則生成,但你又不能不寫——不是全部的重複代碼均可以經過重構並採用高端技術好比泛型來消除的——好比我最痛恨的代碼:git

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 這樣相似的框架有必定的認識。github

一、初識 ButterKnife

1.1 ButterKnife 簡介

說真的,我一直對於 findViewById 這個的東西有意見,後來見到了 Afinal 這個框架,因而咱們就能夠直接經過註解的方式來注入,哇塞,終於能夠跟 findViewById 說『Byte Byte』了,真是好開心。面試

什麼?寨見不是介麼寫麼?緩存

不過,畢竟是移動端,對於用反射實現注入的 Afinal 之類的框架,咱們老是不免有一種發自心裏的抵觸,因而。。。微信

title

別哭哈,不用反射也能夠的~~網絡

這個世界有家神奇的公司叫作 Square,裏面有個大神叫 Jake Wharton,開源了一個神奇的框架叫作 ButterKnife,這個框架雖然也採用了註解進行注入,不過人家但是編譯期生成代碼的方式,對運行時沒有任何反作用,果然見效快,療效好,只是編譯期有一點點時間成本而已。app

說句題外話,現現在作 Android 若是不知道 Jake Wharton,我以爲面試能夠直接 Pass 掉了。。。哈哈,開玩笑啦

title

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 爲例,它生成的代碼以下:

title

讓咱們看一下 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) {
      //省略異常處理
    }
  }

咱們知道參數 targetsource 在這裏都是我們的 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 會報錯,顯然,若是 titleprivate,生成的代碼中又寫到 target.title,這不就是在搞笑麼?小樣兒,你覺得你是生成的代碼, Java 虛擬機就會讓你看見不應看的東西麼?

固然,對須要注入的成員的要求不止這些啦,咱們稍後就會知道,其實對於靜態成員和某些特定包下的類的成員也是不支持注入的。

1.3 小結

這個框架給咱們的感受就是,用起來炒雞簡單有木有。說話想當年,@ 給了咱們上網衝浪的感受,如今,咱們仍然只須要在代碼裏面 @ 幾下,就能夠在後面各類浪了。

等等,這麼簡單的表象後面,究竟隱藏着怎樣的祕密?它那光鮮的外表下面又有那些不可告人的故事?請看下回分解。

二、ButterKnife,給我上一盤蛋炒飯

title

Jake 大神,我賭一個月好萊塢會員,你必定是一個吃貨。。

咱們把生成代碼這個過程比做一次蛋炒飯,在炒的時候你要先準備炊具,接着準備用料,而後開炒,出鍋。

2.1 準備炊具

蛋炒飯是在鍋裏面炒出來的,那麼咱們的 ButterKnife 的"鍋"又是什麼鬼?

閒話少敘,且說從咱們配置好的註解,到最終生成的代碼,這是個怎樣的過程呢?

title

上圖很清晰嘛,雖然什麼都沒說。額。。別動手。。

你看圖裏面 ButterKnife 很厲害的樣子,其實丫是仗勢欺人。仗誰的勢呢?咱們千呼萬喚始出來滴注解處理器,這時候就要登上歷史舞臺啦!

話說 Java 編譯器編譯代碼以前要先來個預處理,這時候編譯器會對 classpath 下面有下圖所示配置的註解處理器進行調用,那麼這時候咱們就能夠幹壞事兒了(怎麼每到這個時候都會很興奮呢。。)

title

因此,若是你要本身寫註解處理器的話,首先要繼承 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 對於須要注入對象的成員有要求的,在解析註解配置時,首先要對被標註的成員進行檢查,若是檢查失敗,直接拋異常。

title

在分析解析過程時,咱們以 @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);

    ...
  }

其實每個註解的解析流程都是相似的,解析的最終目標就是在這個 bindingClassaddField,這意味着什麼呢?

經過前面的分析,其實咱們已經知道解析註解的最終目標是生成那些用於注入的代碼,這就比如咱們讓註解管理器寫代碼。這彷佛是一個頗有意思的話題,若是你的程序足夠聰明,它就能夠本身寫代碼~~

那麼這麼說 addField 就是要給生成的代碼添加一個屬性咯?不不不,是添加一組注入關係,後面生成代碼時,註解管理器就須要根據這些解析來的關係來組織生成的代碼。因此,要不要再看一下生成的代碼,看看還有沒有新的發現?

2.三、出鍋咯

話說,註解配置已經解析完畢,咱們已經知道咱們要生成的代碼長啥樣了,那麼下一個問題就是如何真正的生成代碼。這裏用到了一個工具 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;
  }

三、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 一眼就看到了佈局在哪裏,哈哈哈哈哈

title

實際上是說你胖。。

四、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 的註解支持等等。

五、小結

本文經過對 ButterKnife 的源碼的分析,咱們瞭解到了 ButterKnife 這樣的注入框架的實現原理,同時咱們也對 Java 的註解處理機制有了必定的認識;接着咱們還對 ButterKnife 進行了擴充的簡單嘗試——總而言之,使用起來很是簡單的 ButterKnife 框架的實現實際上涉及了較多的知識點,這些知識點相對生僻,卻又很是的強大,咱們能夠利用這些特性來實現各類各樣個性化的需求,讓咱們的工做效率進一步提升。

來吧,解放咱們的雙手!

更多精彩內容歡迎關注bugly的微信公衆帳號:

相關文章
相關標籤/搜索