用过这么久的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<String> getSupportedAnnotationTypes() {
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.PUBLIC, Modifier.FINAL, Modifier.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.PUBLIC, Modifier.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<String> getSupportedAnnotationTypes() {
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 Activity> getActivity() {
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'
附项目代码~