Android 编译时注解-提升

3,904 阅读10分钟

背景

在前面的文章中,讲解了注解和编译时注解等一些列相关的内容,为了更加全面和真是的了解Android 编译时注解在实战项目中的使用,本文采取实现主流框架butterknife注入view去全面认识编译时注解。

注解专栏-博客

效果

先来张效果图压压惊

先来张图压压惊,实现效果butterknifeview绑定

使用

仿照butterknife实现了@BindView注解,通过WzgJector.bind方法绑定当前MainActivity,整体和butterknife使用完全一模一样,这里为了区分简单的把butterknife改名了WzgJector

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_msg)
    TextView tvMsg;
    @BindView(R.id.tv_other)
    TextView tvOther;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WzgJector.bind(this);
        if(tvMsg!=null){
            tvMsg.setText("我已经成功初始化了");
        }

        if(tvOther!=null){
            tvOther.setText("我就来看看而已");
        }
    }
}

实现

实现的思路和Android编译时注解-初认识实现原理大致一样,所以这里不重复阐述重复的步骤,重点讲解提升的技术点,所以需要在了解基本编译时注解的前提下继续下面的学习

Android编译时注解-初认识

定义注解

这里使用了javaandroid自带的注解,初始一个BindView注解,同时指定了@TargetFIELD,注解BindView带有一个初始的int参数及时使用时的view-id

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

javaandroid自带的注解不太清楚的同学可参考下面两篇文章

Java-注解详解

Android-注解详解

Element详解

Element

有了注解,必然需要有一个对应的注解处理器去处理注解,但是在处理注解的时候需要充分的了解注解处理器中的process方法及时核心的编译代码,而process方法的核心便是Element对象,所以在讲解注解处理器前,需要对Element有全面的认识,方能事半功倍。

由于Element的知识内容的复杂性,这里重点讲解核心内容,基本使用完全是足够了

源码:

public interface Element extends AnnotatedConstruct {
    TypeMirror asType();

    ElementKind getKind();

    Set<Modifier> getModifiers();

    Name getSimpleName();

    Element getEnclosingElement();

    List<? extends Element> getEnclosedElements();

    boolean equals(Object var1);

    int hashCode();

    List<? extends AnnotationMirror> getAnnotationMirrors();

    <A extends Annotation> A getAnnotation(Class<A> var1);

    <R, P> R accept(ElementVisitor<R, P> var1, P var2);
}

可看出其实Element是定义的一个接口,定义了外部调用暴露出的接口

方法 解释
asType 返回此元素定义的类型
getKind 返回此元素的种类:包、类、接口、方法、字段...,如下枚举值
getModifiers 返回此元素的修饰符,如下枚举值
getSimpleName 返回此元素的简单名称,比如activity名
getEnclosingElement 返回封装此元素的最里层元素,如果此元素的声明在词法上直接封装在另一个元素的声明中,则返回那个封装元素; 如果此元素是顶层类型,则返回它的包如果此元素是一个包,则返回 null; 如果此元素是一个泛型参数,则返回 null.
getAnnotation 返回此元素针对指定类型的注解(如果存在这样的注解),否则返回 null。注解可以是继承的,也可以是直接存在于此元素上的

getKind方法

其中getKind方法比较特殊,getKind()方法来获取具体的类型,方法返回一个枚举值TypeKind

源码:

public enum TypeKind {  
    /** The primitive type {@code boolean}. */  
    BOOLEAN,  
    /** The primitive type {@code byte}. */  
    BYTE,  
    /** The primitive type {@code short}. */  
    SHORT,  
    /** The primitive type {@code int}. */  
    INT,  
    /** The primitive type {@code long}. */  
    LONG,  
    /** The primitive type {@code char}. */  
    CHAR,  
    /** The primitive type {@code float}. */  
    FLOAT,  
    /** The primitive type {@code double}. */  
    DOUBLE,  
    /** The pseudo-type corresponding to the keyword {@code void}. */  
    VOID,  
    /** A pseudo-type used where no actual type is appropriate. */  
    NONE,  
    /** The null type. */  
    NULL,  
    /** An array type. */  
    ARRAY,  
    /** A class or interface type. */  
    DECLARED,  
    /** A class or interface type that could not be resolved. */  
    ERROR,  
    /** A type variable. */  
    TYPEVAR,  
    /** A wildcard type argument. */  
    WILDCARD,  
    /** A pseudo-type corresponding to a package element. */  
    PACKAGE,  
    /** A method, constructor, or initializer. */  
    EXECUTABLE,  
    /** An implementation-reserved type. This is not the type you are looking for. */  
    OTHER,  
    /** A union type. */  
    UNION,  
    /** An intersection type. */  
    INTERSECTION;  
}

Element子类

Element 有五个直接子接口,它们分别代表一种特定类型的元素

Tables Are
TypeElement 一个类或接口程序元素
VariableElement 一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数
ExecutableElement 某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素
PackageElement 一个包程序元素
TypeParameterElement 一般类、接口、方法或构造方法元素的泛型参数

五个子类各有各的用处并且有各种独立的方法,在使用的时候可以强制将Element对象转换成其中的任一一种,但是前提是满足条件的转换,不然会抛出异常。

其中最核心的两个子分别是TypeElementVariableElement

TypeElement详解

TypeElement定义的一个类或接口程序元素,相当于当前注解所在的class对象,及时本案例使用代码中的MainActivity

源码如下:

public interface TypeElement extends Element, Parameterizable, QualifiedNameable {
    List<? extends Element> getEnclosedElements();

    NestingKind getNestingKind();

    Name getQualifiedName();

    Name getSimpleName();

    TypeMirror getSuperclass();

    List<? extends TypeMirror> getInterfaces();

    List<? extends TypeParameterElement> getTypeParameters();

    Element getEnclosingElement();
}

这里讲解主要的方法的含义

方法 解释
getNestingKind 返回此类型元素的嵌套种类
getQualifiedName 返回此类型元素的完全限定名称。更准确地说,返回规范 名称。对于没有规范名称的局部类和匿名类,返回一个空名称.
getSuperclass 返回此类型元素的直接超类。如果此类型元素表示一个接口或者类 java.lang.Object,则返回一个种类为 NONE 的 NoType
getInterfaces 返回直接由此类实现或直接由此接口扩展的接口类型
getTypeParameters 按照声明顺序返回此类型元素的形式类型参数

VariableElement详解

源码:

public interface VariableElement extends Element {
    Object getConstantValue();

    Name getSimpleName();

    Element getEnclosingElement();
}

这里VariableElement除了拥有Element的方法以外还有以下两个方法

方法 解释
getConstantValue 变量初始化的值
getEnclosingElement 获取相关类信息

注解处理器

注解处理器需要两个步骤的处理:

  • 1.收集先关的信息

  • 2.生成处理类

Element有了全面的了解过后,注解处理器便可很轻松的学习了,先来看看简单版本的BindView处理

Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        //一、收集信息
        for (Element element : elements) {
            /*检查类型*/
            if (!(element instanceof VariableElement)) {
                return false;
            }
            VariableElement variableElement = (VariableElement) element;

            /*获取类信息*/
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            /*类的绝对路径*/
            String qualifiedName = typeElement.getQualifiedName().toString();
            /*类名*/
            String clsName = typeElement.getSimpleName().toString();
            /*获取包名*/
            String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();

            BindView annotation = variableElement.getAnnotation(BindView.class);
            int id = annotation.value();

            /*参数名*/
            String name = variableElement.getSimpleName().toString();
            /*参数对象类*/
            String type = variableElement.asType().toString();

            ClassName InterfaceName = ClassName.bestGuess("com.example.annotation.api.ViewInjector");
            ClassName host = ClassName.bestGuess(qualifiedName);

            MethodSpec main = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addAnnotation(Override.class)
                    .addParameter(host, "host")
                    .addParameter(Object.class, "object")
                    .addCode(""
                            + " if(object instanceof android.app.Activity){\n"
                            + " host." + name + " = (" + type + ")(((android.app.Activity)object).findViewById(" + id + "));\n"
                            + " }\n"
                            + "else{\n"
                            + " host." + name + " = (" + type + ")(((android.view.View)object).findViewById(" + id + "));\n"
                            + "}\n")
                    .build();

            TypeSpec helloWorld = TypeSpec.classBuilder(clsName + "ViewInjector")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(main)
                    .addSuperinterface(ParameterizedTypeName.get(InterfaceName, host))
                    .build();

            try {
                // 生成 com.example.HelloWorld.java
                JavaFile javaFile = JavaFile.builder(packageName, helloWorld)
                        .addFileComment(" This codes are generated automatically. Do not modify!")
                        .build();
                // 生成文件
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }

大体的思路,先判断Element类型,如果是VariableElement则继续获取相关的包名(这里必须在app包名一致,不然获取不到android类)类对象信息,以及@BindView注解修饰的参数数据;最后将所有需要的数据通过javapoetFiler自动编译创建一个java文件

最后得到的生成类:

package com.wzgiceman.viewinjector;

import com.example.ViewInjector;
import java.lang.Object;
import java.lang.Override;

public class MainActivityViewInjector implements ViewInjector<MainActivity> {
  @Override
  public void inject(MainActivity host, Object object) {
     if(object instanceof android.app.Activity){
     host.tvMsg = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));
     }
    else{
     host.tvMsg = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945));
    }
  }
}

上面的简单处理器中,只是单纯的判断一个注解情况,在信息收集的处理上简化了,导致当前处理器只能同时处理当前相同类中的莫一个注解这里只初始化了tvMsg对象,tvOther并没有初始化,当然这是不符合实际需求的,下面来优化收集和处理方案。

优化

优化方案其实就是多了一步信息的记录的工作

创建信息类对象

首先创建一个类信息对象,其中包含了一下的属性,其中varMap便是记录当前类中所有注解相关的信息

public class VariMsg {
    /*包名*/
    private String pk;
    /*类名*/
    private String clsName;
    /*注解对象*/
    private HashMap<Integer,VariableElement> varMap;
    }

BindViewProcessors

1.初始一个map记录VariMsg对象,因为process方法可能会多次调用,所以需要每次都clear一遍

 Map<String, VariMsg> veMap = new HashMap<>();

2.记录信息
通过veMap记录所有的相关信息,并且每次需要判断是否重复,剔除重复的数据。

    for (Element element : elements) {
            /*检查类型*/
            if (!(element instanceof VariableElement)) {
                return false;
            }
            VariableElement variableElement = (VariableElement) element;

            /*获取类信息*/
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            /*类的绝对路径*/
            String qualifiedName = typeElement.getQualifiedName().toString();
            /*类名*/
            String clsName = typeElement.getSimpleName().toString();
            /*获取包名*/
            String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();

            BindView annotation = variableElement.getAnnotation(BindView.class);
            int id = annotation.value();

            VariMsg variMsg = veMap.get(qualifiedName);
            if (variMsg == null) {
                variMsg = new VariMsg(packageName, clsName);
                variMsg.getVarMap().put(id, variableElement);
                veMap.put(qualifiedName, variMsg);
            } else {
                variMsg.getVarMap().put(id, variableElement);
    }
   }

3.通过javapoet去生成java类文件
这里主要是javapoet的运用,详细用法可去javapoet查看

javapoet-GitHub


  System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        for (String key : veMap.keySet()) {
            ClassName InterfaceName = ClassName.bestGuess("com.example.ViewInjector");
            ClassName host = ClassName.bestGuess(key);
            VariMsg variMsg = veMap.get(key);

            StringBuilder builder = new StringBuilder();
            builder.append(" if(object instanceof android.app.Activity){\n");
            builder.append(code(variMsg.getVarMap(), "android.app.Activity"));
            builder.append("}\n");
            builder.append("else{\n");
            builder.append(code(variMsg.getVarMap(), "android.view.View"));
            builder.append("}\n");

            MethodSpec main = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addAnnotation(Override.class)
                    .addParameter(host, "host")
                    .addParameter(Object.class, "object")
                    .addCode(builder.toString())
                    .build();

            TypeSpec helloWorld = TypeSpec.classBuilder(variMsg.getClsName() + "ViewInjector")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(main)
                    .addSuperinterface(ParameterizedTypeName.get(InterfaceName, host))
                    .build();

            try {
                JavaFile javaFile = JavaFile.builder(variMsg.getPk(), helloWorld)
                        .addFileComment(" This codes are generated automatically. Do not modify!")
                        .build();
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("e--->" + e.getMessage());
            }
        }
        System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        return true;
    }


    /**
     * 根据注解对象生成code方法体
     *
     * @param map
     * @param pk
     * @return
     */
    private String code(Map<Integer, VariableElement> map, String pk) {
        StringBuilder builder = new StringBuilder();
        for (Integer id : map.keySet()) {
            VariableElement variableElement = map.get(id);
            String name = variableElement.getSimpleName().toString();
            String type = variableElement.asType().toString();

            builder.append("host." + name + " = (" + type + ")(((" + pk + ")object).findViewById(" + id + "));\n");
        }
        return builder.toString();
    }

到这里注解处理器最终版本就生成成功了,看下最后生成的代码类

package com.wzgiceman.viewinjector;

import com.example.ViewInjector;
import java.lang.Object;
import java.lang.Override;

public class MainActivityViewInjector implements ViewInjector<MainActivity> {
  @Override
  public void inject(MainActivity host, Object object) {
     if(object instanceof android.app.Activity){
    host.tvMsg = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));
    host.tvOther = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492946));
    }
    else{
    host.tvMsg = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945));
    host.tvOther = (android.widget.TextView)(((android.view.View)object).findViewById(2131492946));
    }
  }
}

api

api模块主要定义的是给外部提供的使用方法,这里使用方法便是WzgJector.bind(this)方法,相同于Butter Knife中的ButterKnife.bind(this);

public class MainActivity extends AppCompatActivity {

   xxxxxx
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WzgJector.bind(this);
        xxxx
    }
}

实现

创建一个app module

定义接口类ViewInjector

暴露的外部方法,主要在编译自动生成的注解处理类中使用

/**
 * 接口
 * Created by WZG on 2017/1/11.
 */
public interface ViewInjector<M> {
    void inject(M m, Object object);
}

实际处理类WzgJector

提供了两个方法,一种是activity绑定,一种是view或者fragment绑定,绑定完以后,通过反射得到相关注解编译处理类及时ViewInjector子类对象,调用inject(M m, Object object)方法完成初始过程。

public class WzgJector {
    public static void bind(Object activity) {
        bind(activity, activity);
    }

    public static void bind(Object host, Object root) {
        Class<?> clazz = host.getClass();
        String proxyClassFullName = clazz.getName() + "ViewInjector";
        try {
            Class<?> proxyClazz = Class.forName(proxyClassFullName);
            ViewInjector viewInjector = (ViewInjector) proxyClazz.newInstance();
            viewInjector.inject(host, root);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

到这里其实会发现,编译时注解并不是完全不使用反射,但是它避免了关键性重复性代码的多次反射使用,继而提升了编译的性能。

结果

到这里butterknife@BindView实现原理基本就是如此,由于时间原因@OnClick就不再讲解了。其实原理一样,小伙伴们可以自己安装本文思路添加@OnClick的处理。

专栏

注解-编译运行时注解

源码

下载源码

建议

如果你有任何的问题和建议欢迎加入QQ群告诉我!