前言
在Gradle Plugin开发基础流程中介绍了如何创建我们自己的插件工程,这次来介绍下如何使用Transform配合ASM进行插桩。使用 AGP Transform API 进行字节码插桩已经非常普遍了,例如 Booster、神策等框架中都有 Transform 的影子。Transform 听起来很高大上,其本质就是一个 Gradle Task
ps:Transform API在AGP 8.0以上已经被删除!!!
Transform 的基本原理
先大概了解下 Transform 的工作机制:
1、工作时机: Transform 工作在 Android 构建中由 Class → Dex 的节点;
2、处理对象: 处理对象包括 Javac 编译后的 Class 文件、Java 标准 resource 资源、本地依赖和远程依赖的 JAR/AAR。Android 资源文件不属于 Transform 的操作范围,因为它们不是字节码;
3、Transform Task: 每个 Transform 都对应一个 Task,Transform 的输入和输出可以理解为对应 Transform Task 的输入输出。每个 TransformTask 的输出都分别存储在 app/build/intermediates/transform/[Transform Name]/[Variant] 文件夹中;
4、Transform 链: TaskManager 会将每个 TransformTask 串联起来,前一个 Transform 的输出会作为下一个 Transform 的输入。
Transform 增量模式
1、增量模式标记位: Transform API 有两个增量标志位,不要混淆:
Transform#isIncremental(): Transform 增量构建的开关,返回 true 才有可能触发增量构建;
TransformInvocation#isIncremental(): 当次 TransformTask 是否增量执行,返回 true 表示正在增量模式。
2、Task 增量模式与 Transform 增量模式的区别: Task 增量模式与 Transform 增量模式的区别在于,Task 增量执行时会跳过整个 Task 的动作列表,而 Transform 增量执行依然会执行 TransformTask,但输入内容会增加变更内容信息。
3、增量模式的输入: 增量模式下的所有输入都是带状态的,需要根据这些状态来做不同的处理,不需要每次所有流程都重新来一遍。比如新增的输入就需要处理,而未修改的输入就不需要处理。Transform 定义了四个输入文件状态:
public enum Status {
// 未修改,不需要处理,也不需要复制操作
NOTCHANGED,
// 新增,正常处理并复制给下一个任务
ADDED,
// 已修改,正常处理并复制给下一个任务
CHANGED,
// 已删除,需同步移除 OutputProvider 指定的目标文件
REMOVED;
}
自定义 Transform 模板
我们把整个流程图做成一个抽象模板类,子类需要重写 provideFunction() 方法,从输入流读取 Class 文件,修改完字节码后再写入到输出流。甚至不需要考虑 Trasform 的输入文件遍历、加解压、增量等,舒服!
abstract class BaseCustomTransform(private val debug: Boolean) : Transform() {
abstract fun provideFunction(): ((InputStream, OutputStream) -> Unit)?
open fun classFilter(className: String) = className.endsWith(SdkConstants.DOT_CLASS)
override fun isIncremental() = true
override fun transform(transformInvocation: TransformInvocation) {
super.transform(transformInvocation)
log("Transform start, isIncremental = ${transformInvocation.isIncremental}.")
val inputProvider = transformInvocation.inputs
val referenceProvider = transformInvocation.referencedInputs
val outputProvider = transformInvocation.outputProvider
// 1. Transform logic implemented by subclasses.
val function = provideFunction()
// 2. Delete all transform tmp files when not in incremental build.
if (!transformInvocation.isIncremental) {
log("All File deleted.")
outputProvider.deleteAll()
}
for (input in inputProvider) {
// 3. Transform jar input.
log("Transform jarInputs start.")
for (jarInput in input.jarInputs) {
val inputJar = jarInput.file
val outputJar = outputProvider.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR
)
if (transformInvocation.isIncremental) {
// 3.1 Transform jar input in incremental build.
when (jarInput.status ?: Status.NOTCHANGED) {
Status.NOTCHANGED -> {
// Do nothing.
}
Status.ADDED, Status.CHANGED -> {
// Do transform.
transformJar(inputJar, outputJar, function)
}
Status.REMOVED -> {
// Delete.
FileUtils.delete(outputJar)
}
}
} else {
// 3.2 Transform jar input in full build.
transformJar(inputJar, outputJar, function)
}
}
// 4. Transform dir input.
log("Transform dirInput start.")
for (dirInput in input.directoryInputs) {
val inputDir = dirInput.file
val outputDir = outputProvider.getContentLocation(
dirInput.name,
dirInput.contentTypes,
dirInput.scopes,
Format.DIRECTORY
)
if (transformInvocation.isIncremental) {
// 4.1 Transform dir input in incremental build.
for ((inputFile, status) in dirInput.changedFiles) {
val outputFile = concatOutputFilePath(inputDir, outputDir, inputFile)
when (status ?: Status.NOTCHANGED) {
Status.NOTCHANGED -> {
// Do nothing.
}
Status.ADDED, Status.CHANGED -> {
// Do transform.
if (inputFile.isFile) {
doTransformFile(inputFile, outputFile, function)
} else if (inputFile.isDirectory) {
log("${inputFile.absolutePath} is dir!!!")
}
}
Status.REMOVED -> {
// Delete
FileUtils.delete(outputFile)
}
}
}
} else {
// 4.2 Transform dir input in full build.
// Traversal fileTree (depthFirstPreOrder).
for (inputFile in FileUtils.getAllFiles(inputDir)) {
val outputFile = concatOutputFilePath(inputDir, outputDir, inputFile)
if (classFilter(inputFile.name)) {
doTransformFile(inputFile, outputFile, function)
} else {
// Copy.
Files.createParentDirs(outputFile)
FileUtils.copyFile(inputFile, outputFile)
}
}
}
}
}
log("Transform end.")
}
/**
* Do transform Jar.
*/
private fun transformJar(
inputJar: File,
outputJar: File,
function: ((InputStream, OutputStream) -> Unit)?
) {
// Create parent directories to hold outputJar file.
Files.createParentDirs(outputJar)
// Unzip.
FileInputStream(inputJar).use { fis ->
ZipInputStream(fis).use { zis ->
// Zip.
FileOutputStream(outputJar).use { fos ->
ZipOutputStream(fos).use { zos ->
var entry = zis.nextEntry
while (entry != null && isValidZipEntryName(entry)) {
if (!entry.isDirectory) {
zos.putNextEntry(ZipEntry(entry.name))
if (classFilter(entry.name)) {
// Apply transform function.
applyFunction(zis, zos, function)
} else {
// Copy.
zis.copyTo(zos)
}
}
entry = zis.nextEntry
}
}
}
}
}
}
/**
* Do transform file.
*/
private fun doTransformFile(
inputFile: File,
outputFile: File,
function: ((InputStream, OutputStream) -> Unit)?
) {
// Create parent directories to hold outputFile file.
Files.createParentDirs(outputFile)
FileInputStream(inputFile).use { fis ->
FileOutputStream(outputFile).use { fos ->
// Apply transform function.
applyFunction(fis, fos, function)
}
}
}
private fun concatOutputFilePath(baseDir: File, outputDir: File, inputFile: File): File {
val relativePath = baseDir.toURI().relativize(inputFile.parentFile.toURI()).path
val realOutputFile = File(outputDir, relativePath)
return File(realOutputFile, inputFile.name)
}
private fun applyFunction(
input: InputStream,
output: OutputStream,
function: ((InputStream, OutputStream) -> Unit)?
) {
try {
if (null != function) {
function.invoke(input, output)
} else {
// Copy
input.copyTo(output)
}
} catch (e: UncheckedIOException) {
throw e.cause!!
}
}
private fun log(logStr: String) {
if (debug) {
println("$name - $logStr")
}
}
}
示例
往所有Activity的onCreate插入一个Toast调用
class InsertToastTransform(private val project: Project) : BaseCustomTransform(true) {
override fun getName() = "InsertToastTransform"
override fun getInputTypes(): Set<QualifiedContent.ContentType> =
setOf(QualifiedContent.DefaultContentType.CLASSES)
override fun getScopes(): MutableSet<in QualifiedContent.Scope> =
EnumSet.of(QualifiedContent.Scope.PROJECT)
override fun provideFunction(): ((InputStream, OutputStream) -> Unit)? {
return { inputStream, outputStream ->
val reader = ClassReader(inputStream)
val writer = ClassWriter(reader, 0)
val cv = ToastClassVisitor(writer)
reader.accept(cv, 0)
val bytes = writer.toByteArray()
outputStream.write(bytes)
}
}
}
class ToastClassVisitor(cv: ClassVisitor) : ClassVisitor(Opcodes.ASM7, cv) {
private var isActivity = false
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)
isActivity = superName == "androidx/appcompat/app/AppCompatActivity"
}
override fun visitMethod(
access: Int,
name: String,
desc: String,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
var mv = cv.visitMethod(access, name, desc, signature, exceptions)
if (isActivity && name == "onCreate" && desc == "(Landroid/os/Bundle;)V") {
println("visitMethod ==> ${name} // ${desc}")
mv = AddToastAdviceAdapter(mv, access, name, desc)
}
return mv
}
}
class AddToastAdviceAdapter(mv: MethodVisitor, access: Int, name: String?, desc: String?) :
MethodVisitor(Opcodes.ASM7, mv) {
override fun visitCode() {
super.visitCode()
mv.visitFieldInsn(
Opcodes.GETSTATIC,
"com/demo/threadmonitornew/utils/ToastUtils",
"INSTANCE",
"Lcom/demo/threadmonitornew/utils/ToastUtils;"
)
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"com/demo/threadmonitornew/utils/ToastUtils",
"showToast",
"()V",
false
)
}
}
插件使用
class ThreadPluginPlugin : Plugin<Project> {
override fun apply(project: Project) {
val androidExtension = project.extensions.getByType(BaseExtension::class.java)
androidExtension.registerTransform(InsertToastTransform(project))
}
}
补充
可以安装一个ASM Bytecode Viewer Support Kotlin插件,在对应的源码上右键ASM Bytecode Viewer,即可直接查看asm代码,并且可以直接对比上一次的结果。
另外推荐参考如下仓库:https://github.sheincorp.cn/CB2Git/asm_hook