手写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("// 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 =
          "// 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阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342