Arouter官方插件适配AGP8.0

157 阅读2分钟

背景

官方插件方案使用了transformAPI,但是在AGP8.0开始此API已被废弃,因此需要使用新的替代方案。根据官方文档说明,我们可以使用Artifacts.forScopeAPI替代。

一、 分析官方插件的实现流程

1.1 流程图

arouter1.png

1.2 核心机制

arouter2.png

1.3 流程详解

阶段一:编译时扫描文件

  1. 扫描所有文件:官方插件使用的transform,因此这个阶段我们需要用新的Artifacts.forScopeAPI来进行适配
  2. 搜集路由注册文件:包含路由组文件(IRouteRoot)、服务提供者组文件(IProviderGroup)、拦截器组文件(IInterceptorGroup)这三种类型;搜集这三种类型的文件,以供后续字节码插桩生成注册代码。
  3. 找到目标注册文件LogisticsCenter,作为后续字节码插桩的目标文件

阶段二:字节码修改

  1. ASM寻找到指定插桩的目标方法:通过ASM寻找到找到LogisticsCenter内部的指定方法loadRouterMap方法。
  2. ASM核心插入逻辑:代码块中的name就是第2步中的路由注册文件路径
extension.classList.forEach { name ->
val newName = name.replace("/", ".")
    println("visitor jar file class: $newName")
    mv.visitLdcInsn(newName)
    mv.visitMethodInsn(
        Opcodes.INVOKESTATIC,
        ScanSetting.GENERATE_TO_CLASS_NAME,
        ScanSetting.REGISTER_METHOD_NAME,
        "(Ljava/lang/String;)V",
        false
    )
} 

6. 结果:在loadRouterMap内部生成以下事例的代码

register("com.alibaba.android.arouter.routes.ARouter$$Root$$m_module1")
register("com.alibaba.android.arouter.routes.ARouter$$Root$$m_module2")
...

二、适配AGP8.0实现方案

2.1 ScanSetting

class ScanSetting {
    companion object {
        const val PLUGIN_NAME = "com.alibaba.arouter"
        /**
         * The register code is generated into this class
         */
        const val GENERATE_TO_CLASS_NAME = "com/alibaba/android/arouter/core/LogisticsCenter"
        /**
         * you know. this is the class file(or entry in jar file) name
         */
        const val GENERATE_TO_CLASS_FILE_NAME = "$GENERATE_TO_CLASS_NAME.class"
        /**
         * The register code is generated into this method
         */
        const val GENERATE_TO_METHOD_NAME = "loadRouterMap"
        /**
         * The package name of the class generated by the annotationProcessor
         */
        const val ROUTER_CLASS_PACKAGE_NAME = "com/alibaba/android/arouter/routes/"
        /**
         * The package name of the interfaces
         */
        const val INTERFACE_PACKAGE_NAME = "com/alibaba/android/arouter/facade/template/"

        /**
         * register method name in class: {@link #GENERATE_TO_CLASS_NAME}
         */
        const val REGISTER_METHOD_NAME = "register"
    }
    /**
     * scan for classes which implements this interface
     */
    var interfaceName = ""

    /**
     * jar file which contains class: {@link #GENERATE_TO_CLASS_NAME}
     */
    var fileContainsInitClass: File = File("")
    /**
     * scan result for {@link #interfaceName}
     * class names in this list
     */
    var classList: MutableList<String> = mutableListOf()

    /**
     * constructor for arouter-auto-register settings
     * @param interfaceName interface to scan
     */
    constructor(interfaceName: String) {
        this.interfaceName = INTERFACE_PACKAGE_NAME + interfaceName
    }
}

2.2 ARoutClassVisitor

class ARoutClassVisitor(
    classVisitor: ClassVisitor?,
    private val appLikeProxyClassList: MutableList<String> = mutableListOf()
) : ClassVisitor(Opcodes.ASM9, classVisitor) {

    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 == ScanSetting.GENERATE_TO_METHOD_NAME) {
            mv = RouteMethodVisitor(Opcodes.ASM5, mv)
        }
        return mv
    }

    inner class RouteMethodVisitor(api: Int, mv: MethodVisitor?): MethodVisitor(api, mv) {

        override fun visitInsn(opcode: Int) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                appLikeProxyClassList.forEach{ name ->
                    val newName = name.replace("/", ".")
                    println("visitor jar file class: $newName")
                    mv.visitLdcInsn(newName)
                    mv.visitMethodInsn(
                        Opcodes.INVOKESTATIC,
                        ScanSetting.GENERATE_TO_CLASS_NAME,
                        ScanSetting.REGISTER_METHOD_NAME,
                        "(Ljava/lang/String;)V",
                        false
                    )
                }
            }
            super.visitInsn(opcode)
        }

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

2.3 RegisterCodeGenerator

class RegisterCodeGenerator(private val registerList: MutableList<ScanSetting>) {
    companion object {
        fun insertInitCodeTo(registerSetting: MutableList<ScanSetting>, jarOutput: JarOutputStream) {
            if (registerSetting.isNotEmpty()) {
                val processor = RegisterCodeGenerator(registerSetting)
                val bytes = RouterClassesTask.needOperateByteArray
                if (bytes != null) {
                    processor.insertInitCodeIntoJarFile(bytes, jarOutput)
                }
            }
        }
    }
    
    fun insertInitCodeIntoJarFile(bytes: ByteArray?, jarOutput: JarOutputStream) {
        jarOutput.putNextEntry(JarEntry(ScanSetting.GENERATE_TO_CLASS_FILE_NAME))
        val input = ByteArrayInputStream(bytes)
        jarOutput.write(referHackWhenInit(input))
        input.use {
            it.copyTo(jarOutput)
        }
        jarOutput.closeEntry()
    }

    private fun referHackWhenInit(inputStream: InputStream): ByteArray {
        val cr = ClassReader(inputStream)
        val cw = ClassWriter(cr, 0)
        val cv = MyClassVisitor(Opcodes.ASM5, cw)
        cr.accept(cv, ClassReader.EXPAND_FRAMES)
        return cw.toByteArray()
    }

    inner class MyClassVisitor(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 == ScanSetting.GENERATE_TO_METHOD_NAME) {
                mv = RouteMethodVisitor(Opcodes.ASM5, mv)
            }
            return mv
        }
    }

    inner class RouteMethodVisitor(api: Int, mv: MethodVisitor?): MethodVisitor(api, mv) {

        override fun visitInsn(opcode: Int) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                registerList.forEach { extension ->
//                    val extension = ScanSetting(it.interfaceName)
                    if (extension.classList.isEmpty()) {
                        println("No class implements found for interface: ${extension.interfaceName}")
                    } else {
                        extension.classList.forEach { name ->
                            val newName = name.replace("/", ".")
                            println("visitor jar file class: $newName")
                            mv.visitLdcInsn(newName)
                            mv.visitMethodInsn(
                                Opcodes.INVOKESTATIC,
                                ScanSetting.GENERATE_TO_CLASS_NAME,
                                ScanSetting.REGISTER_METHOD_NAME,
                                "(Ljava/lang/String;)V",
                                false
                            )
                        }
                    }
                }

            }
            super.visitInsn(opcode)
        }

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

2.4 RouterClassesTask

internal abstract class RouterClassesTask : DefaultTask() {
    companion object {
        var registerList: MutableList<ScanSetting> = mutableListOf(
            ScanSetting("IRouteRoot"),
            ScanSetting("IInterceptorGroup"),
            ScanSetting("IProviderGroup")
        )
        var needOperateByteArray: ByteArray? = null
    }
    @get:Incremental
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val jars: ListProperty<RegularFile>

    @get:Incremental
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val dirs: ListProperty<Directory>

    @get:OutputFile
    abstract val output: RegularFileProperty

    @get:Optional
    @get:OutputFile
    abstract val doc: RegularFileProperty

    @TaskAction
    fun taskAction() {
        JarOutputStream(FileOutputStream(output.get().asFile)).use { jarOutput ->
            jars.get().forEach { file ->
                if (ScanUtil.shouldProcessPreDexJar(file.asFile.absolutePath)) {
                    ScanUtil.scanJar(file.asFile, jarOutput)
                }
            }

            dirs.get().forEach { directory ->
                directory.asFile.walk().filter { it.isFile }.forEach { file ->
                    val relativePath = directory.asFile.toURI().relativize(file.toURI()).path
                    val entryName = relativePath.replace(File.separatorChar, '/')
                    if (ScanUtil.shouldProcessClass(entryName)) {
                        ScanUtil.scanClass(file)
                    }
                    jarOutput.putNextEntry(JarEntry(entryName))
                    file.inputStream().use { inputStream ->
                        inputStream.copyTo(jarOutput)
                    }
                    jarOutput.closeEntry()
                }
            }
            if (needOperateByteArray != null) {
                RegisterCodeGenerator.insertInitCodeTo(registerList, jarOutput)
            }
        }
    }

}

2.5 ArouterPlugin

class ArouterPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            val appExtension = target.extensions.getByType(AndroidComponentsExtension::class.java)
            appExtension.onVariants { variant ->
                val taskProviderTransformAllClassesTask =
                    target.tasks.register(
                        "${variant.name}RouterClassesTask",
                        RouterClassesTask::class.java
                    )
                variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
                    .use(taskProviderTransformAllClassesTask)
                    .toTransform(
                        ScopedArtifact.CLASSES,
                        RouterClassesTask::jars,
                        RouterClassesTask::dirs,
                        RouterClassesTask::output
                    )

            }
        }
    }
}