前言
最近公司也在适配项目至Android 14 否则市场不让上架,因此 gradle也必须适配升级了,就先来试试水。
目前最新的AGP已经到8.3了,适配AGP8.0也要提上日程了,尤其是一些插件项目,因为8.0删除了transform API,所以需要提前做好适配工作。
对于之前未了解过gradle自定义插件的同学可以看下我以前的7.0
之前的gradle自定义插件
的用法 教你如何一步步实现路由插件。
而本篇文章主要讲解transform过时后该如何适配
,所以未接触过的可以先看我之前的文章,Android 路由的实现流程,也包括了asm,注解的处理
效果图如下:
首先创建插件模块
首先我们新建一个空项目,然后在项目中开始添加模块。
注意我在以前的项目中新建了一个分支arouter_use_plugin_gradle7.6
用于适配, 因为现在是使用kotlin所以新建了一个kotlin
文件夹,注意名字一定要对,否则不会帮你编译kotlin文件。结构如下:
build文件配置如下:注意比以前多了kotlin的配置
plugins {
id 'java-library'
id 'groovy'
id 'kotlin-kapt'
id 'kotlin'
}
dependencies {
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
implementation "com.android.tools.build:gradle:7.4.0"
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10"
implementation("org.ow2.asm:asm:9.3")
implementation("org.ow2.asm:asm-commons:9.3")
implementation("org.ow2.asm:asm-analysis:9.3")
implementation("org.ow2.asm:asm-util:9.3")
implementation("org.ow2.asm:asm-tree:9.3")
implementation("commons-io:commons-io:2.8.0")
implementation("org.javassist:javassist:3.29.0-GA")
}
//以下内容主要用于发布插件
//gradle 7.0之后过时 参考:https://docs.gradle.org/current/userguide/publishing_maven.html#publishing_maven:complete_example
//apply plugin: 'maven'
//repositories {
// mavenCentral()
//}
//group = 'com.example.myplugin'
//version = '1.0.3'
//
//uploadArchives {
// repositories {
// mavenDeployer {
./是当前目录../是父级目录
// repository(url: uri('./../repo'))
// }
// }
//}
//以下内容主要用于发布插件
apply plugin: 'maven-publish'
repositories {
mavenCentral()
}
group = 'com.example.arouterplugin'
version = '1.0.0'
publishing {
publications {
mavenJava(MavenPublication) {
groupId = group
artifactId = 'arouter'
version = version
from components.java
}
}
repositories {
maven {
// change URLs to point to your repos, e.g. http://my.org/repo
// def releasesRepoUrl = layout.buildDirectory.dir('repos/releases')
// def snapshotsRepoUrl = layout.buildDirectory.dir('repos/snapshots')
// url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
// ./是当前目录../是父级目录
url ='./../repo'
}
}
}
一、创建插件
下面主要流程是:
- 创建一个实现Plugin接口的插件类MyPlugin1
- 在apply方法中,通过AndroidComponentsExtension获取variant信息
- 使用onVariants遍历每一个variant
- 在遍历中,注册一个AsmClassTransform类,用于ASM修改字节码
- 同时注册一个RouterClassesTask类,用于先扫描全部字节码文件
- 通过variant.artifacts.forScope配置task的输入输出
- 将task的输出作为AsmClassTransform的输入进行字节码修改这样就实现了一个编译时字节码修改的Gradle插件。
package com.example.arouterplugin
import com.android.build.api.artifact.ScopedArtifact
import com.android.build.api.instrumentation.InstrumentationScope
import com.android.build.api.variant.AndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.ScopedArtifacts
import com.example.arouterplugin.gradle76.AsmClassTransform
import com.example.arouterplugin.gradle76.scanall.RouterClassesTask
/**
*@Author tgw
*@Date 2023/09/07
*@describe
*/
class MyPlugin1: Plugin<Project> {
override fun apply(project: Project) {
System.out.println("Android gradle 7.6版本Plugin 使用的AndroidComponentsExtension.................");
//这里appExtension获取方式与原transform api不同,可自行对比
val appExtension = project.extensions.getByType(
AndroidComponentsExtension::class.java
)
System.out.println("Android gradle 7.6版本Plugin 使用的AndroidComponentsExtension.................");
//这里通过transformClassesWith替换了原registerTransform来注册字节码转换操作
appExtension.onVariants { variant ->
//可以通过variant来获取当前编译环境的一些信息,最重要的是可以 variant.name 来区分是debug模式还是release模式编译
System.out.println("Android gradle 7.6版本Plugin 便利.................");
variant.instrumentation.transformClassesWith(AsmClassTransform::class.java, InstrumentationScope.ALL) {
}
}
//先扫描所有文件,在对想要操作的字节码进行操作
appExtension.onVariants { variant ->
val taskProviderTransformAllClassesTask =
project.tasks.register(
"${variant.name}RouterClassesTask",
RouterClassesTask::class.java
)
// https://github.com/android/gradle-recipes
variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
.use(taskProviderTransformAllClassesTask)
.toTransform(
ScopedArtifact.CLASSES,
RouterClassesTask::jars,
RouterClassesTask::dirs,
RouterClassesTask::output
)
}
}
}
常规写法:MyPlugin1 注册要在 resources/META-INF/gradle-plugins/com.example.arouterplugin.properties
文件中,注意必须是这个路径,
详情可看:
一、适配Transform,使用 AsmClassVisitorFactory
**
*@Author tgw
*@Date 2023/09/07
*@describe 一个一个文件的操作扫描
*/
abstract class AsmClassTransform : AsmClassVisitorFactory<InstrumentationParameters.None> {
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
//指定真正的ASM转换器
return AppAroutClassVisitorNew(nextClassVisitor)
}
//通过classData中的当前类的信息,用来过滤哪些类需要执行字节码转换,这里支持通过类名,包名,注解,接口,父类等属性来组合判断
//isInstrumentable用于控制我们的自定义Visitor是否需要处理这个类,通过这个方法可以过滤我们不需要的类,加快编译速度
override fun isInstrumentable(classData: ClassData): Boolean {
println("tgw1 visit isInstrumentable: " + classData.className)
//指定包名执行
// return true
return classData.className.contains("com.example.base_arouter.ARouterUtils")
}
}
以上代码:
-
isInstrumentable用于控制我们的自定义Visitor是否需要处理这个类,通过这个方法可以过滤我们不需要的类,加快编译速度,因为我们只想操作
com.example.base_arouter.ARouterUtils.class
文件,所以过滤它 -
createClassVisitor 创建一个asm 操作器
AppAroutClassVisitorNew
class AppAroutClassVisitorNew(
classVisitor: ClassVisitor?,
val appLikeProxyClassList: MutableList<String> = mutableListOf()
) : ClassVisitor(
Opcodes.ASM9, classVisitor
) {
private var mClassName: String? = null
private var mSuperName: String? = null
private var mInterfaces: Array<String>? = null
override fun visit(
version: Int, access: Int, name: String?, signature: String?,
superName: String?, interfaces: Array<String>?
) {
mClassName = name
mSuperName = superName
mInterfaces = interfaces
println("tgw2 visit: mClassName :$mClassName")
super.visit(version, access, name, signature, superName, interfaces)
}
override fun visitMethod(
access: Int,
name: String?,
desc: String?,
signature: String?,
exception: Array<out String>?
): MethodVisitor {
println("tgw3 visitMethod: " + name)
var mv = super.visitMethod(access, name, desc, signature, exception)
//找到 AppLifeCycleManager里的loadAppLike()方法,我们在这个方法里插入字节码
if ("loadRouterMap" == name) {
mv = LoadAppAsmRouterLikeMethodAdapter(
mv,
appLikeProxyClassList,
mClassName ?: "",
access,
name,
desc
)
}
return mv
}
}
- visit():方法拿到该类的一些基本信息
- visitMethod() :拿到该类的所有方法,这里在
loadRouterMap
方法中,去进行字节插入,为什么是这个方法去进行插入可看, Android 自定义gradle插件实现一个Android路由框架
然后就是字节码操作:LoadAppAsmRouterLikeMethodAdapter
类
class LoadAppAsmRouterLikeMethodAdapter(
mv: MethodVisitor,
private val appLikeProxyClassList: MutableList<String>,
private val mClassName: String,
access: Int,
name: String?,
desc: String?
) : AdviceAdapter(Opcodes.ASM9, mv, access, name, desc) {
// java -classpath /Users/tanguiwu/Downloads/asm-util-9.3.jar org.objectweb.asm.util.ASMifier /Users/tanguiwu/demo/apt-use-learing/base-arouter/build/tmp/kotlin-classes/debug/com/example/base_arouter/ARouterUtils.class
// java -classpath /Users/tanguiwu/Downloads/asm-all-5.1.jar org.objectweb.asm.util.ASMifier /Users/tanguiwu/demo/apt-use-learing/base-arouter/build/tmp/kotlin-classes/debug/com/example/base_arouter/ARouterUtils.class
override fun onMethodEnter() {
super.onMethodEnter()
println("tgw5 -------mClassName------${mClassName}")
mv.visitCode();
//这个有问题拿不到想要被注册的 路由类的集合
val proxyClassName = "ArouterCustomActivityARouteInterfaceImp.class"
//遍历插入字节码,其实就是在 loadAppLike() 方法里插入类似registerAppLike("");的字节码
println("tgw6 开始注入代码:${proxyClassName}")
val fullName = PluginTools.PROXY_CLASS_PACKAGE_NAME + proxyClassName.substring(
0,
proxyClassName.length - 6
)
println("tgw7 注入代码 full classname = ${fullName}")
mv.visitTypeInsn(NEW, fullName);
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, fullName, "<init>", "()V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(
GETFIELD,
PluginTools.REGISTER_CLASS_FILE_NAME.substring(
0,
PluginTools.REGISTER_CLASS_FILE_NAME.length - 6
),
"mRouterMap",
"Ljava/util/Map;"
);
mv.visitMethodInsn(INVOKEVIRTUAL, fullName, "register", "(Ljava/util/Map;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
// mv.visitCode();
// mv.visitTypeInsn(NEW, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp");
// mv.visitInsn(DUP);
// mv.visitMethodInsn(INVOKESPECIAL, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp", "<init>", "()V", false);
// mv.visitVarInsn(ALOAD, 0);
// mv.visitFieldInsn(GETFIELD, "com/example/base_arouter/ARouterUtils", "mRouterMap", "Ljava/util/Map;");
// mv.visitMethodInsn(INVOKEVIRTUAL, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp", "register", "(Ljava/util/Map;)V", false);
// mv.visitInsn(RETURN);
// mv.visitMaxs(2, 1);
// mv.visitEnd();
}
override fun onMethodExit(opcode: Int) {
super.onMethodExit(opcode)
println("-------onMethodExit------")
}
}
上面一段代码就是我在 ARouterUtils
类的loadRouterMap
方法中插入了如下一段代码: 将路由进行初始化
private fun loadRouterMap() {
com.example.myaptuselearing.routes.ArouterCustomActivityARouteInterfaceImp().register(mRouterMap)
}
二、AsmClassVisitorFactory 局限性怎么办(使用DefaultTask)
AsmClassVisitorFactory是没有办法做到像Transform一样,先扫描所有class
收集结果,再执行ASM
修改字节码。原因是AsmClassVisitorFactory在isInstrumentable
方法中确定需要对哪些class
进行ASM
操作,当返回true
之后,就执行了createClassVisitor
方法进行字节码操作去了,这就导致可能你路由表都还没收集完成就去修改了目标class
。
机灵的小伙伴可能会想,那我再注册一个收集路由表的[AsmClassVisitorFactory],我试了一下不行他们会交替执行,
package com.example.arouterplugin
import com.android.build.api.artifact.ScopedArtifact
import com.android.build.api.instrumentation.InstrumentationScope
import com.android.build.api.variant.AndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.ScopedArtifacts
import com.example.arouterplugin.gradle76.AsmClassTransform
import com.example.arouterplugin.gradle76.scanall.RouterClassesTask
/**
*@Author tgw
*@Date 2023/09/07
*@describe
*/
class MyPlugin1: Plugin<Project> {
override fun apply(project: Project) {
System.out.println("Android gradle 7.6版本Plugin 使用的AndroidComponentsExtension.................");
//这里appExtension获取方式与原transform api不同,可自行对比
val appExtension = project.extensions.getByType(
AndroidComponentsExtension::class.java
)
System.out.println("Android gradle 7.6版本Plugin 使用的AndroidComponentsExtension.................");
....
//先扫描所有文件,在对想要操作的字节码进行操作
appExtension.onVariants { variant ->
val taskProviderTransformAllClassesTask =
project.tasks.register(
"${variant.name}RouterClassesTask",
RouterClassesTask::class.java
)
// https://github.com/android/gradle-recipes
variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
.use(taskProviderTransformAllClassesTask)
.toTransform(
ScopedArtifact.CLASSES,
RouterClassesTask::jars,
RouterClassesTask::dirs,
RouterClassesTask::output
)
}
}
}
我们使用了onVariants
API 注册了一个名为gather[Debug|Release]RouterClassesTask
的Task
,返回一个TaskProvider
对象,然后使用variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
来使用这个Task
进行toTransform
操作,可以发现我们无需手动执行该Task
。来看下这个toTransform
的定义。
/**
* Defines all possible operations on a [ScopedArtifact] artifact type.
*
* Depending on the scope, inputs may contain a mix of [org.gradle.api.file.FileCollection],
* [RegularFile] or [Directory] so all [Task] consuming the current value of the artifact must
* provide two input fields that will contain the list of [RegularFile] and [Directory].
*
*/
interface ScopedArtifactsOperation<T: Task> {
/**
* Append a new [FileSystemLocation] (basically, either a [Directory] or a [RegularFile]) to
* the artifact type referenced by [to]
*
* @param to the [ScopedArtifact] to add the [with] to.
* @param with lambda that returns the [Property] used by the [Task] to save the appended
* element. The [Property] value will be automatically set by the Android Gradle Plugin and its
* location should not be considered part of the API and can change in the future.
*/
fun toAppend(
to: ScopedArtifact,
with: (T) -> Property<out FileSystemLocation>,
)
/**
* Set the final version of the [type] artifact to the input fields of the [Task] [T].
* Those input fields should be annotated with [org.gradle.api.tasks.InputFiles] for Gradle to
* property set the task dependency.
*
* @param type the [ScopedArtifact] to obtain the final value of.
* @param inputJars lambda that returns a [ListProperty] or [RegularFile] that will be used to
* set all incoming files for this artifact type.
* @param inputDirectories lambda that returns a [ListProperty] or [Directory] that will be used
* to set all incoming directories for this artifact type.
*/
fun toGet(
type: ScopedArtifact,
inputJars: (T) -> ListProperty<RegularFile>,
inputDirectories: (T) -> ListProperty<Directory>)
/**
* Transform the current version of the [type] artifact into a new version. The order in which
* the transforms are applied is directly set by the order of this method call. First come,
* first served, last one provides the final version of the artifacts.
*
* @param type the [ScopedArtifact] to transform.
* @param inputJars lambda that returns a [ListProperty] or [RegularFile] that will be used to
* set all incoming files for this artifact type.
* @param inputDirectories lambda that returns a [ListProperty] or [Directory] that will be used
* to set all incoming directories for this artifact type.
* @param into lambda that returns the [Property] used by the [Task] to save the transformed
* element. The [Property] value will be automatically set by the Android Gradle Plugin and its
* location should not be considered part of the API and can change in the future.
*/
fun toTransform(
type: ScopedArtifact,
inputJars: (T) -> ListProperty<RegularFile>,
inputDirectories: (T) -> ListProperty<Directory>,
into: (T) -> RegularFileProperty)
/**
* Transform the current version of the [type] artifact into a new version. The order in which
* the replace [Task]s are applied is directly set by the order of this method call. Last one
* wins and none of the previously set append/transform/replace registered [Task]s will be
* invoked since this [Task] [T] replace the final version.
*
* @param type the [ScopedArtifact] to replace.
* @param into lambda that returns the [Property] used by the [Task] to save the replaced
* element. The [Property] value will be automatically set by the Android Gradle Plugin and its
* location should not be considered part of the API and can change in the future.
*/
fun toReplace(
type: ScopedArtifact,
into: (T) -> RegularFileProperty
)
}
RouterClassesTask
任务如下: 主要流程是找到需要被注册的路由文件名称存入集合,找到注册表文件ARouterUtils
在其中进行文件的注册
- 遍历输入的jar文件列表jars,使用JarFile打开每个jar文件。
- 对于jar文件的每个条目jarEntry,判断如果是需要修改的目标类文件,则调用回调函数处理该类的字节码。其他条目则直接复制到输出的jar文件中。
- 同时还遍历目录列表dirs,把目录中的class文件也复制到输出jar中。
- 在过程中收集所有需要注入字节码的目标类全路径,存入addOperateList。
- 最后当遍历完成后,以jarEntry的方式把修改过的目标类文件字节码写入输出jar中。
关键点:
- processTargetJars��法,找到目标类并获取字节码交给回调处理。
- use方法确保流关闭。
- 收集全路径addOperateList,方便后续注入字节码。
- 在最后重新写入修改后的目标类字节码。这样通过Gradle task就可以在构建时修改jar包中的类字节码,实现需要的修改注入逻辑。需要注意的是,需要确保修改后的字节码与原逻辑一致,不要破坏原有功能。
package com.example.arouterplugin.gradle76.scanall
import com.example.arouterplugin.gradle76.PluginTools
import groovy.json.JsonOutput
import org.gradle.api.DefaultTask
import org.gradle.api.file.Directory
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileOutputStream
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
/**
*@Author tgw
*@Date 2023/09/11
*@describe 扫描全部的文件后,再去操作想要操作的文件
*
*
* 参考:
* https://github.com/JailedBird/ArouterGradlePlugin/blob/main/arouter-gradle-plugin/src/main/java/cn/jailedbird/arouter_gradle_plugin/TransformAllClassesTask.kt
* https://juejin.cn/post/7222091234100330554
*/
internal abstract class RouterClassesTask : DefaultTask() {
@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
// @Inject
// abstract fun getWorkerExecutor(): WorkerExecutor
@TaskAction
fun taskAction(inputChanges: InputChanges) {
val addOperateList = mutableListOf<String>()
var needOperateByteArray: ByteArray? = null
JarOutputStream(FileOutputStream(output.get().asFile)).use { jarOutput ->
processTargetJars(jarOutput,addOperateList,){
needOperateByteArray = it
}
processDirs(jarOutput,addOperateList)
//获取所有需要被注册的类
addOperateList.forEach {
println("tgw7 addfile $it")
}
//这行代码先在jarOutput中创建一个名为jarEntry.name的新条目
// 新条目 com/example/base_arouter/ARouterUtils.class
// 该条目在之前的操作processTargetJars()方法中,跳过了
jarOutput.putNextEntry(JarEntry(PluginTools.REGISTER_CLASS_FILE_NAME))
//拿到之前保存的要操作的字节码文件
val input= ByteArrayInputStream(needOperateByteArray)
val reader = ClassReader(input)
val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
val classVisitor = RouterAllCassVisitor(classWriter,addOperateList)
reader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
jarOutput.write(classWriter.toByteArray())
input.use { inputStream ->
inputStream.copyTo(jarOutput)
}
jarOutput.closeEntry()
}
}
private fun processDirs(jarOutput: JarOutputStream,addOperateList: MutableList<String>) {
dirs.get().forEach { directory ->
directory.asFile.walk().forEach { file ->
if (file.isFile) {
println("tgw3 jar file: ${file.name}")
val relativePath = directory.asFile.toURI().relativize(file.toURI()).path
println("tgw4 jar file relativePath: ${relativePath}")
jarOutput.putNextEntry(JarEntry(relativePath.replace(File.separatorChar, '/')))
println("tgw6 jar file relativePath replace : ${relativePath.replace(File.separatorChar, '/')}")
if (PluginTools.isTargetProxyClass(relativePath?:"")) {
println("tgw7 jar file isTargetProxyClass: ${relativePath}")
addOperateList.add(relativePath?:"")
}
file.inputStream().use { inputStream ->
inputStream.copyTo(jarOutput)
}
jarOutput.closeEntry()
}
}
}
}
/**
*jarOutput.putNextEntry(JarEntry(jarEntry.name))
*
* jarFile.getInputStream(jarEntry).use {
* it.copyTo(jarOutput)
* }
* 解释
*1. 使用jarOutput.putNextEntry创建一个和输入jarEntry同名的新入口到输出jar中。
* 2. 使用jarFile.getInputStream获取输入jarEntry对应的输入流。
* 3. 使用InputStream的copyTo方法将输入流复制到JarOutputStream中,完成内容的复制。
* 4. 用use方法保证流被关闭。
* 这样,就实现了从输入的JarFile选择性地复制jarEntry到输出的jar文件中。
*/
private fun processTargetJars(
jarOutput: JarOutputStream,
addOperateList: MutableList<String>,
needOperateByteArrayListener:(it:ByteArray) -> Unit
) {
jars.get().forEach { file ->
JarFile(file.asFile).use { jarFile ->
jarFile.entries().iterator().forEach { jarEntry ->
//找到目标要 操作的 class文件 ,我这里是:com/example/base_arouter/ARouterUtils.class 文件
if (!jarEntry.isDirectory && jarEntry.name.contains(PluginTools.REGISTER_CLASS_FILE_NAME)) {
println("tgw101 jar target: ${jarEntry.name}")
jarFile.getInputStream(jarEntry).use {
// 获取所要操作文件的字节流,后面构建新的文件进行操作
needOperateByteArrayListener.invoke(it.readAllBytes())
}
} else {
runCatching {
println("tgw21 jar : ${jarEntry.name}")
if (PluginTools.isTargetProxyClass(jarEntry.name?:"")) {
addOperateList.add(jarEntry.name?:"")
}
jarOutput.putNextEntry(JarEntry(jarEntry.name))
jarFile.getInputStream(jarEntry).use {
it.copyTo(jarOutput)
}
}/*.onFailure { e ->
Log.e("Copy jar entry failed. [entry:${jarEntry.name}]", e)
}*/
jarOutput.closeEntry()
}
}
}
}
}
}
字节操作:
下面这段代码实现的是通过ASM在指定方法中插入新的Java字节码,来动态注入一些行为逻辑。主要的类是:
RouterAllCassVisitor
这个类继承自ClassVisitor,它会在访问到每个类时通过visitMethod遍历方法,然后在需要注入代码的方法上使用自定义的MethodVisitor。RouterAlllCassVisitorMethodAdapter
这是自定义的MethodVisitor,通过继承自AdviceAdapter,在onMethodEnter时可以插入新的字节码逻辑。关键的逻辑在onMethodEnter方法中:-
- 首先通过appLikeProxyClassList获取到需要注入的类名列表。
-
- 然后遍历该列表,并通过ASM的指令生成对应的字节码逻辑,主要是:- NEW创建实例
- DUP复制栈顶元素
- INVOKESPECIAL调用构造方法初始化
- 获取注册所需的Map字段
- INVOKEVIRTUAL调用register方法进行注册
-
- 这段代码的效果就是在指定方法中动态插入对一些类的实例化并注册的逻辑,实现了一种动态注入和增强的效果。目前这里的
appLikeProxyClassList
在上面已经拿到了想要操作的目标,需要在类访问时能获取到实际的类名列表才能真正实现动态注入。总体上利用了ASM直接操作字节码插入新的逻辑的能力,实现了一种动态增强的效果。
package com.example.arouterplugin.gradle76.scanall
import com.example.arouterplugin.gradle76.PluginTools
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.commons.AdviceAdapter
/**
*@Author tgw
*@Date 2023/09/07
*@describe
*/
class RouterAllCassVisitor(
classVisitor: ClassVisitor?,
val appLikeProxyClassList: MutableList<String> = mutableListOf()
) : ClassVisitor(
Opcodes.ASM9, classVisitor
) {
private var mClassName: String? = null
private var mSuperName: String? = null
private var mInterfaces: Array<String>? = null
override fun visit(
version: Int, access: Int, name: String?, signature: String?,
superName: String?, interfaces: Array<String>?
) {
mClassName = name
mSuperName = superName
mInterfaces = interfaces
println("tgw2 visit: mClassName :$mClassName")
super.visit(version, access, name, signature, superName, interfaces)
}
override fun visitMethod(
access: Int,
name: String?,
desc: String?,
signature: String?,
exception: Array<out String>?
): MethodVisitor {
println("tgw3 visitMethod: " + name)
var mv = super.visitMethod(access, name, desc, signature, exception)
//找到 AppLifeCycleManager里的loadAppLike()方法,我们在这个方法里插入字节码
if ("loadRouterMap" == name) {
mv = RouterAlllCassVisitorMethodAdapter(
mv,
appLikeProxyClassList,
mClassName ?: "",
access,
name,
desc
)
}
return mv
}
}
class RouterAlllCassVisitorMethodAdapter(
mv: MethodVisitor,
private val appLikeProxyClassList: MutableList<String>,
private val mClassName: String,
access: Int,
name: String?,
desc: String?
) : AdviceAdapter(Opcodes.ASM9, mv, access, name, desc) {
// java -classpath /Users/tanguiwu/Downloads/asm-util-9.3.jar org.objectweb.asm.util.ASMifier /Users/tanguiwu/demo/apt-use-learing/base-arouter/build/tmp/kotlin-classes/debug/com/example/base_arouter/ARouterUtils.class
// java -classpath /Users/tanguiwu/Downloads/asm-all-5.1.jar org.objectweb.asm.util.ASMifier /Users/tanguiwu/demo/apt-use-learing/base-arouter/build/tmp/kotlin-classes/debug/com/example/base_arouter/ARouterUtils.class
override fun onMethodEnter() {
super.onMethodEnter()
println("tgw5 -------mClassName------${mClassName}")
mv.visitCode();
//这个有问题拿不到想要被注册的 路由类的集合
appLikeProxyClassList.forEach { proxyClassName ->
// val proxyClassName = "ArouterCustomActivityARouteInterfaceImp.class"
//遍历插入字节码,其实就是在 loadAppLike() 方法里插入类似registerAppLike("");的字节码
println("tgw6 开始注入代码:${proxyClassName}")
val fullName = proxyClassName.substring(
0,
proxyClassName.length - 6
)
println("tgw7 注入代码 full classname = ${fullName}")
mv.visitTypeInsn(NEW, fullName);
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, fullName, "<init>", "()V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(
GETFIELD,
PluginTools.REGISTER_CLASS_FILE_NAME.substring(
0,
PluginTools.REGISTER_CLASS_FILE_NAME.length - 6
),
"mRouterMap",
"Ljava/util/Map;"
);
mv.visitMethodInsn(INVOKEVIRTUAL, fullName, "register", "(Ljava/util/Map;)V", false);
}
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
// mv.visitCode();
// mv.visitTypeInsn(NEW, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp");
// mv.visitInsn(DUP);
// mv.visitMethodInsn(INVOKESPECIAL, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp", "<init>", "()V", false);
// mv.visitVarInsn(ALOAD, 0);
// mv.visitFieldInsn(GETFIELD, "com/example/base_arouter/ARouterUtils", "mRouterMap", "Ljava/util/Map;");
// mv.visitMethodInsn(INVOKEVIRTUAL, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp", "register", "(Ljava/util/Map;)V", false);
// mv.visitInsn(RETURN);
// mv.visitMaxs(2, 1);
// mv.visitEnd();
}
override fun onMethodExit(opcode: Int) {
super.onMethodExit(opcode)
println("-------onMethodExit------")
}
}
工具类:
package com.example.arouterplugin.gradle76
import groovy.io.FileType
import java.io.File
import java.io.FileNotFoundException
object PluginTools {
const val REGISTER_CLASS_FILE_NAME = "com/example/base_arouter/ARouterUtils.class"
//apt 生成的代码文件夹目录
const val PROXY_CLASS_PACKAGE_NAME = "com/example/myaptuselearing/routes/"
const val PROXY_CLASS_PREFIX ="com/example/myaptuselearing/routes"
const val PROXY_CLASS_SUFFIX = "RouteInterfaceImp.class"
/**
* 判断该class是否是我们的apt生成的目标类
*
* @param file
* @return
*/
fun isTargetProxyClass( filePath:String): Boolean {
if (filePath.startsWith(PROXY_CLASS_PREFIX) && filePath.endsWith(PROXY_CLASS_SUFFIX)) {
return true
}
return false
}
}
ok打完收工项目如下:代码