Android APT(注解处理器之编译时注解)

什么是注解

注解,通俗的来说,就是像注释一样,是由程序员在代码中加入的一种“标注”,不影响所编写的原有代码的执行。而这种标注(注解)可以被编码用的IDE、编译器、类加载器的代理程序、其他第三方工具以及原有代码运行期间读取和处理,生成一些新的辅助代码或是提示,从而节省时间,提升效率。这些工具读取注解的时机是根据注解的生命周期来定的,注解的生命周期就是其“存在寿命”,分为三种:

1,源注解

@Retention(RetentionPolicy.SOURCE)
注解将被编译器丢弃。如:@Override

2,类注解(ButterKnife)

@Retention(RetentionPolicy.CLASS)
注解由编译器记录在类文件中,但不需要由VM在运行时保留。

3,运行时注解(EventBus)

@Retention(RetentionPolicy.RUNTIME)
注解由编译器记录在类文件中,并在运行时由VM保存,因此可以反射性地读取它们。 如:@Deprecated

APT(Annotation Processing Tool)注解处理器, 是一个Gradle插件,协助Android Studio 处理annotation processors,

是一种处理注解的工具,确切的说它是javac的一个工具,可以在代码编译期解析注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。

Android Gradle插件2.2版本发布后,Android 官方提供了annotationProcessor插件来代替android-apt,annotationProcessor同时支持 javac 和 jack 编译方式,而android-apt只支持 javac 方式。
同时android-apt作者宣布不在维护,当然目前android-apt仍然可以正常运行

总体流程:自定义注解->自定义注解处理器(会用到javapoet)->注册注解处理器(会用到auto-service)->编译生成java代码

这面我只是简单的做了findViewId和onCliclk事件!

那我们开始说起:

image.png

如图:
apt_annotation ,一个Java Library
主要是用来自定义注解

apt_library,一个Android Library
主要是用来写调用的编译时期生成的java代码的工具类

apt_processor ,一个Java Library
主要是用来处理编译时的注解操作

为什么要建立java Library呢 ?

原因AbstractProcessor不在Android SDK里面!要是不建立 Java Library 是调用不到的!在java jre中。

首先:在apt_annotation module 建立注解
//编译时期注解,作用目标 域生明(类,接口,成员变量,类静态变量)
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}
@Retention(RetentionPolicy.CLASS)
@Target(METHOD)
public @interface OnClick {
    int[] value();
}

那注解写好了:

再来:apt_processor module 建立编译时注解处理的逻辑

在moudle中添加依赖

 implementation project(':apt_annotation')
 implementation 'com.google.auto.service:auto-service:1.0-rc2'
 implementation 'com.squareup:javapoet:1.11.1'
@AutoService(Processor.class)
public class BindViewProcessorByPoet extends AbstractProcessor {

    //写入代码会用到
    private Filer filer;

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 拿到每个类,要生成的代码集合;
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = findAndBuilderByTargets(roundEnvironment);
        for (TypeElement typeElement : builderMap.keySet()) {
            List<CodeBlock.Builder> codeList = builderMap.get(typeElement);
            // 去生成对应的 类文件;
            BindViewCreatorByPoetHelper.writeBindView(typeElement, codeList, filer);
        }
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }

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

    private Map<TypeElement, List<CodeBlock.Builder>> findAndBuilderByTargets(RoundEnvironment env) {
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = new HashMap<>();

        // 遍历带对应注解的元素,就是某个View对象
        for (Element element : env.getElementsAnnotatedWith(BindView.class)) {

            //感觉这里面应该是VariableElement
            BindViewCreatorByPoetHelper.parseBindView(element, builderMap);
        }

        // 遍历带对应注解的元素,就是某个方法
        for (Element element : env.getElementsAnnotatedWith(OnClick.class)) {
            BindViewCreatorByPoetHelper.parseListenerView(element, builderMap);
        }
        return builderMap;
    }

}

这面会实现4个方法:
init:初始化。可以得到ProcessingEnviroment,ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
getSupportedAnnotationTypes:指定这个注解处理器是注册给哪个注解的,这里说明是注解BindView和OnClick
getSupportedSourceVersion:指定使用的Java版本,通常这里返回SourceVersion.latestSupported()
process:可以在这里写扫描、评估和处理注解的代码,生成Java文件
所以说主要的还是

 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 拿到每个类,要生成的代码集合;
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = findAndBuilderByTargets(roundEnvironment);
        for (TypeElement typeElement : builderMap.keySet()) {
            List<CodeBlock.Builder> codeList = builderMap.get(typeElement);
            // 去生成对应的 类文件;
            BindViewCreatorByPoetHelper.writeBindView(typeElement, codeList, filer);
        }
        return true;
    }

这一方法:
我们详细看下
因为大都数代码里面都是有注释的:

private Map<TypeElement, List<CodeBlock.Builder>> findAndBuilderByTargets(RoundEnvironment env) {
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = new HashMap<>();

        // 遍历带对应注解的元素,就是某个View对象
        for (Element element : env.getElementsAnnotatedWith(BindView.class)) {

            //感觉这里面应该是VariableElement
            BindViewCreatorByPoetHelper.parseBindView(element, builderMap);
        }

        // 遍历带对应注解的元素,就是某个方法
        for (Element element : env.getElementsAnnotatedWith(OnClick.class)) {
            BindViewCreatorByPoetHelper.parseListenerView(element, builderMap);
        }
        return builderMap;
    }

Map<TypeElement, List<CodeBlock.Builder>> builderMap = new HashMap<>();
这一个集合进行存储,key则是其实也就是关联Actvity对象的Element,value则是写入的代码集合(一个类维护一个生成的代码块的集合)
然后分别对两个注解添加代码集合:

public class BindViewCreatorByPoetHelper {

    public static void parseBindView(Element element, Map<TypeElement, List<CodeBlock.Builder>> codeBuilderMap) {
        //获取最外层的类名,具体实际就是关联某个Activity对象Element
        //因为此时的element是VriableElement,所以拿到的Enclosing 就应该是Activity对象
        TypeElement classElement = (TypeElement) element.getEnclosingElement();
        // 这个view是哪个类 Class(android.widget.TextView)
        String viewType = element.asType().toString();
        // 注解的值,具体实际可能就是 R.id.xxx
        int value = element.getAnnotation(BindView.class).value();
        // 这个view对象名称(比如TextView)
        String name = element.getSimpleName().toString();

        //创建代码块
        //$L是占位符,会把后面的 name 参数拼接到 $L 所在的地方
        CodeBlock.Builder builder = CodeBlock.builder()
                .add("target.$L = ", name);
        builder.add("($L)target.findViewById($L)", viewType, value);

        List<CodeBlock.Builder> codeList = codeBuilderMap.get(classElement);
        if (codeList == null) {
            codeList = new ArrayList<>();
            codeBuilderMap.put(classElement, codeList);
        }
        codeList.add(builder);
    }

    public static void parseListenerView(Element element, Map<TypeElement, List<CodeBlock.Builder>> codeBuilderMap) {
        //获取最外层的类名,具体实际就是关联某个Activity对象Element
        TypeElement classElement = (TypeElement) element.getEnclosingElement();

        List<CodeBlock.Builder> codeList = codeBuilderMap.get(classElement);
        if (codeList == null) {
            codeList = new ArrayList<>();
            codeBuilderMap.put(classElement, codeList);
        }

        //注解的值
        int[] annotationValue = element.getAnnotation(OnClick.class).value();

        //因为注解@Target是Method,所以这面拿到的就是方法名字的字符串
        String name = element.getSimpleName().toString();

        //创建代码块
        for (int value : annotationValue) {
            CodeBlock.Builder builder = CodeBlock.builder();
            builder.add("target.findViewById($L).setOnClickListener(new android.view.View.OnClickListener() { public void onClick(View v) { target.$L(v); }})", value, name);
            codeList.add(builder);
        }
    }

    public static void writeBindView(TypeElement classElement, List<CodeBlock.Builder> codeList, Filer filer) {
        // enclosingElement ,暗指 某个Activity.
        // 先拿到 Activity 所在包名( cn.citytag.aptdemo.Main3Activity)
        String packageName = classElement.getQualifiedName().toString();
        packageName = packageName.substring(0, packageName.lastIndexOf("."));//(cn.citytag.aptdemo)
        // 再拿到Activity类名(Main3Activity))
        String className = classElement.getSimpleName().toString();

        //此元素定义的类型
        TypeName type = TypeName.get(classElement.asType());

        //if (type instanceof ParameterizedTypeName) {
        // type = ((ParameterizedTypeName) type).rawType;
        //}

        ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBindingPoet");
        MethodSpec.Builder methodSpecBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(type, "target", Modifier.FINAL)
                .addParameter(ClassName.get("android.view", "View"), "source", Modifier.FINAL);
        for (CodeBlock.Builder codeBuilder : codeList) {
            //方法里面 ,代码是什么
            methodSpecBuilder.addStatement(codeBuilder.build());
        }
        methodSpecBuilder.build();

        // 创建类 MainActivity_ViewBinding
        TypeSpec bindClass = TypeSpec.classBuilder(bindingClassName.simpleName())
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpecBuilder.build())
                .build();

        try {
            // 生成文件
            JavaFile javaFile = JavaFile.builder(packageName, bindClass).build();
            //将文件写出
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  List<CodeBlock.Builder> codeList = codeBuilderMap.get(classElement);
  if (codeList == null) {
          codeList = new ArrayList<>();
          codeBuilderMap.put(classElement, codeList);
   }

都会加以判断是否存在此TypeElemen的key,在进行put元素!

这样的话代码集合添加完成之后再进行写入,
还是这个代码,每一个TypeElemen对应一个代码块集合进行写入代码;

   @Override
   public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 拿到每个类,要生成的代码集合;
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = findAndBuilderByTargets(roundEnvironment);
        for (TypeElement typeElement : builderMap.keySet()) {
            List<CodeBlock.Builder> codeList = builderMap.get(typeElement);
            // 去生成对应的 类文件;
            BindViewCreatorByPoetHelper.writeBindView(typeElement, codeList, filer);
        }
        return true;
    }
 public static void writeBindView(TypeElement classElement, List<CodeBlock.Builder> codeList, Filer filer) {
        //  classElement ,就是关联的某个Activity
        // 先拿到 Activity 所在包名( cn.citytag.aptdemo.Main3Activity)
        String packageName = classElement.getQualifiedName().toString();
        packageName = packageName.substring(0, packageName.lastIndexOf("."));//(cn.citytag.aptdemo)
        // 再拿到Activity类名(Main3Activity))
        String className = classElement.getSimpleName().toString();

        //此元素定义的类型
        TypeName type = TypeName.get(classElement.asType());

        //if (type instanceof ParameterizedTypeName) {
        // type = ((ParameterizedTypeName) type).rawType;
        //}

        ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBindingPoet");
        MethodSpec.Builder methodSpecBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(type, "target", Modifier.FINAL)
                .addParameter(ClassName.get("android.view", "View"), "source", Modifier.FINAL);
        for (CodeBlock.Builder codeBuilder : codeList) {
            //方法里面 ,代码是什么
            methodSpecBuilder.addStatement(codeBuilder.build());
        }
        methodSpecBuilder.build();

        // 创建类 MainActivity_ViewBinding
        TypeSpec bindClass = TypeSpec.classBuilder(bindingClassName.simpleName())
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpecBuilder.build())
                .build();

        try {
            // 生成文件
            JavaFile javaFile = JavaFile.builder(packageName, bindClass).build();
            //将文件写出
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
然后:apt_library module 建立Tools类
public class BindViewByPoetTools {
    public static void bind(Activity activity) {
        //获取activity的decorView(根view)
        View view = activity.getWindow().getDecorView();
        bind(activity, view);
    }

    private static void bind(Object obj, View view) {
        String className = obj.getClass().getName();
        //找到该activity对应的Bind类的名字
        String generateClass = className + "_ViewBindingPoet";
        //然后调用Bind类的构造方法,从而完成activity里view的初始化
        try {
            Class.forName(generateClass).getConstructor(obj.getClass(), View.class).newInstance(obj, view);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

最后:app module 进行绑定注解,并调用Tools类!

在app module添加依赖

   implementation project(':apt_annotation')
   implementation project(':apt_library')
   annotationProcessor project(':apt_processor')
为什么没用apt呢!gradle高版本就不用那么麻烦了!直接annotationProcessor这个就可以在编译时处理注解了!
public class Main3Activity extends AppCompatActivity {
    @BindView(R.id.tv_one)
    TextView mTextViewOne;
    @BindView(R.id.tv_two)
    TextView mTextViewTwo;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        BindViewByPoetTools.bind(this);
        mTextViewOne.setText("one");
        mTextViewTwo.setText("two");
    }

    @OnClick({R.id.tv_one, R.id.tv_two})
    public void onBtn1Click(View v) {
        Toast.makeText(this, "", Toast.LENGTH_SHORT).show();
    }
}

最终:ReBuild as 则会生成如下代码:
apt_java.png
package cn.citytag.aptdemo;

import android.view.View;

public class Main3Activity_ViewBindingPoet {
  public Main3Activity_ViewBindingPoet(final Main3Activity target, final View source) {
    target.mTextViewOne = (android.widget.TextView)target.findViewById(2131165325);
    target.mTextViewTwo = (android.widget.TextView)target.findViewById(2131165326);
    target.findViewById(2131165325).setOnClickListener(new android.view.View.OnClickListener() { public void onClick(View v) { target.onBtn1Click(v); }});
    target.findViewById(2131165326).setOnClickListener(new android.view.View.OnClickListener() { public void onClick(View v) { target.onBtn1Click(v); }});
  }
}
介绍下依赖库auto-service:

auto-service的作用是向系统注册processor(自定义注解处理器),
在javac编译时,才会调用到我们这个自定义的注解处理器方法。

主要是自己建立我没有试!这个具体我也不清楚!

在使用注解处理器需要先声明,步骤:
1、需要在 processors 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;
这样声明下来也太麻烦了?这就是用引入auto-service的原因。
通过auto-service中的@AutoService可以自动生成AutoService注解处理器是Google开发的,用来生成 META-INF/services/javax.annotation.processing.Processor 文件的

介绍下依赖库 javapoet:

助于在编译期间生成java代码,要不自己StringBuilder拼接很麻烦!
https://github.com/square/javapoet

如果在as ReBuild的时候报这个问题:

错误: 编码GBK的不可映射字符
在apt_processor gradle
加入下面代码!

tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}

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