Android APT编译时注解

概念

APT(Annotation Processing Tool)编译时注解,是javac的一个工具,在java源文件被编译为.class的过程中,注解处理器会去扫描java源文件处理注解,在这个过程中我们可以自定义注解器,根据自定义注解,生成java代码、文件,减少了很多重复代码的编写。很多流行的框架都是用的这个思想,比如Butterknife、Dragger、Room、组件化框架都是用的编译时注解原理,自动生成了代码,简化了使用。
其实,java源文件到加载运行过程中,很多环节都是可以操作利用的,这也是很多框架的思想。


1.png

在java加载运行过程中的各个环节都是有自己的玩法,

  • 在编译的过程中,可以自定义注解处理器,通过处理注解来自动生成java代码
  • 可以利用字节码插桩技术对class文件进行修改,实现数据收集SDK时,为了实现非侵入的,全量的数据采集,采用了AOP这也叫切面编程,在Android里面常用的框架是aspactj,典型的应用场景就是埋点、计算方法的停留时间。
  • 动态代理主要是在class文件加载到虚拟机的时候自定义ClassLoader来完成,运用场景为热修复框架、换肤框架、插件化框架,还有retrofit核心逻辑也是动态代理。
  • 运行时那就是大家都熟悉的反射。

实现

一般的apt框架都是习惯建立三个module,两个java module,一个Android lib module,这里以模仿butterknife来实现。


2.png
  • apt-annotation 存放注解,例如@BindView
  • apt-processor 存放自定义注解处理器,java代码将在这里组织生成
  • apt-api 暴露给用户的api,我们生成的java代码怎么使用,需要提供api
    当然这只是一个参考的目录结构。
创建annotation module
3.png

new ->module->java module 并创建注解类

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD})
public @interface BindeView {
    int value() default -1;
}

@Retention 注解的存在时期,存在三个时期

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

SOURCE、CLASS、RUNTIME 由于是编译时注解,选择SOURCE就行了。

@Target表示被注解的类型,主要有:

  • TYPE class上的注解
  • FIELD 用来注解变量
  • METHOD 作用与方法
  • PARAMETER 注解方法的参数
    这时候我们就可以使用注解:
public class MainActivity extends AppCompatActivity {

    @BindeView(R.id.tv_hello_world)
    public TextView tv;
    @BindeView(R.id.tv_hello_world2)
    public TextView tv2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        XcInject.bind(this);
        tv.setText("注解成功");
        tv2.setText("注解成功2");
    }
}

注解作用于成员变量,值为View的id

创建注解处理器

同上,创建一个java module,建一个类继承自AbstractProcessor

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor{
    private Types    mTypeUtils;
    private Elements mElementUtils;
    private Filer    mFiler;
    private Messager mMessager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment){
        super.init(processingEnvironment);
        mTypeUtils = processingEnv.getTypeUtils();
        mElementUtils = processingEnv.getElementUtils();
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        //支持的java版本
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //支持的注解类型
        Set<String> annotations = new HashSet<>();
        annotations.add(BindView.class.getCanonicalName());
        return annotations;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

所有的注解处理器必须继承AbstractProcessor类,复写四个重要方法

  • init(ProcessingEnvironment processingEnv) ,当注解处理器被初始化的时候调用,传入ProcessingEnvironment类型,提供注解器运行时处理注解的一些工具类,一般有四个常用的方法
public interface ProcessingEnvironment {
    // 用来处理注解器的异常信息
    Messager getMessager();
    // 用来生成java源文件工具类
    Filer getFiler();
    // 用来处理被注解的Element,获取Element信息
    Elements getElementUtils();
    // 用来操作类型数据
    Types getTypeUtils();

}
  • getSupportedSourceVersion()支持java的版本,一般值为SourceVersion.latestSupported();就可以了
  • getSupportedAnnotationTypes(),返回一个当前注解处理器所有支持的注解的集合。当前注解处理器需要处理那种注解就加入那种注解。如果类型符合,就会调用process()方法

当然上面两个方法在java7之后可以用注解代替:

@AutoService(Processor.class)
@SupportedAnnotationTypes("xc.com.apt.annotation.BindeView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class XAptProcessor extends AbstractProcessor{
  ...
}
  • AutoService
    我们自定义的注解器需要被javac识别并且调用那就必须给注解处理器打一个标记,javac工具扫描到了注解处理器类会实例化它并且调用相应的方法。这里可以导入:
implementation 'com.google.auto.service:auto-service:1.0-rc4'

这是

Element

可以说注解处理器中最重要的就是Element这个元素了,所有被注解过的元素经过注解处理器扫描之后被封装为Element元素,Element是个接口,有五个实现类,分别代表了不同类型的类型的元素:

package com.example;    // PackageElement

public class Foo      // TypeElement
<T> {                       // TypeParameterElement

    private int a;      // VariableElement
    private Foo other;  // VariableElement

    public Foo () {}    // ExecuteableElement

    public void setA (  // ExecuteableElement
                     int newA   // TypeElement
                     ) {}
}
  • TypeElement 一个类的元素,如果注解处理器处理的对象是类或者接口,那么这种元素将是TypeElement
  • PackageElement 一个包名的元素
  • VariableElement 一般为变量、枚举、方法参数元素
  • ExecuteableElement 构造函数、方法的元素
  • TypeParameterElement 泛型元素
public interface Element extends javax.lang.model.AnnotatedConstruct {
    // 获取元素的类型
    TypeMirror asType();
    // 获取Element的类型
    ElementKind getKind();
    // 获取修饰符
    Set<Modifier> getModifiers();
    // 获取名字
    Name getSimpleName();
    // 获取当前元素被封装的元素,比如:当前Element是VariableElement,获取的就是他的所在的类的Elenment --> TypeElement
    Element getEnclosingElement();
    <A extends Annotation> A getAnnotation(Class<A> annotationType);
}
TypeElement
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();
}
  • getEnclosedElements: 获取被封装的上级的元素
  • getQualifiedName: 获取全限定名
  • getSimpleName 获取名字
  • getInterfaces 获取接口
  • getTypeParameters 获取泛型
VariableElement

代表变量类型,主要有三个方法

  • getConstantValue() :获取初始化变量的值
  • getEnclosingElement : 获取类相关元素
  • getSimpleName :获取变量的名字
编写注解处理器

准备工作做好了那就开始编写注解处理器,主要分为两个步骤,首先收集注解的元素信息,再生成java代码
收集信息:

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindeView.class);
        for (Element ele : elements) {
            // 判断Element的类型是否是变量类型Element
            if(!(ele instanceof VariableElement)) {
                return false;
            }
            // 第二种校验方式
            if(ele.getKind() != ElementKind.FIELD) {
                return false;
            }
            // 强转
            VariableElement variableElement = (VariableElement)ele;
            // 获取封装Element,即变量所属的类的Element
            TypeElement enclosingElement = (TypeElement) ele.getEnclosingElement();
            // 获取全限定名
            String qualifiedName = enclosingElement.getQualifiedName().toString();
            // 获取类名
            String className = enclosingElement.getSimpleName().toString();
            // 获取注解类的包名,将生成后的代码放置一个包名下
            String pkgName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();

            ClassName hostName = ClassName.bestGuess(qualifiedName);
            // 获取变量的名字
            String variableName = variableElement.getSimpleName().toString();
            // 获取变量的类型
            String variableType = variableElement.asType().toString();
            // 获取注解的值,也就是变量的id
            BindeView annotation = ele.getAnnotation(BindeView.class);
            int id = annotation.value();
            
            // 生成代码
            // 手动拼接示例代码
            // analysisAnnotated(Element element)

            // javapoet框架生成代码
            MethodSpec method = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addAnnotation(Override.class)
                    .addParameter(hostName, "host")
                    .addParameter(Object.class, "object")
                    .addCode(""
                            + "if(object instanceof android.app.Activity){\n"
                            + "\thost." + variableName + " = (" + variableType + ")(" + "((android.app.Activity)object).findViewById(" + id + "));\n"
                            + "\t}else {\n"
                            + "\thost." + variableName + " = (" + variableType + ")(" + "((android.view.View)object).findViewById(" + id + "));\n"
                            + "}"
                            + ""

                    ).build();

            ClassName interfaceName = ClassName.bestGuess("xc.com.apt.api.ViewInjector");
            TypeSpec classType = TypeSpec.classBuilder(className + "_ViewInjector")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(method)
                    .addSuperinterface(ParameterizedTypeName.get(interfaceName, hostName))
                    .build();

            JavaFile build = JavaFile.builder(pkgName, classType)
                    .addFileComment("this file don't modify")
                    .build();
            try {
                build.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }


        }
        return true;
    }
private void analysisAnnotated(Element element) {
        StringBuilder builder = new StringBuilder()
                .append("package com.xc.apt;\n\n")
                .append("public class APTTest {\n\n")
                .append("\tpublic String getMessage() {\n")
                .append("\t\treturn \"");

        String objectType = element.getSimpleName().toString();
        builder.append(objectType).append(" hello world!\\n");
        builder.append("\";\n") 
                .append("\t}\n")
                .append("}\n"); 

        try {
            JavaFileObject source = processingEnv.getFiler().createSourceFile("com.xc.apt.APTTest");
            Writer writer = source.openWriter();
            writer.write(builder.toString());
            writer.flush();
            writer.close();
        } catch (IOException e) {
            
        }

    }
  • roundEnvironment.getElementsAnnotatedWith 获取被BindView注解过的元素集合
  • for循环遍历所有被注解过的元素,因为我们注解的是变量类型元素,所以做下类型校验可以用instanceof或者ele.getKind()校验都是可以的,校验完成进行强转
  • variableElement.getEnclosingElement()获取变量所在的类的TypeElement
  • 获取类的全限定名、类名、包名、变量信息等为生成代码做准备
  • 每个Activity 生成一个对应的xxxActivity_ViewInjector的类的代码

生成代码的方式可以自己手动拼接,按照java源文件的语法拼接字符串,这种方式相对麻烦,不够优雅,而且面对相对复杂的场景难以封装。所以早就前人造轮子了,用javapoet 框架来组织代码非常方便,工程引入:

implementation 'com.squareup:javapoet:1.10.0'

具体的用法可以去github看wiki,写的很详细

到此为止,注解处理器的代码基本功能已经完成了,但是现在还不能生成java代码,因为现在它仅仅只是一个普通的java类而已,那么我们怎么样才能是注解处理器在编译时被编译器识别、实例化并且运行呢?我们需要把注解处理器注册给编译器。

1、在 processors 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.process.Processors 文件;
4、在 javax.annotation.process.Processors 文件写入注解处理器的全限定名;

这样在编译器扫描源文件的时候回去改文件中查找是否有自定义注解器,如果有就会实例化运行。为了简化步骤google也为我们写了个框架只需一个注解也就是AutoService,在上面源码中也也已看到导入:

implementation 'com.google.auto.service:auto-service:1.0-rc4'

最后主工程还需要依赖processor

annotationProcessor project(':apt-processor')

查看源码就知道AutoService也是一个注解处理器,专门为自定义注解处理器生成META-INF文件的


4.png
5.png

可以在processor->build下看到:


6.png

配置好了之后rebuild工程就可以看到生成好的代码app\build\generated\source\apt:

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

inject方法传入两个参数一个是host,一个是object,在Activity中host和object是同一个对象,这里为了兼容Fragment把host和object分开。

公开API

我们自动生成的代码如果给用户使用呢,不可能说用户还要知道你的代码生成规则,知道类名然后调用,那这个体验就有点差了,这就需要我们暴露公共方法。

public class XcInject {

    public static void bind(Object host) {
        String name = host.getClass().getName();
        try {
            Class<?> aClass = Class.forName(name + "_ViewInjector");
            Method method = aClass.getMethod("inject", host.getClass(), Object.class);
            method.invoke(aClass.newInstance(),host,host);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

    }
}

使用:

public class MainActivity extends AppCompatActivity {

    @BindeView(R.id.tv_hello_world)
    public TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        XcInject.bind(this);
        tv.setText("注解成功");
    }
}

到这里就基本完成了,但是要做下代码优化,上述注解处理器只能处理单个变量的情况,实际上一个Activity是有多个变量使用注解的,这就需要把一个相同的类的所有注解元素存起来,统一去findViewById。

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindeView.class);
        for (Element ele : elements) {
            /** 判断Element的类型是否是变量类型Element */
            if(!(ele instanceof VariableElement)) {
                return false;
            }
            /** 第二种校验方式*/
            if(ele.getKind() != ElementKind.FIELD) {
                return false;
            }
            VariableElement variableElement = (VariableElement)ele;
            /** 获取封装Element,即变量所属的类的Element */
            TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement();
            /** 获取全限定名 */
            String qualifiedName = enclosingElement.getQualifiedName().toString();
            /** 获取类名 */
            String className = enclosingElement.getSimpleName().toString();
            /** 获取注解类的包名,将生成后的代码放置一个包名下 */
            String pkgName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();

//            ClassName hostName = ClassName.bestGuess(qualifiedName);
//            /** 获取变量的名字 */
//            String variableName = variableElement.getSimpleName().toString();
//            /** 获取变量的类型 */
//            String variableType = variableElement.asType().toString();
            /** 获取注解的值,也就是View的id*/
            BindeView annotation = ele.getAnnotation(BindeView.class);
            int id = annotation.value();

            /** 将相同类的Element用map装起来*/
            ClassInfo classInfo = classInfoMap.get(qualifiedName);
            if(classInfo == null) {
                classInfo = new ClassInfo(pkgName,className);
                classInfoMap.put(qualifiedName,classInfo);
            }
            /** 用id作为键值,保证唯一性 */
            classInfo.getVariableElementMap().put(id,variableElement);

        }

        /** 收集完信息,生成代码*/
        for (String key:classInfoMap.keySet()) {
            ClassInfo classInfo = classInfoMap.get(key);
            ClassName hostName = ClassName.bestGuess(key);

            MethodSpec method = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addAnnotation(Override.class)
                    .addParameter(hostName, "host")
                    .addParameter(Object.class, "object")
                    .addCode( ""
                            + "if(object instanceof android.app.Activity){\n"
                            + getMethodCode(classInfo,"android.app.Activity")
                            + "}else {\n"
                            + getMethodCode(classInfo,"android.view.View")
                            + "}"
                            + ""

                    ).build();

            /** 生成class相关信息*/
            ClassName interfaceName = ClassName.bestGuess("xc.com.apt.api.ViewInjector");
            TypeSpec classType = TypeSpec.classBuilder(classInfo.getClassName() + "_ViewInjector")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(method)
                    .addSuperinterface(ParameterizedTypeName.get(interfaceName, hostName))
                    .build();

            JavaFile build = JavaFile.builder(classInfo.getPkg(), classType)
                    .addFileComment("this file don't modify")
                    .build();
            try {
                build.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return true;
    }

这样就是显示了ButterKnife的基本功能 了。

源码

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

推荐阅读更多精彩内容