手寫ButterKnife注解框架

[TOC]

zero bind library是一個仿ButterKnife的編譯期注解框架的練習,旨在熟悉編譯期注解和注解處理器的工作原理以及相關的API。當前基本都使用Android Studio進行android開發,因此這個練習也基于AS開發環境(AS3.0, gradle-4.1-all, com.android.tools.build:gradle:3.0.0)。練習中大量參考了ButterKnife的源碼,這些代碼基本都源于ButterKnife,甚至目錄結構和gradle的一些配置和編寫風格,注釋未及之處參考JakeWharton/butterknife 。筆者水平有限,錯誤在所難免,歡迎批評指正。

關于Processor

為了能更好的了解注解處理器在處理注解時進行了那些操作,代碼調試的功能似乎是必不可少的,然而注解處理器是在javac之前執行,所以直接在處理器中打斷點然后運行是調試不到注解處理器的。可以搜索相關的文章了解,比如這個如何調試編譯時注解處理器AnnotationProcessor ,鑒于調試的麻煩,剛開始了解Processor可以使用類似于打印日志的方式,這里需要注意的是System.out.println()無法在控制臺打印日志,因此首先搭建一個具有日志輸出功能的Processor。以下給出一個LoggerProcessor

package zero.annotation.processor;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.tools.Diagnostic;

public abstract class LoggerProcessor extends AbstractProcessor {

  private Messager messager;

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    messager = processingEnv.getMessager();
  }

  protected void error(Element element, String message, Object... args) {
    printMessage(Diagnostic.Kind.ERROR, element, message, args);
  }

  protected void note(Element element, String message, Object... args) {
    printMessage(Diagnostic.Kind.NOTE, element, message, args);
  }

  private void printMessage(Diagnostic.Kind kind, Element element, String message, Object[] args) {
    if (args.length > 0) {
      message = String.format(message, args);
    }
    messager.printMessage(kind, message, element);
  }
}

Processor#init顧名思義對注解處理器進行一些配置,如這里獲取Message對象。注解處理器框架涉及到大量的接口,這些接口用于幫助我們對注解進行處理,比如ProcessorMessagerElement等等都是接口。

Messager#printMessage(Diagnostic.Kind, CharSequence, Element)

    /**
     * Prints a message of the specified kind at the location of the
     * element.
     *
     * @param kind the kind of message
     * @param msg  the message, or an empty string if none
     * @param e    the element to use as a position hint
     */
    void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e);

這里傳入的參數Element用于源碼的定位,比如處理注解時警告或者錯誤信息。上面的note()方法使用后note(element, "bind with layout id = %#x", id)的效果如:

/home/jmu/AndroidStudioProjects/zero/sample/src/main/java/com/example/annotationtest/MainActivity.java:9: 注: bind with layout id = 0x7f09001b
public class MainActivity extends AppCompatActivity {
       ^

error()將使得注解處理器在調用處打印錯誤信息,并導致最終編譯失敗:

...MainActivity.java:9: 錯誤: bind with layout id = 0x7f09001b
public class MainActivity extends AppCompatActivity {
       ^
2 個錯誤

:sample:compileDebugJavaWithJavac FAILED

FAILURE: Build failed with an exception.

有了這兩個日志方法,就可以在適當的時候在控制臺打印想要了解的信息。

第一個注解@ContentView

ContentView.java

package zero.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ContentView {
  int value();
}

這個注解使用在Activity類上,為Activity指定布局。類似于ButterKnife(ButterKnife不提供類似的注解),@ContentView的作用使得我們將來要在

package com.example.annotationtest;

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Zero.bind(this);
  }
}

Zero.bind(this)之后調用注解處理器生成的java代碼Activity.setContentView(id),注意不是使用反射來調用Activity.setContentView

ContentViewProcessor

package zero.annotation.processor;

@SupportedAnnotationTypes({"zero.annotation.ContentView"})
public class ContentViewProcessor extends LoggerProcessor {
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    Set<? extends Element> elements = env.getElementsAnnotatedWith(ContentView.class);
    for (Element element : elements) {
      Element enclosingElement = element.getEnclosingElement();
      System.out.println(enclosingElement.getClass());
      int id = element.getAnnotation(ContentView.class).value();
      note(element, "bind with layout id = %#x", id);
    }
    return true;
  }

  @Override
  public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
  }
}

再議Processor(詳見api)

  1. Set<String> getSupportedAnnotationTypes();

    指定該注解處理器可以處理那些注解,重寫該方法返回一個Set<String>或者在處理器上使用注解@SupportedAnnotationTypes

  2. SourceVersion getSupportedSourceVersion();

    支持的java編譯器版本,重寫或者使用@SupportedSourceVersion注解

  3. void init(ProcessingEnvironment processingEnv);

    Initializes the processor with the processing environment.

  4. boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);

    處理注解的方法,待處理的注解通過參數annotations傳遞,返回值表示注解是否已被處理,roundEnv表示當前和之前的處理環境。

上面的代碼簡單的遍歷了使用@ContentView的類,并將其中的布局文件id打印在控制臺(驗證System.out.println是否生效)。我們循序漸進旨在能在探索中了解Processor 。為了在AS上使用該處理器,需要進行一些配置,這些配置相比eclipse相對簡單。

//1.結構
sample
├── build.gradle
├── proguard-rules.pro
└── src
    └── main
        ├── AndroidManifest.xml
        └── java/android/com/example/annotationtest
                                    └── MainActivity.java
zero-annotation
├── build.gradle
└── src/main/java/zero/annotation
                       └── ContentView.java

zero-annotation-processor/
├── build.gradle
└── src/main
        ├── java/zero/annotation/processor
        │                        ├── ContentViewProcessor.java
        │                        └── LoggerProcessor.java
        └── resources/META-INF/services
                               └── javax.annotation.processing.Processor
                               
//2.1 javax.annotation.processing.Processor內容
zero.annotation.processor.ContentViewProcessor

//2.2 sample/build.gradle依賴部分
dependencies {
    //其他依賴...
    annotationProcessor project(path: ':zero-annotation-processor')
    api project(path: ':zero-annotation')
}

對比eclipse下的配置,as中只需要上面的2.1,2.2即可使用自定義的注解處理器。

Processor生成java代碼

建立Android library :zero, 依賴

zero
├── build.gradle
├── proguard-rules.pro
└── src/main
        ├── AndroidManifest.xml
        └── java/zero
                ├── IContent.java
                └── Zero.java

//IContent.java
public interface IContent {
  void setContentView(Activity activity);
}

//build.gradle.dependencies
dependencies {
    ...
    annotationProcessor project(path: ':zero-annotation-processor')
    compile project(path: ':zero-annotation-processor')
}

提供IContent接口,希望使用了@ContentView后的Activity可以在同目錄下生成一個形如Activity$$ZeroBind的類,并且實現IContent接口,如MainActivity$$ZeroBind

// Generated code from Zero library. Do not modify!
package com.example.annotationtest;

public class MainActivity$$ZeroBind implements zero.IContent {

  @Override
  public void setContentView(android.app.Activity activity) {
    activity.setContentView(2131296283);
  }
}

當使用Zero.bind(this)時,反射創建MainActivity$$ZeroBind對象,調用IContent.setContentView來為MainActivity設置布局。因此下面的小目標就是通過Processor生成MainActivity$$ZeroBind.java文件:

@SupportedAnnotationTypes({"zero.annotation.ContentView"})
public class ContentViewProcessor extends LoggerProcessor {

  public static final String SUFFIX = "$$ZeroBind";

  private Filer filer;
  private Elements elementUtils;
  private Types typeUtils;

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    filer = processingEnv.getFiler();
    elementUtils = processingEnv.getElementUtils();
    typeUtils = processingEnv.getTypeUtils();
  }

  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    Set<? extends Element> elements = env.getElementsAnnotatedWith(ContentView.class);
    for (Element element : elements) {
//      Element enclosingElement = element.getEnclosingElement();
//      note(enclosingElement, "%s", enclosingElement.getClass().getSuperclass());
      int id = element.getAnnotation(ContentView.class).value();
//      note(element, "bind with layout id = %#x", id);
      TypeMirror typeMirror = element.asType();
//      note(element, "%s\n%s", typeMirror.toString(), typeMirror.getKind());

      try {
        String classFullName = typeMirror.toString() + SUFFIX;
        JavaFileObject sourceFile = filer.createSourceFile(classFullName, element);
        Writer writer = sourceFile.openWriter();
        TypeElement typeElement = elementUtils.getTypeElement(typeMirror.toString());
        PackageElement packageOf = elementUtils.getPackageOf(element);
        writer.append("http:// Generated code from Zero library. Do not modify!\n")
          .append("package ").append(packageOf.getQualifiedName()).append(";\n\n")
          .append("public class ").append(typeElement.getSimpleName()).append(SUFFIX).append(" implements zero.IContent {\n\n")
          .append("  @Override\n")
          .append("  public void setContentView(android.app.Activity activity) {\n")
          .append("    activity.setContentView(").append(String.valueOf(id)).append(");\n")
          .append("  }\n")
          .append("}")
          .flush();
        writer.close();
      } catch (IOException e) {
        error(element, "不能寫入java文件!");
      }
    }
    return true;
  }

  @Override
  public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
  }
}

通過上面的處理器將產生MainActivity$$ZeroBind.java文件在:

sample/
├── build
    └── generated
        └── source
            └── apt
                └── debug
                    └── com
                        └── example
                            └── annotationtest
                                └── MainActivity$$ZeroBind.java
 //注解處理器生成的源代碼都在 build/generated/source/apt目錄下

這個源碼與MainActivity在同一個包中,因此可以訪問到MainActivity中的包級成員。

為了說明上面的代碼以及理解,需要一些準備知識。

javax.lang.model包

描述
javax.lang.model Classes and hierarchies of packages used to model the Java programming language.
javax.lang.model.element Interfaces used to model elements of the Java programming language.
javax.lang.model.type Interfaces used to model Java programming language types.
javax.lang.model.util Utilities to assist in the processing of program elements and types.

主要介紹:ElementTypeMirror

Element

參看https://docs.oracle.com/javase/7/docs/api/javax/lang/model/element/Element.html

All Known Subinterfaces:

ExecutableElement, PackageElement, Parameterizable, QualifiedNameable, TypeElement, TypeParameterElement, VariableElement

繼承關系
Element
    PackageElement (javax.lang.model.element)
    ExecutableElement (javax.lang.model.element)
    VariableElement (javax.lang.model.element)
    TypeElement (javax.lang.model.element)
    QualifiedNameable (javax.lang.model.element)
        PackageElement (javax.lang.model.element)
        TypeElement (javax.lang.model.element)
    Parameterizable (javax.lang.model.element)
        ExecutableElement (javax.lang.model.element)
        TypeElement (javax.lang.model.element)
    TypeParameterElement (javax.lang.model.element)

public interface Element

代表程序中的元素,如包、類或方法。每個元素表示一個靜態的、語言級的構造(不是運行時虛擬機構造的)。

元素的比較應該使用 equals(Object) 方法. 不能保證任何特定元素總是由同一對象表示。

實現基于一個 Element 對象的類的操作, 使用 visitor 或者 getKind() 方法. 由于一個實現類可以選擇多個 Element 的子接口,使用 instanceof 來決定在這種繼承關系中的一個對象的實際類型未必是可靠的。

package com.example.demo;//[PackageElement, ElementKind.PACKAGE]
public class Main {//[TypeElement,ElementKind.CLASS]
  int a;//[VariableElement, ElementKind.FIELD]
  
  static {//[ExecutableElement, ElementKind.STATIC_INIT]
    System.loadLibrary("c");
  }
  {//[ExecutableElement, ElementKind.INSTANCE_INIT]
    a = 100;
  }
  public Main(){//[ExecutableElement,ElementKind.CONSTRUCTOR]
    int b = 10;//[VariableElement, ElementKind.LOCAL_VARIABLE]
  }
  
  public String toString(){//[ExecutableElement, ElementKind.METHOD]
    return super.toString();
  }
}

public @interface OnClick{//[TypeElement, ElementKind.ANNOTATION_TYPE]
  
}

public interface Stack<T>{//[TypeElement,ElementKind.INTERFACE]
  T top;//[VariableElement, ElementKind.FIELD, TypeKind.TYPEVAR]
  TypeNotExists wtf;//[VariableElement, ElementKind.FIELD, TypeKind.ERROR]
}

Method Detail

  1. TypeMirror asType() 返回元素定義的類型

    一個泛型元素定義一族類型,而不是一個。泛型元素返回其原始類型. 這是元素在類型變量相應于其形式類型參數上的調用. 例如, 對于泛型元素 C<N extends Number>, 返回參數化類型 C<N> . Types 實用接口有更通用的方法來獲取元素定義的所有類型的范圍。

  2. ElementKind getKind() 返回元素的類型

  3. List<? extends AnnotationMirror> getAnnotationMirrors() 返回直接呈現在元素上的注解

    使用getAllAnnotationMirrors可以獲得繼承來的注解

  4. <A extends Annotation> A getAnnotation(Class<A> annotationType)

    返回呈現在元素上的指定注解實例,不存在返回null 。注解可以直接直接呈現或者繼承。

  5. Set<Modifier> getModifiers() 返回元素的修飾符

  6. Name getSimpleName() 返回元素的簡單名字

    泛型類的名字不帶任何形式類型參數,比如 java.util.Set<E> 的SimpleName是 "Set". 未命名的包返回空名字, 構造器返回"<init>",靜態代碼快返回 "<clinit>" , 匿名內部類或者構造代碼快返回空名字.

  7. Element getEnclosingElement()

    返回元素所在的最里層元素, 簡言之就是閉包.

    • 如果該元素在邏輯上直接被另一個元素包裹,返回該包裹的元素
    • 如果是一個頂級類, 返回包元素(PackageElement)
    • 如果是包元素返回null
    • 如果是類型參數或泛型元素,返回類型參數(TypeParametrElement)
  8. List<? extends Element> getEnclosedElements()

    返回當前元素直接包裹的元素集合。類和接口視為包裹字段、方法、構造器和成員類型。 這包括了任何隱式的默認構造方法,枚舉中的valuesvalueOf方法。包元素包裹在其中的頂級類和接口,但不認為包裹了子包。其他類型的元素當前默認不包裹任何元素,但可能 跟隨API和編程語言而變更。

    注意某些類型的元素可以通過 ElementFilter中的方法分離出來.

TypeMirror

參考https://docs.oracle.com/javase/7/docs/api/javax/lang/model/type/TypeMirror.html

All Known Subinterfaces:

ArrayType, DeclaredType, ErrorType, ExecutableType, NoType, NullType, PrimitiveType, ReferenceType, TypeVariable, UnionType, WildcardType

public interface TypeMirror

表示java中的一個類型. 類型包含基本類型、聲明類型 (類和接口)、數組、類型變量和null 類型. 也表示通配符類型參數(方法簽名和返回值中的), 以及對應包和關鍵字 void的偽類型.

類型的比較應該使用 Types. 不能保證任何特定類型總是由同一對象表示。

實現基于一個 TypeMirror 對象的類的操作, 使用 visitor 或者 getKind() 方法. 由于一個實現類可以選擇多個 TypeMirror 的子接口,使用 instanceof 來決定在這種繼承關系中的一個對象的實際類型未必是可靠的。

Utility

javax.lang.model.util下的接口(主要指ElementsTypes)擁有一些實用的方法。

  1. PackageElement Elements.getPackageOf(Element type)

    Returns the package of an element. The package of a package is itself.

  2. TypeElement Elements.getTypeElement(CharSequence name)

    Returns a type element given its canonical name.

  3. boolean Types.isAssignable(TypeMirror t1, TypeMirror t2)

    Tests whether t1 is assignable to t2.

  4. boolean Types.isSameType(TypeMirror t1, TypeMirror t2)

    Tests whether two TypeMirror objects represent the same type. Return true if and only if the two types are the same

  5. boolean Types.isSubtype(TypeMirror t1, TypeMirror t2)

    Return true if and only if the t1 is a subtype of t2

Process生成java代碼續

現在我們詳細注釋下ContentViewProcessor#process ,代碼有少許不同

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    Set<? extends Element> elements = env.getElementsAnnotatedWith(ContentView.class);
    for (Element element : elements) {
      //ContentView定義時指定作用范圍是類,所以只能作用于類上,Element一定是類元素
      if(element.getKind() != ElementKind.CLASS){
        error(element, "ContentView注解必須作用在類上!");
        throw new RuntimeException();
      }
      
      TypeElement typeElement = (TypeElement) element;
      //獲取包元素,主要為了方便獲取Element的包名
      //element是類元素,因此還可以使用:
      //PackageElement packageOf = (PackageElement) element.getEnclosingElement();
      PackageElement packageOf = elementUtils.getPackageOf(element);
      int id = element.getAnnotation(ContentView.class).value();

      try {
        //仿照ButterKnife,使用自己的后綴
        String classFullName = typeElement.getQualifiedName() + SUFFIX;
        //JavaFileObject createSourceFile(CharSequence name, Element... originatingElements)
        //name:完整類名
        //originatingElements:和創建的文件相關的類元素或包元素,可省略或為null
        JavaFileObject sourceFile = filer.createSourceFile(classFullName, element);
        Writer writer = sourceFile.openWriter();
        //關于ContentView注解的java 文件模板
        String tmp =
          "http:// Generated code from Zero library. Do not modify!\n" +
            "package %s;\n\n" +
            "public class %s implements zero.IContent {\n\n" +
            "  @Override\n" +
            "  public void setContentView(android.app.Activity activity) {\n" +
            "    activity.setContentView(%d);\n" +
            "  }\n" +
            "}";
        //填充包名,類名,布局文件id
        writer.write(String.format(tmp, packageOf.getQualifiedName(), typeElement.getSimpleName()+SUFFIX, id));
        writer.close();
      } catch (IOException e) {
        error(element, "不能寫入java文件!");
      }
    }
    return true;//ContentView被我處理了
  }

Zero.bind

基于注解處理器生成的java代碼已完成,最后一道工序需要將代碼調用起來即可。

public class Zero {
  public static void bind(Activity activity){
    try {
      String fullName = activity.getClass().getCanonicalName()+ ContentViewProcessor.SUFFIX;
      Class<?> zeroBind = Class.forName(fullName);
      IContent content = (IContent) zeroBind.getConstructor().newInstance();
      content.setContentView(activity);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

現在可以向ButterKnife一樣使用Zero.bind 。這里根據我們定義的規則使用了少量的運行時反射手段用于動態調用適當的代碼,另外發布時需要將相應的類不做混淆處理即可。

本文著重介紹注解處理器相關api及其應用,至于代碼的封裝可以參考筆者添加 @BindView@OnClick 后的代碼(zero-bind-library)或者ButterKnife

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380