Android APT 快速教程

Android APT快速教程

简介

APT(Annotation Processing Tool)即注解处理器,是一种用来处理注解的工具。JVM会在编译期就运行APT去扫描处理代码中的注解然后输出java文件。

confused.jpg

简单来说~~就是你只需要添加注解,APT就可以帮你生成需要的代码

许多的Android开源库都使用了APT技术,如ButterKnife、ARouter、EventBus等

动手实现一个简单的APT

apt_steps.jpg

小目标

在使用Java开发Android时,页面初始化的时候我们通常要写大量的view = findViewById(R.id.xx)代码

作为一个优(lan)秀(duo)的程序员,我们现在就要实现一个APT来完成这个繁琐的工作,通过一个注解就可以自动给View获得实例

本demo地址

第零步 创建一个项目

创建一个项目,名称叫 apt_demo
创建一个Activity,名称叫 MainActivity
在布局中添加一个TextView, id为test_textview


example.jpg

第一步 自定义注解

创建一个Java Library Module名称叫 apt-annotation

在这个module中创建自定义注解 @BindView

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}
  • @Retention(RetentionPolicy.CLASS):表示这个注解保留到编译期
  • @Target(ElementType.FIELD):表示注解范围为类成员(构造方法、方法、成员变量)

第二步 实现APT Compiler

创建一个Java Library Module名称叫 apt-compiler

在这个Module中添加依赖

dependencies {
    implementation project(':apt-annotation')
}
androidstudio_version.jpg

在这个Module中创建BindViewProcessor类

直接给出代码~~

public class BindViewProcessor extends AbstractProcessor {
    private Filer mFilerUtils;       // 文件管理工具类
    private Types mTypesUtils;    // 类型处理工具类
    private Elements mElementsUtils;  // Element处理工具类

    private Map<TypeElement, Set<ViewInfo>> mToBindMap = new HashMap<>(); //用于记录需要绑定的View的名称和对应的id

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mFilerUtils = processingEnv.getFiler();
        mTypesUtils = processingEnv.getTypeUtils();
        mElementsUtils = processingEnv.getElementUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes; //将要支持的注解放入其中
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();// 表示支持最新的Java版本
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        System.out.println("start process");
        if (set != null && set.size() != 0) {
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//获得被BindView注解标记的element

            categories(elements);//对不同的Activity进行分类

            //对不同的Activity生成不同的帮助类
            for (TypeElement typeElement : mToBindMap.keySet()) {
                String code = generateCode(typeElement);    //获取要生成的帮助类中的所有代码
                String helperClassName = typeElement.getQualifiedName() + "$$Autobind"; //构建要生成的帮助类的类名

                //输出帮助类的java文件,在这个例子中就是MainActivity$$Autobind.java文件
                //输出的文件在build->source->apt->目录下
                try {
                    JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement);
                    Writer writer = jfo.openWriter();
                    writer.write(code);
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
            return true;
        }

        return false;
    }

    //将需要绑定的View按不同Activity进行分类
    private void categories(Set<? extends Element> elements) {
        for (Element element : elements) {  //遍历每一个element
            VariableElement variableElement = (VariableElement) element;    //被@BindView标注的应当是变量,这里简单的强制类型转换
            TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement(); //获取代表Activity的TypeElement
            Set<ViewInfo> views = mToBindMap.get(enclosingElement); //views储存着一个Activity中将要绑定的view的信息
            if (views == null) {    //如果views不存在就new一个
                views = new HashSet<>();
                mToBindMap.put(enclosingElement, views);
            }
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);    //获取到一个变量的注解
            int id = bindAnnotation.value();    //取出注解中的value值,这个值就是这个view要绑定的xml中的id
            views.add(new ViewInfo(variableElement.getSimpleName().toString(), id));    //把要绑定的View的信息存进views中
        }

    }

    //按不同的Activity生成不同的帮助类
    private String generateCode(TypeElement typeElement) {
        String rawClassName = typeElement.getSimpleName().toString(); //获取要绑定的View所在类的名称
        String packageName = ((PackageElement) mElementsUtils.getPackageOf(typeElement)).getQualifiedName().toString(); //获取要绑定的View所在类的包名
        String helperClassName = rawClassName + "$$Autobind";   //要生成的帮助类的名称

        StringBuilder builder = new StringBuilder();
        builder.append("package ").append(packageName).append(";\n");   //构建定义包的代码
        builder.append("import com.example.apt_api.template.IBindHelper;\n\n"); //构建import类的代码

        builder.append("public class ").append(helperClassName).append(" implements ").append("IBindHelper");   //构建定义帮助类的代码
        builder.append(" {\n"); //代码格式,可以忽略
        builder.append("\t@Override\n");    //声明这个方法为重写IBindHelper中的方法
        builder.append("\tpublic void inject(" + "Object" + " target ) {\n");   //构建方法的代码
        for (ViewInfo viewInfo : mToBindMap.get(typeElement)) { //遍历每一个需要绑定的view
            builder.append("\t\t"); //代码格式,可以忽略
            builder.append(rawClassName + " substitute = " + "(" + rawClassName + ")" + "target;\n");    //强制类型转换

            builder.append("\t\t"); //代码格式,可以忽略
            builder.append("substitute." + viewInfo.viewName).append(" = ");    //构建赋值表达式
            builder.append("substitute.findViewById(" + viewInfo.id + ");\n");  //构建赋值表达式
        }
        builder.append("\t}\n");    //代码格式,可以忽略
        builder.append('\n');   //代码格式,可以忽略
        builder.append("}\n");  //代码格式,可以忽略

        return builder.toString();
    }

    //要绑定的View的信息载体
    class ViewInfo {
        String viewName;    //view的变量名
        int id; //xml中的id

        public ViewInfo(String viewName, int id) {
            this.viewName = viewName;
            this.id = id;
        }
    }
}

too_long.jpg
take_it_easy.jpg

2-1 继承AbstractProcessor抽象类

我们自己实现的APT类需要继承AbstractProcessor这个类,其中需要重写以下方法:

  • init(ProcessingEnvironment processingEnv)
  • getSupportedAnnotationTypes()
  • getSupportedSourceVersion()
  • process(Set<? extends TypeElement> set, RoundEnvironment roundEnv)

2-2 init(ProcessingEnvironment processingEnv)方法

这不是一个抽象方法,但progressingEnv参数给我们提供了许多有用的工具,所以我们需要重写它

@Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mFilerUtils = processingEnv.getFiler();
        mTypesUtils = processingEnv.getTypeUtils();
        mElementsUtils = processingEnv.getElementUtils();
    }
  • mFilterUtils 文件管理工具类,在后面生成java文件时会用到
  • mTypesUtils 类型处理工具类,本例不会用到
  • mElementsUtils Element处理工具类,后面获取包名时会用到

限于篇幅就不展开对这几个工具的解析,读者可以自行查看文档~

2-3 getSupportedAnnotationTypes()

由方法名我们就可以看出这个方法是提供我们这个APT能够处理的注解

这也不是一个抽象方法,但仍需要重写它,否则会抛出异常(滑稽),至于为什么有兴趣可以自行查看源码~

这个方法有固定写法~

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes; //将要支持的注解放入其中
    }

2-4 getSupportedSourceVersion()

顾名思义,就是提供我们这个APT支持的版本号

这个方法和上面的getSupportedAnnotationTypes()类似,也不是一个抽象方法,但也需要重写,也有固定的写法

@Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();// 表示支持最新的Java版本
    }

2-5 process(Set<? extends TypeElement> set, RoundEnvironment roundEnv)

最主要的方法,用来处理注解,这也是唯一的抽象方法,有两个参数

  • set 参数是要处理的注解的类型集合
  • roundEnv 表示运行环境,可以通过这个参数获得被注解标注的代码块
@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        System.out.println("start process");
        if (set != null && set.size() != 0) {
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);//获得被BindView注解标记的element

            categories(elements);//对不同的Activity进行分类

            //对不同的Activity生成不同的帮助类
            for (TypeElement typeElement : mToBindMap.keySet()) {
                String code = generateCode(typeElement);    //获取要生成的帮助类中的所有代码
                String helperClassName = typeElement.getQualifiedName() + "$$Autobind"; //构建要生成的帮助类的类名

                //输出帮助类的java文件,在这个例子中就是MainActivity$$Autobind.java文件
                //输出的文件在build->source->apt->目录下
                try {
                    JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName, typeElement);
                    Writer writer = jfo.openWriter();
                    writer.write(code);
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
            return true;
        }
        return false;
    }

process方法的大概流程是:

  1. 扫描所有被@BindView标记的Element
  2. 遍历Element,调用categories方法,把所有需要绑定的View变量按所在的Activity进行分类,把对应关系存在mToBindMap中
  3. 遍历所有Activity的TypeElment,调用generateCode方法获得要生成的代码,每个Activity生成一个帮助类

2-6 categories方法

把所有需要绑定的View变量按所在的Activity进行分类,把对应关系存在mToBindMap中

    private void categories(Set<? extends Element> elements) {
        for (Element element : elements) {  //遍历每一个element
            VariableElement variableElement = (VariableElement) element;    //被@BindView标注的应当是变量,这里简单的强制类型转换
            TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement(); //获取代表Activity的TypeElement
            Set<ViewInfo> views = mToBindMap.get(enclosingElement); //views储存着一个Activity中将要绑定的view的信息
            if (views == null) {    //如果views不存在就new一个
                views = new HashSet<>();
                mToBindMap.put(enclosingElement, views);
            }
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);    //获取到一个变量的注解
            int id = bindAnnotation.value();    //取出注解中的value值,这个值就是这个view要绑定的xml中的id
            views.add(new ViewInfo(variableElement.getSimpleName().toString(), id));    //把要绑定的View的信息存进views中
        }
    }

注解应该已经很详细了~
实现方式仅供参考,读者可以有自己的实现

2-7 generateCode方法

按照不同的TypeElement生成不同的帮助类(注:参数中的TypeElement对应一个Activity)

private String generateCode(TypeElement typeElement) {
        String rawClassName = typeElement.getSimpleName().toString(); //获取要绑定的View所在类的名称
        String packageName = ((PackageElement) mElementsUtils.getPackageOf(typeElement)).getQualifiedName().toString(); //获取要绑定的View所在类的包名
        String helperClassName = rawClassName + "$$Autobind";   //要生成的帮助类的名称

        StringBuilder builder = new StringBuilder();
        builder.append("package ").append(packageName).append(";\n");   //构建定义包的代码
        builder.append("import com.example.apt_api.template.IBindHelper;\n\n"); //构建import类的代码

        builder.append("public class ").append(helperClassName).append(" implements ").append("IBindHelper");   //构建定义帮助类的代码
        builder.append(" {\n"); //代码格式,可以忽略
        builder.append("\t@Override\n");    //声明这个方法为重写IBindHelper中的方法
        builder.append("\tpublic void inject(" + "Object" + " target ) {\n");   //构建方法的代码
        for (ViewInfo viewInfo : mToBindMap.get(typeElement)) { //遍历每一个需要绑定的view
            builder.append("\t\t"); //代码格式,可以忽略
            builder.append(rawClassName + " substitute = " + "(" + rawClassName + ")" + "target;\n");    //强制类型转换

            builder.append("\t\t"); //代码格式,可以忽略
            builder.append("substitute." + viewInfo.viewName).append(" = ");    //构建赋值表达式
            builder.append("substitute.findViewById(" + viewInfo.id + ");\n");  //构建赋值表达式
        }
        builder.append("\t}\n");    //代码格式,可以忽略
        builder.append('\n');   //代码格式,可以忽略
        builder.append("}\n");  //代码格式,可以忽略

        return builder.toString();
    }

大家可以对比生成的代码

package com.example.apt_demo;
import com.example.apt_api.template.IBindHelper;

public class MainActivity$$Autobind implements IBindHelper {
    @Override
    public void inject(Object target ) {
        MainActivity substitute = (MainActivity)target;
        substitute.testTextView = substitute.findViewById(2131165315);
    }

}

有没有觉得字符串拼接很麻烦呢,不仅麻烦还容易出错,那么有没有更好的办法呢(留个坑)

同样的~ 这个方法的设计仅供参考啦啦啦~~~

finished.jpg

第三步 注册你的APT

这应该是最简单的一步
这应该是最麻烦的一步

what.jpg

客官别急,往下看就知道了~

注册一个APT需要以下步骤:

  1. 需要在 processors 库的 main 目录下新建 resources 资源文件夹;
  2. 在 resources文件夹下建立 META-INF/services 目录文件夹;
  3. 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
  4. 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;
example2.jpg

正如我前面所说的~
简单是因为都是一些固定的步骤
麻烦也是因为都是一些固定的步骤

最后一步 对外提供API

4.1 创建一个Android Library Module,名称叫apt-api,并添加依赖

dependencies {
    ...

    api project(':apt-annotation')
}
androidstudio_version.jpg

4.2 分别创建launcher、template文件夹

example3.jpg

4.3 在template文件夹中创建IBindHelper接口

public interface IBindHelper {
    void inject(Object target);
}

这个接口主要供APT生成的帮助类实现

4.4在launcher文件夹中创建AutoBind类

    public class AutoBind {
        private static AutoBind instance = null;

        public AutoBind() {
        }

        public static AutoBind getInstance() {
            if(instance == null) {
                synchronized (AutoBind.class) {
                    if (instance == null) {
                        instance = new AutoBind();
                    }
                }
            }
            return instance;
        }

        public void inject(Object target) {
            String className = target.getClass().getCanonicalName();
            String helperName = className + "$$Autobind";
            try {
                IBindHelper helper = (IBindHelper) (Class.forName(helperName).getConstructor().newInstance());
                helper.inject(target);
            }   catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

这个类是我们的API的入口类,使用了单例模式
inject方法是最主要的方法,但实现很简单,就是通过反射去调用APT生成的帮助类的方法去实现View的自动绑定

完成!拉出来遛遛

在app module里添加依赖

dependencies {
    ...
    annotationProcessor project(':apt-compiler')
    implementation project(':apt-api')
}
androidstudio_version.jpg

我们再来修改MainActivity中的代码

public class MainActivity extends AppCompatActivity {

    @BindView(value = R.id.test_textview)
    public TextView testTextView;

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

        AutoBind.getInstance().inject(this);
        testTextView.setText("APT 测试");
    }
}

大功告成!我们来运行项目试试看


result.jpg

TextView已经正确显示了文字,我们的小demo到这里就完成啦~

还可以更好

我们的APT还可以变得更简单!

痛点

  • 生成代码时字符串拼接麻烦且容易出错
  • 继承AbstrctProcessor时要重写多个方法
  • 注册APT的步骤繁琐

下面我们来逐个击破~

使用JavaPoet来替代拼接字符串

JavaPoet是一个用来生成Java代码的框架,对JavaPoet不了解的请移步官方文档
JavaPoet生成代码的步骤大概是这样(摘自官方文档):

    MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

使用JavaPoet来生成代码有很多的优点,不容易出错,可以自动import等等,这里不过多介绍,有兴趣的同学可以自行了解

使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion()

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.example.apt_annotation.BindView")
public class BindViewProcessor extends AbstractProcessor {
    ...
}

这是javax.annotation.processing中提供的注解,直接使用即可

使用Auto-Service来自动注册APT

这是谷歌官方出品的一个开源库,可以省去注册APT的步骤,只需要一行注释
先在apt-compiler模块中添加依赖

dependencies {
    ...
    
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
}

然后添加注释即可

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    ...
}

总结

APT是一个非常强大而且频繁出现在各种开源库的工具,学习APT不仅可以让我们在阅读开源库源码时游刃有余也可以自己开发注解框架来帮自己写代码~
本demo地址

end.jpg

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

推荐阅读更多精彩内容

  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,656评论 2 59
  • 介绍 APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它...
    带心情去旅行阅读 40,819评论 45 164
  • 本文由玉刚说写作平台提供写作赞助,版权归玉刚说微信公众号所有原作者:Xiasem版权声明:未经玉刚说许可,不得以任...
    xiasem阅读 36,092评论 21 146
  • 感恩中午吃完饭去山上散步……呼吸一下新鲜空气……锻炼下腿脚 瞬间幸福感倍整…… 今年过年没有在爸妈身边……好想念他...
    灿宝儿阅读 168评论 0 0
  • 小兽依偎在大兽身上,睡着了。大兽说,头朝左,不放下了
    大雨不愁阅读 172评论 0 1