为什么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()
}
}