ButterKnife原理分析(二)注解的处理

上一篇我们讲解了ButterKnife的设计思想,理解了ButterKnife绑定相关源码的实现逻辑。但是它是怎么通过注解的方式生成的那些逻辑代码,这才是最让我们迫切想知道,因此在这篇,我将说说ButterKnife中注解处理的原理。本篇主要有以下内容:

  1. 注解Annotation
  2. 注解处理器AbstractProcessor
  3. AutoService注册注解处理器
  4. JavaPoet生成Java代码
  5. Element元素相关
  6. 编译时注解解析
  7. 例子项目

注解(Annotation)

注解(Annotation)在Java中已经是很普遍的使用了,它其实就是一种标记信息,然后程序在编译或者运行的时候可以读取这个标记信息,去执行特定的逻辑,比如@BindView(R.id.tv_text) TextView tvText,程序在编译时会读取到这个@BindView注解,解析出它的值R.id.tv_text,再根据它注解的这个tvText,就可以生成类似tvText = (TextView)findViewById(R.id.tv_text);的功能代码。
注解按生命周期可以分为

  • RetentionPolicy.SOURCE(源码注解),只在源码中存在,在编译时会被丢弃,通常用于检查性的操作,如@Override。
  • RetentionPolicy.CLASS(编译时注解),在编译后的class文件中依然存在,通常用于编译时处理,如ButterKnife的@BindView。
  • RetentionPolicy.RUNTIME(运行时注解),不仅在编译后的class文件中存在,在被jvm虚拟机加载之后,仍然存在,通常用于运行时处理,如Retrofit的@Get。

同时注解按使用的对象可以分为

  • ElementType.TYPE(类型注解),标记在接口、类、枚举上。
  • ElementType.FIELD(属性注解),标记在属性字段上。
  • ElementType.METHOD(方法注解),标记在方法上。
  • ElementType.PARAMETER(方法参数注解),标记在方法参数上。
  • ElementType.CONSTRUCTOR(构造方法注解),标记在构造方法上。
  • ElementType.LOCAL_VARIABLE(本地变量注解),标记在本地变量上。
  • ElementType.ANNOTATION_TYPE(注解的注解),标记在注解上。
  • ElementType.PACKAGE(包注解),标记在包上。
  • ElementType.TYPE_PARAMETER(类型参数注解,Java1.8加入),标记类型参数上。
  • ElementType.TYPE_USE(类型使用注解,Java1.8加入),标记在类的使用上。

我们首先来认识ButterKnife的一个自定义属性注解@BindView

/**
 * 作用于View的注解,如@BindView(R.id.text) TextView tvText
 *
 * @Retention(RetentionPolicy.CLASS) 表示生命周期到类的编译时期
 * @Target(ElementType.FIELD) 表示注解作用在字段上
 *
 * Created by Administrator on 2017/12/31 0031.
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {

    @android.support.annotation.IdRes
    int value();
}

它可以保留在类编译之后,使用场景是作用在属性上

关于注解的详细介绍查看Java注解基础概念总结

注解处理器AbstractProcessor

注解只是一种标记信息,所以需要我们自己去处理注解,注解的处理有编译时注解处理和运行时注解处理。运行时注解,我们可以通过反射获取注解信息,进而进行相应处理。而编译时注解就需要使用注解处理器(Annotation Processor)进行处理。那什么是注解处理器?

注解处理器是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这些生成的Java代码是在新生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。
要实现一个注解处理器需要继承AbstractProcessor,并重写它的4个方法,同时必须要有一个无参的构造方法,以便注解工具能够对它进行初始化。

public class ViewBindProcessor extends AbstractProcessor {  
    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;

    @Override  
    public synchronized void init(ProcessingEnvironment processingEnv) {  
        super.init(processingEnv);  
        
        //提供给注解处理器使用的工具类
        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }  
  
    @Override  
    public Set<String> getSupportedAnnotationTypes() {  
        //添加需要处理的注解
        Set<String> annotataions = new LinkedHashSet<String>();  
        annotataions.add(MyAnnotation.class.getCanonicalName());  
        return annotataions;  
    }  
  
    @Override  
    public SourceVersion getSupportedSourceVersion() {
        //指定使用的Java版本
        return SourceVersion.latestSupported();  
    } 
    
     @Override  
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {  
        //这里实现注解的处理,重点方法
        return false;  
    }  
}  
  • init,会被注解处理工具调用,参数ProcessingEnvironment提供了Elements,Types,Filer,Messager 等。
  • getSupportedAnnotationTypes(),指定注解处理器要处理哪些注解,返回一个字符串集合,包含要处理注解的全名。
  • getSupportedSourceVersion, 指定使用的Java版本,通常这里返回SourceVersion.latestSupported()
  • process,相当于每个处理器的main函数,在这里可以做扫描、评估和处理注解代码的操作,以及生成Java文件。

那么init方法中ProcessingEnvironment提供的Elements,Types,Filer,Messager 等是做什么用的呢?

  • Elements:用来处理程序元素的工具类
  • Types:用来处理类型数据的工具类
  • Filer:用来给注解处理器创建文件
  • Messager:用来给注解处理器报告错误,警告,提示等消息。

AutoService注册注解处理器

以前要注册注解处理器是要在module的META-INF目录下新建services目录,并创建一个名为javax.annotation.processing.Processor的文件,然后在文件中写入要注册的注解处理器的全名,例如在javax.annotation.processing.Processor的文件中加上

com.pinery.compile.ViewBindProcessor

来注册ViewBindProcessor注解处理器。

后来Google推出了通过添加AutoService注解库来实现注解处理器的注册,通过在你的注解处理器上加上@AutoService(Processor.class)注解,即可在编译时生成 META-INF/services/javax.annotation.processing.Processor 文件。
配置AutoService需要在工程的build.gradle中添加

JavaPoet生成Java代码

JavaPoet是Square公司出品的生成Java源文件库,正如其名,会写Java代码的诗人,使用它的一系列API就可以很方便的生成java源代码了。

JavaPoet中有几个常用的类:

  • MethodSpec,代表一个构造函数或方法声明。
  • TypeSpec,代表一个类,接口,或者枚举声明。
  • FieldSpec,代表一个成员变量,一个字段声明。
  • JavaFile,包含一个顶级类的Java文件。

这是一个计算从1到100相加的方法

public static int caculateNum() {
    int result = 0;
    for(int i = 1; i < 100; i++) {
      result = result + i;
    }
    return result;
}

我们用MethodSpec实现这个方法声明

MethodSpec caculateMethod = MethodSpec.methodBuilder("caculateNum")
      .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
      .returns(int.class)
      .addStatement("int result = 0")
      .beginControlFlow("for(int i = $L; i < $L; i++)", 1, 100)
      .addStatement("result = result $L i", "+")
      .endControlFlow()
      .addStatement("return result")
      .build();

可以发现,通过addModifiers添加方法修饰符,returns来定义方法的返回值类型,addStatement来添加方法中的一行语句,它会处理分号和换行,beginControlFlow和endControlFlow构成一个封闭的控制语段,适用于if,for,while等。$L相当于一个占位符,代表的是一个字面量,其他还有:

  • $S for Strings,代表一个字符串
  • $T for Types,代表一个类型,使用它会自动import导入包
  • $N for Names,代表我们自己生成的方法名或者变量名等等

    例如
addStatement("$T.out.println($S)", System.class, "Hello World"))

这是定义的一个属性

private final String name = "Pinery";

我们用MethodSpec实现这个方法声明

FieldSpec nameField = FieldSpec.builder(String.class, "name")
    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
    .initializer("$S", "Pinery")
    .build();

下面是一个类的定义

public class MyClass{

    private final String name = "Pinery";

    public static int caculateNum() {
        int result = 0;
        for(int i = 1; i < 100; i++) {
          result = result + i;
        }
        return result;
    }
}

我们用TypeSpec实现这个方法声明

TypeSpec helloWorld = TypeSpec.classBuilder("MyClass")
    .addModifiers(Modifier.PUBLIC)
    .addField(nameField)
    .addMethod(caculateMethod)
    .build();

通过TypeSpec添加了属性实现和方法实现,其他常用的还有

  • addTypeVariable,添加泛型声明
  • addSuperinterface,添加接口实现
  • addJavadoc,添加注释
  • interfaceBuilder,生成一个接口

等,详细使用可以参考JavaPoet官方文档

Element元素相关

注解处理工具扫描java源文件,源代码中的每一部分都是程序中的Element元素,如包,类,方法,字段等。每一个元素代表一个静态的,语言级别的结构。源代码其实是一种结构化的文本(例如json文本就是一种结构化的文本),因此需要对它进行解析,解析的话,解析器会解析某些信息代表某些结构,例如源代码中的类声明信息代表TypeElement类型元素,方法声明信息代表代表ExecutableElement类型元素。有了这些结构,就能完整的表示整个源代码信息了。
Element元素分为以下类型:

  • ExecutableElement: 可执行元素,包括类或接口的方法、构造方法或初始化程序
  • PackageElement: 包元素,提供对有关包及其成员的信息的访问
  • TypeElement: 类或接口元素,提供对有关类型及其成员的信息的访问
  • TypeParameterElement: 表示一般类、接口、方法或构造方法元素的形式类型参数,类型参数声明一个 TypeVariable
  • VariableElement: 表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数。

Element元素还有个asType()可以获取元素类型TypeMirror,TypeMirror有以下具体类型:

  • ArrayType: 表示一个数组类型。多维数组类型被表示为组件类型也是数组类型的数组类型。
  • DeclaredType: 表示某一声明类型,是一个类 (class) 类型或接口 (interface) 类型。这包括参数化的类型(比如 java.util.Set<String>)和原始类型。
  • ErrorType: 表示无法正常建模的类或接口类型。这可能是处理错误的结果。大多数对于派生于这种类型(比如其成员或其超类型)的信息的查询通常不会返回有意义的结果。
  • ExecutableType: 表示 executable 的类型。executable 是一个方法、构造方法或初始化程序。
  • NoType: 在实际类型不适合的地方使用的伪类型。NoType 的种类有:
    • VOID:对应于关键字 void。
    • PACKAGE:包元素的伪类型。
    • NONE:用于实际类型不适合的其他情况中;例如,java.lang.Object 的超类。
  • NullType: 表示 null 类型。此类表达式 null 的类型。
  • PrimitiveType: 表示一个基本类型。这些类型包括 boolean、byte、short、int、long、char、float 和 double。
  • ReferenceType: 表示一个引用类型。这些类型包括类和接口类型、数组类型、类型变量和 null 类型。
  • TypeVariable: 表示一个类型变量。类型变量可由某一类型、方法或构造方法的类型参数显式声明。
  • WildcardType: 表示通配符类型参数。

编译时注解解析

有了上面知识点的了解之后,下面就可以进行编译时的注解解析了,需要以下几个步骤:

  1. 定义注解
  2. 定义一个继承自AbstractProcessor的注解处理器,重写它4个方法中
  3. 使用AutoService注册自定义的注解处理器
  4. 实现process方法,在这里处理注解
  5. 处理所有注解,得到TypeElement和注解信息等信息
  6. 使用JavaPoet生成新的Java类

在annotations的moudle中定义一个注解

/**
 * 作用于View的注解,如@BindView(R.id.text) TextView tvText
 *
 * @Retention(RetentionPolicy.CLASS) 表示生命周期到类的编译时期
 * @Target(ElementType.FIELD) 表示注解作用在字段上
 *
 * Created by Administrator on 2017/12/31 0031.
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {

    @android.support.annotation.IdRes
    int value();
}

在compile的moudle中定义一个ViewBindProcessor注解处理器

/**
 * 自定义的AbstractProcessor,用于编译时处理注解
 */
@AutoService(Processor.class) //添加AutoService注解,自动注册ViewBindProcessor注解处理器
public class ViewBindProcessor extends AbstractProcessor{

    private Map<TypeElement, List<ViewBindInfo>> bindMap = new HashMap<>();

    //用来处理类型数据的工具类
    private Types typeUtils;
    //用来处理程序元素的工具类
    private Elements elementUtils;
    //用来给注解处理器创建文件
    private Filer filer;
    //用来给注解处理器报告错误,警告,提示等消息。
    private Messager messager;

    /**
     * 会被注解处理工具调用,参数ProcessingEnvironment提供了Elements,Types,Filer,Messager 等。
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);

        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

    /**
     * 指定注解处理器是注册给那一个注解的,它是一个字符串的集合,意味着可以支持多个类型的注解,并且字符串是合法全名。
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();

        //添加自定义的BindView注解
        annotataions.add(BindView.class.getCanonicalName());

        return annotataions;
    }

    /**
     * 指定使用的Java版本,通常这里返回SourceVersion.latestSupported()
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 相当于每个处理器的main函数,在这里可以做扫描、评估和处理注解代码的操作,以及生成Java文件。
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        collectBindViewAnnotations(roundEnvironment);

        generateJavaFilesWithJavaPoet();

        return false;
    }

    /**
     * 收集BindView注解
     * @param roundEnvironment
     * @return
     */
    private boolean collectBindViewAnnotations(RoundEnvironment roundEnvironment){
        //查找所有添加了注解BindView的元素
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        if(elements == null || elements.isEmpty()){
            return false;
        }

        for(Element element : elements){
            //注解BindView必须添加在属性上
            if(element.getKind() != ElementKind.FIELD){
                error(element, "只有类的属性可以添加@%s注解", BindView.class.getCanonicalName());
                return false;
            }

            //获取注解的值
            int viewId = element.getAnnotation(BindView.class).value();
            //这个元素是属性类型的元素
            VariableElement viewElement = (VariableElement) element;
            //获取直接包含属性元素的元素,即类元素
            TypeElement typeElement = (TypeElement) viewElement.getEnclosingElement();

            //将类型元素作为key,保存到bindMap暂存
            List<ViewBindInfo> viewBindInfoList = bindMap.get(typeElement);
            if(viewBindInfoList == null){
                viewBindInfoList = new ArrayList<>();
                bindMap.put(typeElement, viewBindInfoList);
            }

            info("注解信息:viewId=%d, name=%s, type=%s", viewId, viewElement.getSimpleName().toString(), viewElement.asType().toString());

            viewBindInfoList.add(new ViewBindInfo(viewId, viewElement.getSimpleName().toString(), viewElement.asType()));
        }

        return true;
    }

    /**
     * 生成注解处理之后的Java文件
     */
    private void generateJavaFilesWithJavaPoet(){
        if(bindMap.isEmpty()){
            return;
        }

        //针对每个类型元素,生成一个新文件,例如,针对MainActivity,生成MainActivity_ViewBind文件
        for(Map.Entry<TypeElement, List<ViewBindInfo>> entry : bindMap.entrySet()){
            TypeElement typeElement = entry.getKey();
            List<ViewBindInfo> list = entry.getValue();

            //获取当前类型元素所在的包名
            String pkgName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();

            //获取类的全名称
            ClassName t = ClassName.bestGuess("T");
            ClassName viewBinder = ClassName.bestGuess("com.pinery.bind_lib.ViewBinder");

            //定义方法结构
            MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bind")
                    .addAnnotation(Override.class)//Override注解
                    .addModifiers(Modifier.PUBLIC)//public修饰符
                    .returns(void.class)//返回类型void
                    .addParameter(t, "activity")//参数类型
                    ;
            //为方法添加实现
            for(ViewBindInfo info : list){
                methodSpecBuilder.addStatement("activity.$L = activity.findViewById($L)", info.viewName, info.viewId);
            }

            //定义类结构
            TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + "_ViewBinder")
                    .addModifiers(Modifier.PUBLIC)//public修饰符
                    .addTypeVariable(TypeVariableName.get("T", TypeName.get(typeElement.asType())))//泛型声明
                    .addSuperinterface(ParameterizedTypeName.get(viewBinder, t))
                    .addMethod(methodSpecBuilder.build())//方法
                    .build();

            //定义一个Java文件结构
            JavaFile javaFile = JavaFile.builder(pkgName, typeSpec).build();
            try {
                //写入到filer中
                javaFile.writeTo(filer);
            }catch (Exception ex){
                ex.printStackTrace();
            }

        }

    }

    /**
     * 错误提示
     * @param element
     * @param msg
     * @param args
     */
    private void error(Element element, String msg, Object... args){
        //输出错误提示
        messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
    }

    /**
     * 信息提示
     * @param msg
     * @param args
     */
    private void info(String msg, Object... args) {
        messager.printMessage(
                Diagnostic.Kind.NOTE,
                String.format(msg, args));
    }

}

这里会使用一个ViewBindInfo用于存储view的id, 名称,和类型的对应关系,如下:

public class ViewBindInfo {

    public int viewId;
    public String viewName;
    public TypeMirror typeMirror;

    public ViewBindInfo(int viewId, String viewName, TypeMirror typeMirror){
        this.viewId = viewId;
        this.viewName = viewName;
        this.typeMirror = typeMirror;
    }

}

这样就完成了编译时注解的处理。接下来在MainActivity中使用注解


public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_text)
    TextView tvText;
    @BindView(R.id.btn_text)
    Button btnText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        BindHelper.bind(this);

        tvText.setText("Hello, BindView");
        btnText.setText("Hello, BindView");
    }
}

编译后会生成一个MainActivity_ViewBinder.java文件。我们在看看BindHelper的实现


public class BindHelper {

    /**
     * 绑定方法
     * @param activity
     */
    public static void bind(Activity activity) {
        try {
            Class<?> viewBinderClazz = Class.forName(activity.getClass().getCanonicalName() + "_ViewBinder");
            ViewBinder viewBinder = (ViewBinder) viewBinderClazz.newInstance();
            viewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

    }

}

这里就会通过反射生成MainActivity_ViewBinder的对象,调用bind方法作view的绑定处理。

例子项目

CompileAnnotation

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容