我能用注解处理器APT做什么 - 手写一个路由框架
引言
一般来说,我们在项目开发中,功能性类似的同一层级,会有许多相同逻辑。很多时候,一个简单有效的方法,就是定义base类,比如我们已经司空见惯以至于写习惯了的BaseActivity、BaseFragment、BasePresenter等,然后在子类对应的流程节点中,对super进行调用。
而在父类中实现的这些逻辑,被设计之初就是对应的某些特定的切面(Aspect),可以称作AOP思想的一种体现。
但在某些情况下,很多逻辑并不适合放在Activity基类中,在无法多继承的限制下,就需要通过代理或包装的方式来实现解耦,例如androidx中的LifeCycle相关框架。
同样的,面向对象的思想中,组合也是优于继承的。
特别是对于一些可以高度抽象、代码高度一致的情况,为了避免大量的重复性代码,可以通过APT或ASM进行代码的自动生成,或者对编译完的class进行字节码操作插桩。今天主要来聊一聊APT的使用。
APT 是什么
APT 全称 Annotation Processing Tool,即注解处理器,APT 工具是用于注解处理的命令行程序,它可以找到源码中对应注解的对象(类、方法、字段)并使用注解处理器对其进行处理。
进而根据需求在编译期生成一些附加的代码,然后与源码共同进行编译。最后打包到一起。
注解处理器是基于注解(Annotation)的,需要定义一些注解,对应到想要处理的对象。例如ButterKnife对字段的注入,需要定义字段上的注解;例如ARouter对页面的关联,需要定义类上的注解。
然后利用 javax.annotation.processing.Processor 对代码中带有对应注解的部分进行扫描,拿到注解下的类信息、字段信息、方法信息,按想要的逻辑进行自动编码,生成供后期调用的逻辑。
路由框架要解决什么问题
在单项目单模块的开发场景中,activity之间的相互跳转没有任何障碍,彼此间的代码都是可见的,如果需要统一跳转逻辑,方便review路由线路,也只需要一个工具类,把页面引用、需要的参数进行简单封装而已。
但在模块化的开发中,模块间没有互相依赖,无法感知其他模块中的具体类型,就无法拿到构建intent需要的Activity.class。
当然也可以定义公用的字符串常量,使用反射进行class的加载。但反射的性能较差,固定的少量使用还好,但随着项目增大,页面数量上涨,页面间数据交互复杂化,过多的反射使用就可能成为性能瓶颈。
而且,既然都要对页面路由进行封装了,难道就不搞个传递数据的自动注入?还用getExtras、putExtras一个一个的赋值?不会吧不会吧?
好,现在我们有了初步的预期,想要一个可以跨模块的,可以根据字符串或数字对应页面并实现跳转的,可以传递各种参数、而且可以在目标页面中自动注入参数的脚手架!
这篇文章,我们就一起来实现一个自己的路由框架。
本文很多地方参考了ARouter的实现,并做了许多简化,重在说明APT的使用和路由框架的思想。
先来画几个图,清晰一下总流程
自己画的-1.jpg如上图,模块化的项目中,主要业务功能集中在几个功能模块里,能够引用的逻辑只有自身包含的,和基础功能集中所包含的。而在APP模块中,可以拿到所有模块的引用
所以,我们可以在APP中,在启动时,将各个模块中想要暴露的XXX.class收集起来,放到基础功能中某个地方中存起来,然后在功能模块里,就可以使用约定好的方式,通过某个数字或字符串标签,路由到对应页面中了。
自己画的凑合看吧-2.png
如上图,这个流程用文字来描述就是:
- 你要有一个路由器框架🤣🤣🤣
- 在每个功能模块中,准备好自己包含的路由信息(有几个页面可以被别人调,每个页面对应什么标签)。
- app启动时,收集所有功能模块中暴露出的路由信息,写到公用路由器的路由表中。
- 功能中需要跳转时,调用路由器提供的方法,由路由器查询路由表,并执行跳转。
好!现在就能写一个路由框架啦!
是的,根据上面的分析,现在真的能写出路由框架了,其实比想象的还要简单。
只需要定义这样两个类,就算是完事了,请看下面的代码:
- Router 单例路由器,包含路由表,提供跳转方法。
- MetaProvider 元信息提供者接口,需要功能模块中提供实现,供app调用收集。
路由器
/** 没错这就是路由器本器 */
class Router private constructor() {
companion object {
val instance = Router()
}
// 路由表,存着支持跳转的class信息
private val metaMap = hashMapOf<String, Class<*>>()
// 使用这个方法注入元信息,可多次使用
fun injectMeta(provider: MetaProvider) {
metaMap.putAll( provider.getMetaHere() )
}
fun jumpTo(context: Context, tag: String) {
if(null == metaMap[tag]) {
// 找不到,就打个日志好了
} else {
context.startActivity(Intent(context, metaMap[tag]))
}
}
}
元信息提供者
interface MetaProvider {
fun getMetaHere(): Map<String, Class<*>>
}
模块中的具体实现
A 模块中有这么一个实现类,其他模块也是类似
class A_MetaProvider : MetaProvider {
// 在模块中写这个,当然可以使用自己包含的页面引用
override fun getMetaHere(): Map<String, Class<*>> {
return mapOf(
"A-Activity111" to Activity111::class.java,
"A-Activity222" to Activity222::class.java,
"A-Activity333" to Activity333::class.java,
)
}
}
就这,已经可以用辣
class SimpleApplication: Application() {
// 分别调用几个模块中的提供者,把所有模块提供的元信息都塞到路由表里面
override fun onCreate() {
super.onCreate()
Router.instance.injectMeta(A_MetaProvider())
Router.instance.injectMeta(B_MetaProvider())
Router.instance.injectMeta(C_MetaProvider())
}
}
具体使用跳转的时候就像这样:
----------- 省略一大堆 ------------
btnTest.setOnClickListener {
Router.instance.jumpTo(this@SimpleActivity, "A-Activity222")
}
----------- 省略一大堆 ------------
是的没错,虽然很多情况还没有考虑,但这还真就是我们路由框架的关键流程,这个sdk中的全部,就是这少到可怜的一个类,和一个接口。
真的,就这样吧,一滴都没有了。
欸?好像哪里不太对,这个文章标题是啥来着???
嘿,皮一下,就很舒服。
🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡🤡
从上面的代码中可以看出,有一些代码是属于重复性的模板代码的。
比如功能模块中的Provider实现类...们,
比如Application中几乎完全相同的三句注入代码。
这种规律性的重复代码,很适合在编译期生成,可以通过APT的能力,定义一个类注解,挂在Activity类上,注解的值就是tag字符串,这样开发者就不需要再写这些类似清单的蛋疼代码了。
现在开始操刀进行APT改造
现在我们主要的任务是如何自动生成元信息提供者,和自动化的注入逻辑。路由类和提供者接口是不需要动的。
首先在sdk模块中定义一个注解
- 这个当然是要写到路由框架模块中的
@Target(AnnotationTarget.CLASS) // 这个代表此注解可以在类上使用
@Retention(AnnotationRetention.SOURCE) // 这个是作用范围,只在源码期有效,编译完就没了,反射拿不到
annotation class RouteMeta(val value: String)
挂到Activity上
@RouteMeta("A-Activity222")
class SimpleActivity2: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// do something
}
}
然后跳转还是上面写过的onClick中的内容。
因为是跨模块的开发,我们定义一个变量声明在模块的gradle中
--- build.gradle --- 这里我们定一个 "ROUTER_NAME" 变量,值就是模块项目名,稍后APT中会使用到
android {
......
defaultConfig {
......
javaCompileOptions {
annotationProcessorOptions {
arguments = [ROUTER_NAME : project.getName()]
}
}
}
}
新建一个注解处理器的module,我们就叫它route-apt
这个库中不会包含安卓资源,所以建个java library就可以了,需要添加google的auto-service库,我们的全部本事都靠它才能施展。还需要把注解类按原包结构直接复制一份过来。
因为java-library不能依赖android-library,所以其实最好是把注解单独作为一个模块,这样sdk和apt都可以对其依赖。
gradle全文:
apply plugin: 'java-library'
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation 'com.google.auto.service:auto-service:1.0.1'
}
最核心的是一个处理器类,继承AbstractProcessor
因为我抄了很多现有java逻辑,这一部分就不用kotlin了(才不是懒呢)。
/** 这类就是我们所有处理的入口了 */
public class MetaProcessor extends AbstractProcessor {
private static final String KEY_ROUTER_NAME = "ROUTER_NAME";
private Messager messager;
private Elements elementUtils;
private Types types;
// 这里重写一下初始化,拿一些工具,因为这部分代码是编译时过程的一部分,没有debug的机会,
// 只能在每个关键阶段多打一些日志来验证了。
// 当然,processingEnv其实是父类的成员变量,这里不拿也可以在任何地方拿到。
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
types = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
}
// 这个方法重写一下,返回我们要处理的注解类名,用于过滤
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new HashSet<>();
supportTypes.add(RouteMeta.class.getCanonicalName());
return supportTypes;
}
// 重点就在这里了,这是处理的入口方法,会从这里开始处理源代码,并根据逻辑生成新代码。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
process方法逻辑相对复杂,我们拿出来单独看
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
String routerName = processingEnv.getOptions().get(KEY_ROUTER_NAME);
messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: --------- process start --------- " + routerName);
// 搞一个map在循环时找到目标暂存进去
Map<String, TypeElement> elementMap = new HashMap<>();
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(RouteMeta.class);
for (Element item : elements) {
if(isActivity(item.asType())) {
RouteMeta routeMeta = item.getAnnotation(RouteMeta.class);
messager.printMessage(Diagnostic.Kind.NOTE,
"MetaProcessor: found activity: " + item.asType().toString()
+ ", path = " + routeMeta.value());
if(!elementMap.containsKey(routeMeta.value())) {
elementMap.put(routeMeta.value(), (TypeElement) item);
}
}
}
messager.printMessage(Diagnostic.Kind.NOTE,
"MetaProcessor: find activity with annotation end, count is " + elementMap.size());
// 已经找到了当前模块中所有带有 RouteMeta 注解的activity,开始生成对应provider类
generateCode(routerName, elementMap);
return true;
}
// 判断是否是activity类型
private boolean isActivity(TypeMirror typeMirror) {
TypeMirror activityType = elementUtils.getTypeElement("android.app.Activity").asType();
if(!types.isSubtype(typeMirror, activityType)) {
messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: unsupported annotation on type: " + typeMirror.toString());
return false;
}
return true;
}
// 生成包含全部带注解的 activity 的 provider,这部分就是写代码了,根据拿到的信息,一句一句拼出来
private void generateCode(String routerName, Map<String, TypeElement> elementMap) {
if(elementMap.isEmpty()) {
return;
}
String packageName = "com.example.router";
String className = routerName + "_$_RouteProvider";
StringBuilder stringBuffer = new StringBuilder("package ").append(packageName).append(";\n\n");
stringBuffer.append("import java.util.Map;\n");
stringBuffer.append("import java.util.HashMap;\n");
stringBuffer.append("import com.example.route_sdk.MetaProvider;\n\n");
stringBuffer.append("public class ").append(className).append(" implements MetaProvider {\n");
stringBuffer.append("public Map<String, Class<?>> getMetaHere() {\n");
stringBuffer.append("HashMap<String, Class<?>> map = new HashMap<>();\n");
elementMap.forEach( (k, v) -> {
stringBuffer.append("map.put(")
.append("\"").append(k).append("\"")
.append(", ")
.append(v.asType().toString())
.append(".class);\n");
});
stringBuffer.append("return map;\n");
stringBuffer.append("}\n");
stringBuffer.append("}\n");
try {
Writer writer = processingEnv.getFiler()
.createSourceFile(className)
.openWriter();
writer.write(stringBuffer.toString());
writer.close();
messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: write java file done, class name is: " + className);
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.NOTE, "MetaProcessor: catch IOException when write java file, class name is: " + className);
e.printStackTrace();
}
}
为了让gradle识别我们的processor,需要添加配置文件。
在src\main下创建文件夹resources,在其中创建 META-INF\services,
创建一个文本文件:javax.annotation.processing.Processor
内容是处理器全类名,如:com.example.route_apt.MetaProcessor
好,代码写完,build一下项目,控制台中可以看到输出的日志了,代表我们的Processor生效了:
看个截图吧1.png然后,在模块下的build文件夹中可以分别找到生成的java文件,和临时编译的class文件:
build\generated\ap_generated_sources\debug\out 下的 xxx.java
build\intermediates\javac\debug\classes\com\example\person 下的 xxx.class
(我的studio版本是北极狐,gradle版本7.0,不同版本下的临时文件目录可能不一致。)
最后再简化一下初始注入逻辑
APT的工作此时已经完成了,上面生成的类会随着项目代码一起打包到最终的apk中,代码中也是可见的。在初始化时可以通过直接引用来完成路由注入。
但我们在知道其命名规则的情况下,完全可以通过类名反射的方式来使用,避免写过多重复代码,这部分的反射调用数量等于模块的数量,不会太多,性能也不会成为问题。
在Router里加一个注入方法:
public class Router {
..................
............
/**
* 通过模块名和固定的包名和类后缀,拼接要加载的Provider类名,通过反射进行路由信息收集
* @param projectNames 要加载的模块工程名
*/
public void injectFromProject(String... projectNames) {
String packageName = "com.example.router.";
String classSuffix = "_$_RouteProvider";
try {
for (String item : projectNames) {
MetaProvider provider = (MetaProvider) Class.forName(packageName + item + classSuffix).newInstance();
injectMeta(provider);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
这个方法需要传入模块名,还是会有一些耦合性,阿里的ARouter这部分逻辑是遍历apk中所有dex,根据包名和类型来找出对应的Provider 的。这部分代码比较多,这里就不贴了。
最后,在Application的onCreate中,调用此方法完成初始化,就可以啦!
Router.getInstance().injectFromProject("app", "person", "order", "xxxx");
🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉
--------------------------- 完结,撒花 ---------------------------
🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉