众所周知, Transform
接口在 AGP8.0 中已经被移除了, 而以往的大量工程实践都需求运用该接口结合 ASM 对字节码进行操作, 例如: 代码插桩, 代码替换等.
接下来将探索在 AGP8.0 中有哪些计划能够替代 Transform
到达相同的效果.
一. Instrumentation
Instrumentation 作为 Google 首推的 Transform
替代计划, 只需求完成 AsmClassVisitorFactory 接口即完成对字节码处理.
原 Transform
处理流程:
flowchart LR
input[Jar/classes]
transform1([Transform])
intermediate1[Jar/classes]
transform2([Transform])
output[Jar/classes]
input --> transform1
transform1 --> intermediate1
intermediate1 --> transform2
transform2 --> output
现 Instrumentation
处理流程:
flowchart LR
input[Jar/classes]
transform1([ClassVisitor])
transform2([ClassVisitor])
output[Jar/classes]
input --> transform1
transform1 --> transform2
transform2 --> output
相较于 Transform
处理流程, Instrumentation
流程免去对中间产品的读写, 而且一定是以 Jar/classes 文件为单位的增量处理, 使得全量编译和增量编译速度都有较大的提升.
插桩实践
下面以完成一个的 TraceAsmClassVisitorFactory
为例, 实践如何运用 Instrumentation
相关接口.
TraceAsmClassVisitorFactory
将在一切方法进口和出口插桩 Trace#beginSection
和 Trace#endSection
代码.
TraceClassVisitor
class TraceClassVisitor(
api: Int,
next: ClassVisitor
) : ClassVisitor(api, next) {
private var className: String? = null
// 开端处理 class 文件时, 记载当时 class 的姓名
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
className = name
super.visit(version, access, name, signature, superName, interfaces)
}
// 遍历 method 时, 运用 MethodVisitor 参加插桩逻辑
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
return TraceMethodVisitor(api, mv, access, name, descriptor)
}
private inner class TraceMethodVisitor(
api: Int,
methodVisitor: MethodVisitor?,
access: Int,
name: String?,
descriptor: String?
) : AdviceAdapter(
api,
methodVisitor,
access,
name,
descriptor
) {
// method 进口刺进 `Trace#beginSection` 代码
override fun onMethodEnter() {
mv.visitLdcInsn("$className#$name")
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"android/os/Trace",
"beginSection",
"(Ljava/lang/String;)V",
false
)
}
// method 出口刺进 `Trace#endSection` 代码
override fun onMethodExit(opcode: Int) {
mv.visitMethodInsn(
INVOKESTATIC,
"android/os/Trace",
"endSection",
"()V",
false
)
}
}
}
TraceAsmClassVisitorFactory
abstract class TraceAsmClassVisitorFactory: AsmClassVisitorFactory<InstrumentationParameters.None> {
// 对一切 class 文件都进行插桩处理
override fun isInstrumentable(classData: ClassData): Boolean {
return true
}
// 遍历 class 时, 运用 ClassVisitor 参加插桩逻辑
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
val apiVersion = instrumentationContext.apiVersion.get()
return TraceClassVisitor(apiVersion, nextClassVisitor)
}
}
TraceTransformPlugin
abstract class TraceTransformPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.plugins.withId("com.android.application") {
project.extensions.configure(ApplicationAndroidComponentsExtension::class.java) { extension ->
extension.onVariantsAsmClassVisitorFactory { variant ->
// 注册用于插桩的 AsmClassVisitorFactory
variant.instrumentation.transformClassesWith(
TraceAsmClassVisitorFactory::class.java,
InstrumentationScope.ALL
) {}
// 因为对 method 进行了插桩, 需求从头计算被插桩函数的栈帧
variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)
}
}
}
}
}
优下风
-
优势: 运用简略, 只需重视
ClassVisitor
逻辑, 无需处理 class 文件的读取与写入. - 下风: class 处理进程是固定的, 只能完成相对简略的代码插桩与替换, 无法满意个性化需求.
二. Transform Task
经过 Instrumentation
的确能很方便的完成插桩与替换逻辑, 但是理想很丰满,现实很骨感, 许多时候并不能满意需求, 例如:
-
先剖析再操作: 在完成 hook 框架时, 一般需求先对工程中的一切 class 进行一次遍历剖析后, 再对相关的 class 文件进行字节码操作.
-
输出文件产品: 上文中的
TraceClassVisitor
在调用Trace#beginSection
时传入了完整的类名与方法名, 这往往会导致生成的 trace 文件过大. 工程实践中一般会 生成methodId
与类名#方法名
的映射, 在Trace#beginSection
调用时只是传入methodId
, 这时就需求在插桩结束后保存当时的methodId
与类名#方法名
的映射文件.
当对 class 的处理进程有自定义需求时, 就需求经过 Transform Task
来完成. Transform Task
经过自己完成一个 Gradle Task
, 然后运用 ApplicationAndroidComponentsExtension
提供的接口将其注册到 class 的处理流程中.
class 处理流程:
flowchart LR
source[classes]
dependencies[jars]
intermediate_classes[classes]
intermediate_jars[jars]
output[jar]
asm_source([transformClassesWithAsm])
asm_dependencies([transformJartsWithAsm])
transform_task([Transform Task])
source --> asm_source
asm_source --> intermediate_classes
dependencies --> asm_dependencies
asm_dependencies --> intermediate_jars
intermediate_classes --> transform_task
intermediate_jars --> transform_task
transform_task --> output
Transform Task
会在 Instrumentation
履行完成后再履行, 而且输出的产品是一个包含一切 class 的 jar 文件.
插桩实践
下面以完成一个的 TraceTransformTask
为例, 调整上文的中的 TraceClassVisitor
插桩逻辑, 在调用 Trace#beginSection
时传入 methodId
参数, 并在插桩完成后输出 methodId
与 类名#方法名
的映射文件.
TraceClassVisitor
class TraceClassVisitor(
api: Int,
next: ClassVisitor,
private val mapping: (String) -> Long,
) : ClassVisitor(api, next) {
private var className: String? = null
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
className = name
super.visit(version, access, name, signature, superName, interfaces)
}
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
return TraceMethodVisitor(api, mv, access, name, descriptor)
}
private inner class TraceMethodVisitor(
api: Int,
methodVisitor: MethodVisitor?,
access: Int,
name: String?,
descriptor: String?
) : AdviceAdapter(
api,
methodVisitor,
access,
name,
descriptor
) {
override fun onMethodEnter() {
// 运用 methodId 替代 className#methodName
val methodId = mapping("$className#$name")
mv.visitLdcInsn("$methodId")
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"android/os/Trace",
"beginSection",
"(Ljava/lang/String;)V",
false
)
}
override fun onMethodExit(opcode: Int) {
mv.visitMethodInsn(
INVOKESTATIC,
"android/os/Trace",
"endSection",
"()V",
false
)
}
}
}
TraceTransformTask
abstract class TransformTask : DefaultTask() {
// 一切输入的 jar 文件
@get:InputFiles
abstract val allJars: ListProperty<RegularFile>
// 一切输入的 classes 目录
@get:InputFiles
abstract val allDirectories: ListProperty<Directory>
// 输出的 jar 文件
@get:OutputFile
abstract val outputJar: RegularFileProperty
// 输出的 mapping 文件
@get:OutputFile
abstract val mappingFile: RegularFileProperty
@TaskAction
fun transform() {
var currentMethodId = 0L
val mapping = mutableMapOf<String, Long>()
val factory: (ClassWriter) -> ClassVisitor = { next ->
TraceClassVisitor(Opcodes.ASM9, next) { method ->
mapping.getOrPut(method) { currentMethodId++ }
}
}
JarOutputStream(
outputJar.get().asFile
.outputStream()
.buffered()
).use { output ->
// 遍历一切的 jar 文件输入, 并进行插桩
allJars.get().forEach { jar ->
transformJar(output, jar.asFile, factory)
}
// 遍历一切的 classes 目录输入, 并进行插桩
allDirectories.get().forEach { dir ->
transformClasses(output, dir.asFile, factory)
}
}
// 插桩后, 输出 mapping 信息
mappingFile.get().asFile
.writeText(mapping.toString())
}
private fun transformJar(
output: JarOutputStream,
jar: File,
factory: (ClassWriter) -> ClassVisitor
) {
JarInputStream(
jar.inputStream()
.buffered()
).use { input ->
while (true) {
val entry = input.nextEntry ?: break
if (entry.isDirectory) continue
if (!entry.name.endsWith(".class")) continue
transform(entry.name, input, output, factory)
}
}
}
private fun transformClasses(
output: JarOutputStream,
rootDir: File,
factory: (ClassWriter) -> ClassVisitor
) {
rootDir.walk().forEach { child ->
if (child.isDirectory) return@forEach
if (!child.name.endsWith(".class")) return@forEach
val name = child.toRelativeString(rootDir)
child.inputStream()
.buffered()
.use { input ->
transform(name, input, output, factory)
}
}
}
// 创建 ClassReader, ClassVisitor, ClassWriter 进行插桩
private fun transform(
name: String,
input: InputStream,
output: JarOutputStream,
factory: (ClassWriter) -> ClassVisitor
) {
val entry = ZipEntry(name)
output.putNextEntry(entry)
val cr = ClassReader(input)
val cw = ClassWriter(ClassWriter.COMPUTE_MAXS)
cr.accept(factory(cw), ClassReader.EXPAND_FRAMES)
output.write(cw.toByteArray())
output.closeEntry()
}
}
TraceTransformPlugin
abstract class TraceTransformPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.plugins.withId("com.android.application") {
project.extensions.configure(ApplicationAndroidComponentsExtension::class.java) { extension ->
extension.onVariants {
// 装备 mapping 文件输出路径
val transformTask = project.tasks.register(
"transform${it.name.capitalized()}",
TransformTask::class.java
) { task ->
task.mappingFile.set(project.buildDir.resolve("outputs/transform/mapping.txt"))
}
// 将 TransformTask 注册到 class 处理流程中
it.artifacts
.forScope(ScopedArtifacts.Scope.ALL)
.use(transformTask)
.toTransform(
ScopedArtifact.CLASSES,
TransformTask::allJars,
TransformTask::allDirectories,
TransformTask::outputJar
)
}
}
}
}
}
优下风
-
优势:
- 处理 class 文件的方法更加灵活, 能够按需完成对应的处理流程, 例如能够先对全工程进行剖析再进行字节码操作.
- 能够与其它的
Gradle Task
合作, 运用其它Gradle Task
的输出作为输入或将其输出作为其它Gradle Task
的输入.
-
下风:
- 相较于
Instrumentation
需求处理许多额定的逻辑, 例如对 class 文件的读写. - 完成一个高性能的
Task
需求付出许多额定的尽力, 例如完成 Incremental tasks 等.
- 相较于
参考资料
- Transform API is removed