美文网首页
ARouter ksp注解处理器 实现思路

ARouter ksp注解处理器 实现思路

作者: 代码我写的怎么 | 来源:发表于2023-01-31 16:21 被阅读0次

    注解处理器到底承担了什么作用?

    考虑清楚这个问题 对我们的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中 加一个注解

    例如:

    这是大家最熟悉的代码了,那么关键的地方就在于, 我的注解处理器 到底要做什么? 要生成什么样的类,来达成我们最终的目的。

    本质上来说,我们一个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的 这三种注解处理器的时候 都要传递一个参数的

    然后再关注下 loadInto 这个方法

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

    Arouter$$Group$$group的值
    

    我们来看一下这个类里面干了啥

    这个类也有一个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

    里面的内容就简单了,把我们的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 比较好

    稍微配置一下即可, 然后打上断点,按下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 辅助类

    这里有一个难点 就是 如何写这个方法参数的类型

    看下具体代码 如何来解决这个问题

    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)
    }
    

    最后看下实现效果:

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

    path的信息:

    作者:vivo高启强
    链接:https://juejin.cn/post/7195005316067491895

    最后

    如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

    如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。

    相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

    全套视频资料:

    一、面试合集

    二、源码解析合集

    三、开源框架合集

    相关文章

      网友评论

          本文标题:ARouter ksp注解处理器 实现思路

          本文链接:https://www.haomeiwen.com/subject/bfhbhdtx.html