注解处理器究竟承担了什么作用?

考虑清楚这个问题 对咱们的ksp完成 会有非常大的协助, 否则那一坨注解处理器的完成 你是根本不知道为什么要那样完成,自然ksp的完成你是无从下手的。

首要咱们来看一个典型的例子, 咱们有2个module 一个叫 moduleA 一个叫moduleB , moduleA中的类 想跳转到moudleB中的类 要怎样做? 一般都是

startActivity(B.class)

这就会带来一个问题了, moduleA 要引用到moduleB 中的类, 你只能让moduleA依靠moduleB, 那假如反过来呢?moduleB还得依靠moduleA了, 这种相互间的引用 必定是不可的了,那应该怎样做?

便是参阅Arouter的做法就能够了, 例如moduleB中的B.class 我想对外暴露 ,让别的module中能够跳转到我这个activity ,那就我在B.class中 加一个注解

例如:

ARouter-KSP 注解处理器实现-文件生成思路(一)

这是大家最了解的代码了,那么要害的当地就在于, 我的注解处理器 究竟要做什么? 要生成什么样的类,来达到咱们终究的意图。

本质上来说,咱们一个apk,下面必定有多个module, 不论他们的依靠联系怎样,他的编译联系都是确认的,这句话怎样理解?

moduleA 编译今后生成一堆class文件,moduleB 编译今后也生成一堆class文件, 等等。 终究这些class文件 都是在咱们的app moudle 编译时汇总的, 考虑明白这个问题 那就好理解了,

回到前面的例子中,咱们在moduleB中 加了注解,然后咱们能够使用注解处理器 来生成一个类,这个类中保护一个map,这个map的key便是 咱们注解中的path的字符串之,value 则是本身咱们B.class

这样多个module 在app module 中汇总编译的时分 咱们就能够拿到一个巨大的map 这个map中key便是path的值,value 便是方针的class

之后跳转的时分只要在navigation中传递一下path的值,然后根据再到map中寻找到对应的class就能够了。

你看,arouter的注解处理器 是干啥的,咱们就想清楚了吧,便是生成一堆辅佐类,这个辅佐类的终究意图便是帮咱们生成path 和class的对应联系的

理想和实践中的不同

在上一个末节中,咱们基本弄清楚了arouter 注解处理器的作用, 可是仅靠这一节的知识要完全看懂arouter-compiler的代码仍是不够, 由于实践上arouter 的map生成要比 咱们前面一个末节 所说的要杂乱的多。为什么?

你细心考虑一下, 假如是多个module 都使用了route注解,那这些注解的类中的path的值 是不是有可能是重复的?

比方moduleB中 有一个类叫X.class 他的path是 /x1/xc moduleC中 有一个类叫Y.class 他的 path值也是 /x1/xc

这就会导致一个问题了,在app 编译的时分 同样一个path 会对应着2个class,此时跳转就会呈现错误了。

咱们来看下,Arouter中 是怎样规划来处理这个问题的 他首要引入了一个Group的概念, 比方咱们上面的path x1 便是group, 当然你也能够手动指定group ,可是意思都是一样的

首要生成一个名为

Arouter$$Root$$moduleName

的类,这个类继承的是IRouteRoot这个接口

这儿咱们要关注的是moduleName ,咱们在用annotaionProcessor 或许kapt 或许ksp的 这三种注解处理器的时分 都要传递一个参数的

ARouter-KSP 注解处理器实现-文件生成思路(一)

ARouter-KSP 注解处理器实现-文件生成思路(一)

然后再关注下 loadInto 这个办法

这个办法一看便是生成了一个map 对吧, 这个map的key 便是 group的值,而value则是注解处理器生成的一个类 完成了IRouteGroup接口

Arouter$$Group$$group的值

咱们来看一下这个类里边干了啥

ARouter-KSP 注解处理器实现-文件生成思路(一)

这个类也有一个loadInfo 办法

它的key 便是path的值, value 便是RouteMeta目标,留意这个目标中就详细包含了Activity.class了,

所以Arouter 实践上便是把咱们的map给分了级,

首要是使用 moduleName 来生成 IRouteRoot的类 ,这样能够规避不同module之间有抵触的现象 其次是使用 group的概念 再次对路由进行分层, 这样一方面是降低抵触几率,别的一方面,使用group的概念,咱们还能够做路由的懒加载,究竟项目大了今后 一次性加载悉数路由信息也是有成本的,有了group的概念,

咱们就能够依照group的级别来加载了,实践上arouter本身路由加载也是这样做的。

路由使用group分组今后, 默认任何实践路由信息都不会加载, 当每次调用者发起一次路由加载事情时,都会依照group的信息来查找,第一次触发某个group 时,再去加载这个group下面的一切路由信息

ksp的基础完成

首要咱们新建一个module ,命名大家随意,留意这个module的build 文件写法即可

apply plugin: 'java'
apply plugin: 'kotlin'
compileJava {
    sourceCompatibility = '1.8'
    targetCompatibility = '1.8'
}
sourceSets.main {
    java.srcDirs("src/main/java")
}
dependencies {
    implementation 'com.alibaba:arouter-annotation:1.0.6'
    implementation("com.squareup:kotlinpoet:1.11.0")
    implementation("com.squareup:kotlinpoet-ksp:1.11.0")
    implementation("com.squareup:kotlinpoet-metadata:1.11.0")
    implementation 'com.alibaba:fastjson:1.2.69'
    implementation 'org.apache.commons:commons-lang3:3.5'
    implementation 'org.apache.commons:commons-collections4:4.1'
    implementation("com.google.devtools.ksp:symbol-processing-api:1.6.20-1.0.5")
}
apply from: rootProject.file('gradle/publish.gradle')

其次,去meta-inf 下 新建一个文件,文件名是固定的

com.google.devtools.ksp.processing.SymbolProcessorProvider

ARouter-KSP 注解处理器实现-文件生成思路(一)

里边的内容就简略了,把咱们的ksp注解处理器装备进去即可

com.alibaba.android.arouter.compiler.processor.RouteSymbolProcessorProvider
com.alibaba.android.arouter.compiler.processor.InterceptorSymbolProcessorProvider
com.alibaba.android.arouter.compiler.processor.AutowiredSymbolProcessorProvider

这儿要留意一下,即使是一个纯java代码的module 也能够使用ksp来生成代码的

注解处理器怎样debug?

注解处理器的代码其实还挺晦涩难明的,全靠日志打印很费事,这儿仍是会debug 比较好

ARouter-KSP 注解处理器实现-文件生成思路(一)

ARouter-KSP 注解处理器实现-文件生成思路(一)

略微装备一下即可, 然后打上断点,按下debug开关,rebuild 工程即可触发注解处理器的调试了

使用ksp 注解处理器来生成辅佐类

这儿篇幅有限, 咱们只做辅佐类的生成, 至于辅佐类里边的loadInto办法 咱们暂不做完成,详细的完成咱们留到下一篇文章再说,这一节只做一下 辅佐类生成这个操作

首要咱们来装备一下 使用ksp的module

ksp {
    arg("AROUTER_MODULE_NAME", project.getName())
}
ksp project(':arouter-compiler')

然后要留意的是,即使是纯java代码的module 也能够使用ksp来生成代码的, 唯一要留意的是你需求在这个module下 添加

apply plugin: 'kotlin-android'

现在注解处理器也装备好了, 咱们就能够干活了。

先放一个基础类就行

class RouteSymbolProcessorProvider : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return RouteSymbolProcessor(environment.options, environment.logger, environment.codeGenerator)
    }
}

第一步,咱们要取出moduleName,这个东西的作用前面现已介绍过了,

val moduleName = options[KEY_MODULE_NAME]

第二步,咱们要取出项目中 使用Route注解的类,拿到这些类的信息

// 取出来 使用route注解的
val symbols = resolver.getSymbolsWithAnnotation(Route::class.qualifiedName!!)
// 先取出来 有哪些 类用了Route注解
val elements = symbols.filterIsInstance<KSClassDeclaration>().toList()

第三步, 也是最要害的一步,咱们要取出Route 中的要害信息作一个map,key是group,value是path的list

其实也便是一个group 下面对应的一切path信息

这儿有几个要害点, 要把Route中的path和group的值 都提取出来, 假如没有指定group 则 path的第一段作为group的值

别的便是在取的时分 要判断一下 这个element的注解是不是Route注解, 由于一个类能够有多个注解,咱们要取特定的Route注解 才能取到咱们想要的值

要害代码

val map = mutableMapOf<String, List<String>>()
elements.forEach {
    it.annotations.toList().forEach { ks ->
        // 避免多个注解的情况
        if (ks.shortName.asString() == "Route") {
            var path = ""
            var group = ""
            ks.arguments.forEach { ksValueA ->
                if (ksValueA.name?.asString() == "path") {
                    path = ksValueA.value as String
                }
                if (ksValueA.name?.asString() == "group") {
                    group = ksValueA.value as String
                }
            }
            // 假如没有装备group 则去path中取
            if (group.isEmpty()) {
                group = path.split("/")[1]
            }
            if (map.contains(group)) {
                map[group] = map[group]!!.plus(path)
            } else {
                map[group] = listOf(path)
            }
        }
    }
}

第四步,咱们生成IRouteRoot 辅佐类

这儿有一个难点 便是 怎样写这个办法参数的类型

ARouter-KSP 注解处理器实现-文件生成思路(一)

看下详细代码 怎样来处理这个问题

private fun String.quantifyNameToClassName(): com.squareup.kotlinpoet.ClassName {
    val index = lastIndexOf(".")
    return com.squareup.kotlinpoet.ClassName(substring(0, index), substring(index + 1, length))
}
// IRouteRoot 这个接口 办法参数的界说 MutableMap<String, Class<out IRouteGroup>>?
val parameterSpec = ParameterSpec.builder(
    "routes",
    MUTABLE_MAP.parameterizedBy(
        String::class.asClassName(),
        Class::class.asClassName().parameterizedBy(
            WildcardTypeName.producerOf(
                Consts.IROUTE_GROUP.quantifyNameToClassName()
            )
        )
    ).copy(nullable = true)
).build()

参数的这个问题处理掉今后 就很简略了

直接依照姓名规矩 生成一下 类即可

val rootClassName = "ARouter$$Root$$$moduleName"
val packageName = "com.alibaba.android.arouter"
val file = FileSpec.builder("$packageName.routes", rootClassName)
    .addType(
        TypeSpec.classBuilder(rootClassName).addSuperinterface(
            com.squareup.kotlinpoet.ClassName(
                "com.alibaba.android.arouter.facade.template",
                "IRouteRoot"
            )
        ).addFunction(
            FunSpec.builder("loadInto").addModifiers(KModifier.OVERRIDE)
                .addParameter(parameterSpec)
                .addStatement("TODO()").build()
        ).build()
    )
    .build()
file.writeTo(codeGen, false)

最终一步, 咱们要生成IRrouteGroup的辅佐类,里边放入对应path的信息

这儿path的信息 我用注释表示下即可,

// 生成group 辅佐类
map.forEach { (key, value) ->
    val rootClassName = "ARouter$$Group$$$key"
    // IRouteGroup 这个接口 办法参数的界说 MutableMap<String,RouteMeta>?
    val parameterSpec = ParameterSpec.builder(
        "atlas",
        MUTABLE_MAP.parameterizedBy(
            String::class.asClassName(),
            RouteMeta::class.asClassName()
        ).copy(nullable = true)
    ).build()
    val packageName = "com.alibaba.android.arouter"
    // val rootClass = com.squareup.kotlinpoet.ClassName("", rootClassName)
    val file = FileSpec.builder("$packageName.routes", rootClassName)
        .addType(
            TypeSpec.classBuilder(rootClassName).addSuperinterface(
                com.squareup.kotlinpoet.ClassName(
                    "com.alibaba.android.arouter.facade.template",
                    "IRouteGroup"
                )
            ).addFunction(
                FunSpec.builder("loadInto").addModifiers(KModifier.OVERRIDE)
                    .addParameter(parameterSpec)
                    .addComment("path: $value")
                    .addStatement("TODO()").build()
            ).build()
        )
        .build()
    file.writeTo(codeGen, false)
}

最终看下完成作用:

对应的辅佐类 应该是都生成了:

ARouter-KSP 注解处理器实现-文件生成思路(一)

path的信息:

ARouter-KSP 注解处理器实现-文件生成思路(一)