ARouter适配 AGP 8.0 + 方案

为什么ARouter 无法适配AGP 8.0

具体可以看我之前的分析 简单来说, 就是去除了registerTransform函数以后,默认提供的Transform Action的方案 无法做到全量扫描,他是一个个jar包 依次执行你的aciton, 所以你无法在aciton的过程中 获取到全部ARouter的关键类信息,导致扫描结果不全 从而整体插件失败

回顾ARouter-Register插件的执行过程

即然要改这个插件,所以一定得弄清楚这个插件 到底做了什么, 其实这个插件真的非常简单,主要就干了两件事

遍历所有class,看有哪些class 是继承如下3个接口的

java 复制代码
val arouterInterfaceList = listOf(
    "com/alibaba/android/arouter/facade/template/IRouteRoot",
    "com/alibaba/android/arouter/facade/template/IInterceptorGroup",
    "com/alibaba/android/arouter/facade/template/IProviderGroup",
)

然后 搜集到信息以后 来做字节码修改。

注意看loadRouterMap方法内部,register方法 其实字节码修改的部分非常简单就是新增register方法调用 传递我们前面搜集到的class 信息即可

Transform Action的替代方案

前面说过,之所以 agp8.0+ 无法 在路由组件上生效,主要是无法在新的api下 搜集到全部的信息 那就要换一种实现方式了

建议看一下谷歌官方文档

然后仔细阅读该代码

谷歌官方代码

其实你把这个工程跑起来 读懂剩下的操作 就很简单了,无非就是照葫芦画瓢

源码修改准备工作 - 支持kotlin 插件编写

原来的register插件用groovy 编写的,新版本准备用kotlin 编写, 因为groovy 实在是太难用了

修改一下

这里agp的依赖版本 我们改成7.4.x 即可,因为要用7.4的特性

源码修改准备工作 - 增加app module 测试用例

ARouter的初始代码 app module下 没有 ARouter的 注解,注解全部在子module下,这里我们要新增一个app module 下的注解,这里是因为 后续我们的代码会对这里有影响,需要这个测试场景

我们在app module 下新建一个activity

源码修改 - 关键的task 指定

kotlin 复制代码
class PluginLaunch : Plugin<Project> {
    override fun apply(p0: Project) {
        val androidComponents = p0.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.onVariants { variant ->
            val taskProvider = p0.tasks.register("${variant.name}ModifyClasses", ModifyClassesTask::class.java)
            // 这里的参数一定得是ALL  如果是project 则在单仓多module的场景下,找不到其他module的编译结果
            variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
                .use(taskProvider)
                .toTransform(
                    ScopedArtifact.CLASSES,
                    ModifyClassesTask::allJars,
                    ModifyClassesTask::allDirectories,
                    ModifyClassesTask::output,
                )
        }
    }
}

这里其实关键的就是toTransform ,注意这个方法agp 7.4 以后才有, 有兴趣的可以自己查一下注释 其实翻译过来就是

我把搜集到的jar包,还有direct 信息都给到你这个任务里,你把输出给到output就行了

另外如果你的工程不是多module或者多aar,没有做过组件化的 ScopedArtifacts.Scope.ALL 可以改成project,这样扫描的范围更小。 否则这里一定要用ALL 这个参数

本质上其实我们的task里面要做的事情和registerTransform里 老的实现是一模一样的。 无非就是换了个api而已。

源码修改- asm处理修改

asm的部分 不介绍了, 我们只是稍微修改一下 原来的代码 即可

先定义几个变量

ruby 复制代码
// ARouter的3个重要接口
val arouterInterfaceList = listOf(
    "com/alibaba/android/arouter/facade/template/IRouteRoot",
    "com/alibaba/android/arouter/facade/template/IInterceptorGroup",
    "com/alibaba/android/arouter/facade/template/IProviderGroup",
)

// 用于存储找到的接口信息
val gatherArouterInterfaceInfo = mutableSetOf<String>()

// 生成的ARouter关键类 一定是该包名开头的
const val ROUTER_CLASS_PACKAGE_NAME = "com/alibaba/android/arouter/routes/"

第一个asm处理类,其实就是查一下这个类 是不是继承了 arouter的3个接口 如果是的话就往set里面添加即可

kotlin 复制代码
class GatherClassInfoVisitor(api: Int, cv: ClassVisitor?) :
    ClassVisitor(api, cv) {
    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?,
    ) {
        super.visit(version, access, name, signature, superName, interfaces)
        name?.let { className ->
            arouterInterfaceList.forEach {
                interfaces?.forEach { inter ->
                    if (it == inter) {
                        gatherArouterInterfaceInfo.add(className)
                    }
                }
            }
        }
    }
}

第二个就是asm写入的处理,其实就是将上面的class信息 写入到 某个方法里 而已

kotlin 复制代码
class ModifyClassVisitor(api: Int, cv: ClassVisitor?) :
    ClassVisitor(api, cv) {
    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?,
    ) {
        super.visit(version, access, name, signature, superName, interfaces)
    }

    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?,
    ): MethodVisitor {
        var mv = super.visitMethod(access, name, descriptor, signature, exceptions)
        if (name == "loadRouterMap") {
            mv = AddMethodVisitor(Opcodes.ASM5, mv)
        }
        return mv
    }
}

这里字节码就不过多介绍了,没啥意义,很简单

kotlin 复制代码
class AddMethodVisitor(api: Int, mv: MethodVisitor?) : MethodVisitor(api, mv) {
    override fun visitInsn(opcode: Int) {
        if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
            ModifyClassesTask.gatherArouterInterfaceInfo.forEach {
                val name = it.replace("/", ".")
                mv.visitLdcInsn(name)
                mv.visitMethodInsn(
                    Opcodes.INVOKESTATIC,
                    "com/alibaba/android/arouter/core/LogisticsCenter",
                    "register",
                    "(Ljava/lang/String;)V",
                    false,
                )
            }
        }

        super.visitInsn(opcode)
    }

    override fun visitMaxs(maxStack: Int, maxLocals: Int) {
        super.visitMaxs(maxStack + 4, maxLocals)
    }
}

Task的关键实现

这里稍微解释一下task的流程,

我们拿到allJars 和allDir 其实就是拿到了项目里全部的类了, 我们就可以先扫描其中有哪些类是我们想要的。

有人要问allDir和allJars的区别, 对于组件化的项目来说

allJars 就是你依赖的其他aar或者其他module中的 class信息 allDir 就是你app module里的信息

如果你没做组件化,其实你就只用扫描allDir信息就可以了,因为只有你这个app module里才用到arouter对吧。

但是要注意了,最后allJars和allDir的信息 你都是要写入到output中的。否则你的类就会少一部分。

谨记, allDir和allJars 都要最终输出到output中,但是具体扫描那一块怎么扫描 你可以自己决定以提高效率

scss 复制代码
abstract class ModifyClassesTask : DefaultTask() {

    @get:InputFiles
    abstract val allJars: ListProperty<RegularFile>

    @get:InputFiles
    abstract val allDirectories: ListProperty<Directory>

    @get:OutputFile
    abstract val output: RegularFileProperty

    companion object {

        // ARouter的3个重要接口
        val arouterInterfaceList = listOf(
            "com/alibaba/android/arouter/facade/template/IRouteRoot",
            "com/alibaba/android/arouter/facade/template/IInterceptorGroup",
            "com/alibaba/android/arouter/facade/template/IProviderGroup",
        )

        // 用于存储找到的接口信息
        val gatherArouterInterfaceInfo = mutableSetOf<String>()

        // 生成的ARouter关键类 一定是该包名开头的
        const val ROUTER_CLASS_PACKAGE_NAME = "com/alibaba/android/arouter/routes/"
    }

    // 这一步主要是查找 当前的类 是不是继承自 ARouter的关键接口 如果是 就写入该类的信息即可
    private fun scanClassForGatherClass(inputStream: InputStream) {
        val cr = ClassReader(inputStream)
        val cw = ClassWriter(cr, 0)
        val cv = GatherClassInfoVisitor(Opcodes.ASM5, cw)
        cr.accept(cv, ClassReader.EXPAND_FRAMES)
    }

    @TaskAction
    fun taskAction() {
        val jarOutput = JarOutputStream(
            BufferedOutputStream(
                FileOutputStream(
                    output.get().asFile,
                ),
            ),
        )
        println("ModifyClassTask start find Jars ARouter interface class")
        // 这一步只查找 不做任何写入
        allJars.get().forEach { file ->
            val jarFile = JarFile(file.asFile)
            // 遍历jar包里的文件 找到想要判定的class文件
            jarFile.entries().iterator().forEach { jarEntry ->
                if (jarEntry.name.startsWith(ROUTER_CLASS_PACKAGE_NAME) && jarEntry.name.endsWith(
                        ".class",
                    )
                ) {
                    val inputStream = jarFile.getInputStream(jarEntry)
                    scanClassForGatherClass(inputStream)
                    inputStream.close()
                }
            }
            jarFile.close()
        }
        println("ModifyClassTask start find Directories ARouter interface class")
        // 这一步 除了要查找当前app-module的代码是否有ARouter的关键信息以外
        // 还要负责把当前的类 拷贝到 最终task的输出中
        allDirectories.get().forEach { directory ->
            println("handling allDirectories " + directory.asFile.absolutePath)
            directory.asFile.walk().forEach { file ->
                if (file.isFile) {
                    // 只扫描class文件
                    if (file.name.endsWith(".class") && file.isDirectory.not()) {
                        scanClassForGatherClass(file.inputStream())
                    }
                    val relativePath = directory.asFile.toURI().relativize(file.toURI()).path
                    // 写入到jaroutput中
                    jarOutput.putNextEntry(JarEntry(relativePath.replace(File.separatorChar, '/')))
                    file.inputStream().use { inputStream ->
                        inputStream.copyTo(jarOutput)
                    }
                    jarOutput.closeEntry()
                }
            }
        }
        println("gatherArouterInterfaceInfo size: ${gatherArouterInterfaceInfo.size}")
        gatherArouterInterfaceInfo.forEach {
            println("gather key class info : $it")
        }
        println("ModifyClassTask start modify bytecode to LogisticsCenter")
        allJars.get().forEach { file ->
            val jarFile = JarFile(file.asFile)
            jarFile.entries().iterator().forEach { jarEntry ->

                if (jarEntry.name == "com/alibaba/android/arouter/core/LogisticsCenter.class") {
                    val inputStream = jarFile.getInputStream(jarEntry)
                    val cr = ClassReader(inputStream)
                    val cw = ClassWriter(cr, 0)
                    val cv = ModifyClassVisitor(Opcodes.ASM5, cw)
                    cr.accept(cv, ClassReader.EXPAND_FRAMES)
                    val byte = cw.toByteArray()
                    jarOutput.putNextEntry(JarEntry(jarEntry.name))
                    jarOutput.write(byte)
                    inputStream.close()
                    return@forEach
                }
                kotlin.runCatching {
                    // 有重复的 META/INF 路径 就不管了
                    jarOutput.putNextEntry(JarEntry(jarEntry.name))
                    jarFile.getInputStream(jarEntry).use {
                        it.copyTo(jarOutput)
                    }
                }.let {
                    if (it.isFailure) {
                        logger.error("ModifyClassesTask copy file failed")
                        logger.error("${it.exceptionOrNull()}")
                    }
                }
                jarOutput.closeEntry()
            }
            jarFile.close()
        }
        jarOutput.close()

    }
}
相关推荐
Estar.Lee2 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh2 小时前
uiautomator案例
android
工业甲酰苯胺3 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3434 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee5 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯6 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey7 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!9 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟9 小时前
Android音频采集
android·音视频
小白也想学C11 小时前
Android 功耗分析(底层篇)
android·功耗