What?你连用了这么久的ButterKnife原理都不知道?--深入浅出APT(一)

114 阅读7分钟

用过这么久的ButterKnife还有EventBus了。

你是不是也比较好奇为啥加了注解咱们就省了那些findViewById的繁琐工作。

EventBus到底是做了哪些操作省略了广播的查找以及分发,做到如此方便省力的解耦工作的。

今天咱们就来简单聊一下APT到底在背后默默帮我们做了多少省心事儿~

?什么是APT

APT,就是Annotation Processing Tool的简称,中文翻译过来是注解处理工具,也就是EventBus和ButterKnife用到的核心技术。

在代码编译期间可以对注解进行处理,并且生成Java文件,减少手动的代码输入。

注解还分成运行时注解和编译时注解。大名鼎鼎的Retrofit就是通过运行时注解,通过动态代理的方式来生成网络请求。

而编译时注解,像一些熟知的三方库Dagger2,ButterKnife,Eventbus其实都有使用啦~

一、编译时注解

也有人叫他代码生成,其实他们还是有些区别的。

编译时注解在编译时,通过注解,获取必要的信息,在项目中生成代码。

这些生成的代码在运行时可以调用,和咱们直接运行时提前手写的代码没有任何区别。

这种感觉就类似于告诉编译器,我用注解告诉编译器哪块位置你帮我注意一下,我打了tag了。

编译器也心领神会地给你打了一个手势,ojbk。

于是你就不需要写那些重复的逻辑性代码了,在编译器编译的时候收集并分析你的这些tag。

并且把这些参数程序化地塞进代码里,生成新代码。

上述这一系列过程又称作APT--Annotation Processing Tool

二、大概原理

其实Java API已经提供了扫描源码并解析注解的框架,我们可以通过继承AbstractProcessor类来实现自己的注解解析逻辑。

而APT的原理就是在注解了某些代码元素(如字段、函数、类等)后,在编译时编译器会检查AbstractProcessor的子类。

并且自动调用其process()方法,然后将添加了指定注解的所有代码元素作为参数传递给该方法,开发者再根据注解元素在编译期输出对应的Java代码去实现一些特定的逻辑。

拆解轮子

一、注解处理器

1、注解处理器是一个在javac中的,用来编译时扫描和处理注解的工具。你可以为特定的注解注册你自己的注解处理器。

2、注解处理器可以生成Java代码,这些生成的Java代码会组成.java文件,但不能修改已经存在的Java类(即不能向已有的类中添加新方法).而这些新生成的Java文件,会同时与其他普通的纯手写Java源代码一起被javac编译。

二、抽象处理器

每一个注解处理器都要继承于AbstractProcessor,如下所示:

注: 需要新建一个javaLib 

plugins {
    id 'java-library'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
}

dependencies {
    implementation 'com.google.auto.service:auto-service:1.0-rc5'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc5'
}
public class MyProcessor extends AbstractProcessor {

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

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

    @Override
    public Set<StringgetSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

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

几个主要的方法如下

1、init(ProcessingEnvironment processingEnv)

每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnvironment参数。ProcessingEnvironment提供很多有用的工具类Elements,Types和Filer.后面我们将看到详细的内容

2、process(Set set, RoundEnvironment roundEnvironment)

相当于每个处理器的主函数main(),在这里写扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnvironment,可以让你查询出包含特定注解的被注解元素。

3、getSupportedAnnotationTypes()

这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本 处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。

4、getSupportedSourceVersion()

用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java7的话,也可以返回SourceVersion.RELEASE_7。否则返回前者就可以了。

三、android-apt被替代

3.1 annotationProcessor替代者

Android Gradle 插件 2.2 版本的发布,之前 android-apt 作者在官网发表声明证实了后续将不会继续维护 android-apt,并推荐大家使用 Android 官方插件提供的相同能力。

也就是说, android-apt 即将告别开发者,退出历史舞台,Android Gradle 插件也提供了名为 annotationProcessor 的功能来完全代替 android-apt。

annotationProcessor和apt区别?  

Android 官方的 annotationProcessor 同时支持 javac 和 jack 编译方式,而 android-apt 只支持 javac 方式。

当然,目前 android-apt 在 Android Gradle 插件 2.2 版本上面仍然可以正常运行,如果你没有想支持 jack 编译方式的话,可以继续使用 android-apt。

3.2什么是jack编译方式?

Jack (Java Android Compiler Kit)是新的Android 编译工具,从Android 6.0 开始加入,替换原有的编译工具。

例如javac, ProGuard, jarjar和 dx。它主要负责将java代码编译成dex包,并支持代码压缩,混淆等。

3.3 Jack工具的主要优势

  • 完全开放源码,源码均在AOSP中,合作伙伴可贡献源码
  • 加快编译源码,Jack 提供特殊的配置,减少编译时间:pre-dexing, 增量编译和Jack编译服务器.
  • 支持代码压缩,混淆,重打包和multidex,不在使用额外单独的包,例如ProGuard。

四、自己造轮子

4.1 实现目标

拆解轮子之后,接下来就到了我们自己造轮子的激动时刻啦。

举个栗子。 

平常我们在APP内需要启动一个Activity时需要构造一个Intent,使用startActivity 或者 startActivityForResult等等,其实逻辑都是通用的。

而构造intent时必不可少的就是目标页的activity.class。那对于组件化的工程来说,可能出现的问题是我只知道路径。

比如我只知道目标页的name,但是由于组件化的原因,我拿不到A的class.

今天我们就通过给Activity添加一个注解

@Router(value = "MainActivity")

然后通过注解来启动Activity

MyRouter.navigate(FirstMainActivity.this, "SecondMainActivity", null);

来实现这个跳转

4.2自定义注解

首先我们需要定义注解Router,作用对象就是类,作用范围就是编译时。然后接受一个参数,也就是Activity字符串,作为我们识别跳转的Activity路由。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)

public @interface Router {

    String value();
}

4.3 创建注解处理器

注解处理器一般会是一个项目比较底层的模块,因此我们需要创建一个Java Library(注意:不是Android Library),自定义自己的处理器并且继承AbstractProcessor,自己去实现process方法。

注意 

编译时自动扫描注解时,如果你的处理器不起作用。很有可能是你的Process.class 写成了Process.class

//正确的写法
@AutoService(Processor.class)
//错误的写法
@AutoService(Process.class)
@AutoService(Processor.class)
public class RouterProcessor extends AbstractProcessor {

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        String pkgName = null;
        //首先获取注解元素
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Router.class);

        if (elements.isEmpty())
            return false;
        //定义一个public static 类型的 map方法
        MethodSpec.Builder mapMethod = MethodSpec
                .methodBuilder("init")
                .addModifiers(Modifier.PUBLICModifier.FINALModifier.STATIC);
        //遍历注解元素
        for (Element element : elements) {
            if (element.getKind() == ElementKind.CLASS) {
                pkgName = String.valueOf(processingEnv.getElementUtils()
                        .getPackageOf(element)
                        .getQualifiedName());
                Router router = element.getAnnotation(Router.class);
                //获取activity的 class name
                ClassName className = ClassName.get((TypeElement) element);
                //获取uri地址
                String path = router.value();
                //生成代码Routers.map(uri,xxx.Class);
                ClassName router1 = ClassName.get("com.example.mylibrary""MyRouter");
                mapMethod.addStatement("$T.add($S,$T.class)", router1, path, className);
            }
        }
        mapMethod.addCode("\n");

        //生成RouterMapping文件
        TypeSpec helloWorldClass = TypeSpec.classBuilder("RouterMapping")
                .addModifiers(Modifier.PUBLICModifier.FINAL)
                .addMethod(mapMethod.build())
                .build();

        assert pkgName != null;
        JavaFile javaFile = JavaFile.builder(pkgName, helloWorldClass).build();
        try {
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }

        return true;
    }

    @Override
    public Set<StringgetSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Router.class.getCanonicalName());
        return annotations;
    }

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

上面用到了JavaPoet 去生成相应的Java文件,后续也将持续更新JavaPoet的一些使用

可以看到,重点就是process方法, 方法方法里面主要工作就是生成Java文件。我们具体看下步骤

首先我们在Router的路由表里自己声明了一个map去做路由的映射

public class MyRouter {
    //页面路由表
    private static final List<Path> paths = new ArrayList<>();

    //将页面插入到路由表中
    public static void add(String path, Class<? extends Activity> activity) {
        paths.add(new Path(path, activity));
    }

    public static void navigate(Context context, String path, Bundle bundle) {
        //遍历路由表,进行uri的匹配,匹配成功,则启动对面的Activity页面
        for (Path p : paths) {
            if (p.value.equals(path)) {
                Intent intent = new Intent(context, p.getActivity());
                intent.putExtra(path, bundle);
                Log.i("navigate", path+"====="+p.getActivity());
                context.startActivity(intent);
                return;
            }
        }
    }


    /**
     * 自定义路由记录
     */
    public static class Path {
        private final String value;
        private final Class<? extends Activity> activity;

        public Path(String value, Class<? extends Activity> activity) {
            this.value = value;
            this.activity = activity;
        }

        public Class<? extends ActivitygetActivity() {
            return activity;
        }
    }

}

那新生成的Java文件需要做到的是找到该Router文件,并且利用该map将注解中的路由添加。

//遍历注解元素
for (Element element : elements) {
    if (element.getKind() == ElementKind.CLASS) {
        pkgName = String.valueOf(processingEnv.getElementUtils()
                .getPackageOf(element)
                .getQualifiedName());
        Router router = element.getAnnotation(Router.class);
        //获取activity的 class name
        ClassName className = ClassName.get((TypeElement) element);
        //获取uri地址
        String path = router.value();
        //生成代码Routers.map(uri,xxx.Class);
        ClassName router1 = ClassName.get("com.example.mylibrary""MyRouter");
        mapMethod.addStatement("$T.add($S,$T.class)", router1, path, className);
    }
}

如果你的项目结构都正确,RouterProcessor也打上了@AutoService(Processor.class)的注解。

那么build之后,会在两个子模块和主模块看到相应的RouterMapping文件,我们需要做到的是在app中提前init(),让app的路由表尽快初始化。

之后,我们的自定义路由就可以进行跳转啦~

附项目结构图

1、目录结构 

注意:router-annotation 和routerlib都是javalib

2、app的依赖关系 

implementation project(':routerlib')
implementation project(':myrouter')
implementation project(':secondmodule')
annotationProcessor project(':router-annotation')//非常重要--指定注解解析器

3、secondmodule是被隔离的子路由 

implementation project(':routerlib')
implementation project(':myrouter')
annotationProcessor project(':router-annotation')

4、myrouter是baselib,作为路由的基础方法库 

5、routerlib声明注解 

6、router-annotation 是注解解析器 

implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
implementation project(':routerlib')
implementation 'com.squareup:javapoet:1.10.0'

附项目代码~

github.com/zhuhejiaode…