背景
官方插件方案使用了transformAPI,但是在AGP8.0开始此API已被废弃,因此需要使用新的替代方案。根据官方文档说明,我们可以使用Artifacts.forScopeAPI替代。
一、 分析官方插件的实现流程
1.1 流程图
1.2 核心机制
1.3 流程详解
阶段一:编译时扫描文件
- 扫描所有文件:官方插件使用的transform,因此这个阶段我们需要用新的Artifacts.forScopeAPI来进行适配
- 搜集路由注册文件:包含路由组文件(IRouteRoot)、服务提供者组文件(IProviderGroup)、拦截器组文件(IInterceptorGroup)这三种类型;搜集这三种类型的文件,以供后续字节码插桩生成注册代码。
- 找到目标注册文件
LogisticsCenter,作为后续字节码插桩的目标文件
阶段二:字节码修改
- ASM寻找到指定插桩的目标方法:通过ASM寻找到找到
LogisticsCenter内部的指定方法loadRouterMap方法。 - 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
)
}
}
}
}