TransformAPI + ASM实现自动插桩

1,390 阅读6分钟

Android Build 插桩原理:Transform 与 ASM


一、Transform 概述

Transform是 Android Gradle Plugin 提供的一套开放 API,允许开发者在class → dex流程之间,对字节码做自定义修改——典型场景就是字节码插桩(如自动埋点、性能检测、热修复)。

  • Transform作用阶段
    transform流程
    图中resource是指Java资源(如properties/xml),不是Android的res目录。

  • 资源/jar/class流经Transform过程
    transform中间数据流
    配置文件和代码一起打包进jar。

  • Transform的日志表现:

    :app:transformClassesWithTestForDebug
    
    • transform固定,Test是当前Transform的名字,ForDebug表明构建类型。

二、Transform执行流程 & 代码示例

1. 流程细节

  • Transform会按顺序处理所有输入(class/jar)
  • 支持增量构建(isIncremental),也支持每次全量执行
  • 可以单独处理本地源码、三方库、依赖

Transform处理流程:

@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    TransformOutputProvider outputProvider = transformInvocation.outputProvider
    outputProvider.deleteAll() // 清理旧产物
    def inputs = transformInvocation.inputs
    inputs.each {
        // jarInputs:三方依赖与aar/jar产物
        it.jarInputs.each {
            println (it.status) // 新增/变更/删除
        }
        // directoryInputs:本地class
        it.directoryInputs.each {
            it.changedFiles.entrySet().each {
                println(it.key.name + it.value.name()) // 类名+变更类型
            }
        }
    }
}
  • isIncremental=false 时,每次全量处理

目录关系示意:
目录关系


2. Transform自定义代码核心(精简版)

class ASMTransform extends Transform {
    @Override public String getName() { return "asm"; }
    @Override public Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS; }
    @Override public Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT; }
    @Override public boolean isIncremental() { return false; }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        outputProvider.deleteAll(); // 非增量构建全量清理
        for (TransformInput input : transformInvocation.getInputs()) {
            // 1. 处理源码目录
            for (DirectoryInput dirInput : input.getDirectoryInputs()) {
                File src = dirInput.getFile();
                File dest = outputProvider.getContentLocation(
                    dirInput.getName(), dirInput.getContentTypes(), dirInput.getScopes(), Format.DIRECTORY
                );
                processInject(src, dest);
            }
            // 2. 处理依赖jar
            for (JarInput jarInput : input.getJarInputs()) {
                File src = jarInput.getFile();
                File dest = outputProvider.getContentLocation(
                    jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR
                );
                FileUtils.copyFile(src, dest);
            }
        }
    }

    // ASM插桩逻辑封装到此方法
    private void processInject(File src, File dest) throws IOException {
        // 递归src下所有class文件
        for (File file : FileUtils.getAllFiles(src)) {
            FileInputStream fis = new FileInputStream(file);
            ClassReader cr = new ClassReader(fis);
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            cr.accept(new ClassInjectTimeVisitor(cw, file.getName()), ClassReader.EXPAND_FRAMES);
            byte[] newClassBytes = cw.toByteArray();
            // 写入目标目录
            String fullClassPath = file.getAbsolutePath().replace(src.getAbsolutePath(), "");
            File outFile = new File(dest, fullClassPath);
            FileUtils.mkdirs(outFile.getParentFile());
            FileOutputStream fos = new FileOutputStream(outFile);
            fos.write(newClassBytes);
            fos.close();
        }
    }
}

三、ASM 字节码插桩

1. 基本原理

ASM 是什么?

  • Java字节码级操作框架,支持直接生成、修改class文件,不依赖源代码

流程图:
ASM插桩流程

关键类说明:

  • ClassReader:读取class字节码
  • ClassVisitor:访问class结构(可递归到方法/字段等)
  • MethodVisitor:访问/修改方法体
  • ClassWriter:输出字节码

应用场景:
如在所有@ASMTest标记的方法自动插入代码,统计方法耗时。

2. 例子:自动埋点

原始代码

@ASMTest
public static void main(String[] var0) throws InterruptedException {
    Thread.sleep(1000L);
}

插桩后

@ASMTest
public static void main(String[] var0) throws InterruptedException {
    long var1 = System.currentTimeMillis();
    Thread.sleep(1000L);
    long var3 = System.currentTimeMillis();
    System.out.println("execute:" + (var3 - var1));
}

3. 方法签名知识

字节码指令表
签名表

  • INVOKESTATIC java/lang/System.currentTimeMillis ()J

  • 字节码调用例子:

    invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
    

四、Transform + ASM 插桩全流程

  1. 编写ASM ClassVisitor/MethodVisitor实现插桩

  2. 自定义Transform:遍历所有class/jar,对class文件做ASM插桩,jar直接copy

  3. 将Transform注册为Gradle插件

    public class ASMPlugin implements Plugin<Project> {
        @Override
        public void apply(Project project) {
            BaseExtension android = project.getExtensions().getByType(BaseExtension.class);
            android.registerTransform(new ASMTransform());
        }
    }
    
  4. Gradle build时自动执行transform任务,完成插桩和构建


五、实用注意点 & 前瞻性建议

  • 插桩Transform建议只处理自己代码PROJECT_ONLY),三方库高风险
  • 非增量构建适合初学/调试,生产应做增量优化
  • 大型项目需关注build性能与class/jar混合情况
  • Android官方新构建体系(如AGP 8.x)对Transform兼容性已不再保证,未来建议关注新API/替代方案(如AGP Instrumentation API)

六、参考资料

测试源码:github.com/xingchaozha…

学后检测

一、单选题

  1. Transform的主要作用是什么?

    A. 运行时动态修改APK内容
    B. 在class到dex转换前,对字节码或资源做自定义处理
    C. 管理Android资源ID分配
    D. 控制APK签名和加固流程

    答案:B

    解析:
    Transform本质就是插在class→dex转换前的编译期钩子,用于修改class、jar或Java资源。


  1. 在Transform的transform()实现中,哪一项最能区分本地代码和三方依赖?

    A. input.jarInputs
    B. input.directoryInputs
    C. outputProvider
    D. transformInvocation.context

    答案:B

    解析:
    directoryInputs专指本地(本module)编译产物class文件,jarInputs指三方依赖的jar/aar等。


  1. ASM插桩时,如果你想“进入每个方法体的最前面插入一行代码”,你应该扩展哪个类?

    A. ClassLoader
    B. MethodVisitor
    C. FieldVisitor
    D. FileVisitor

    答案:B

    解析:
    MethodVisitor可hook每个方法体,实现插入/修改/删除指令。


  1. Transform机制下,增量构建的优点主要是?

    A. 插桩逻辑更加准确
    B. 兼容所有第三方库
    C. 明显提升大型项目的编译速度
    D. 编译出的APK体积更小

    答案:C

    解析:
    增量构建只处理变动class,大幅提升大型工程的build速度。


二、多选题

  1. 关于自定义Transform的功能,下列说法哪些正确?

    A. 可以处理Java资源文件(如properties、配置xml)
    B. 可以全量处理所有class,也可以只处理增量变动class
    C. 可以实现自定义代码插桩、埋点、热修复等
    D. 只能在debug包启用,release包不能用

    答案:A、B、C

    解析:
    Transform对资源/class/jar都有处理能力;支持全量和增量构建;用途极广。release包也可用。


  1. 使用ASM做方法耗时统计插桩,可能遇到哪些实际问题?

    A. 对Kotlin和Java编译产物都可插桩
    B. 方法体极短/内联优化后,插桩效果可能消失
    C. 插桩过多可能影响性能和包体积
    D. 插桩对混淆、签名无影响

    答案:A、B、C

    解析:
    D错误,插桩后class会影响混淆和签名,混淆表和调试要特别注意。


三、判断题

  1. Transform机制只支持修改自己代码,不能操作三方依赖的jar包。 ( )

    答案:错

    解析:
    通过jarInputs可以处理三方依赖(但实际很少直接这样用,容易踩坑)。


  1. ASM插桩只会影响方法本身的字节码,不影响方法签名。 ( )

    答案:对

    解析:
    ASM通常只改方法体指令,不改方法声明/签名。


  1. Transform和ASM是Android官方文档推荐的“长期插桩解决方案”,未来不会废弃。 ( )

    答案:错

    解析:
    AGP 7.0+已不推荐Transform,后续更倾向新的Instrumentation API。


四、简答题

  1. 简述Transform的核心运行流程,以及在编译期字节码插桩中的角色。

    参考答案:
    Transform插入在class→dex阶段,可拿到所有class/jar资源,允许自定义修改/插桩。开发者实现Transform类,覆盖transform()方法,遍历输入的class文件,通过ASM等工具修改字节码,写回输出目录。最终这些插桩后的class会被后续流程打包进APK。


  1. 请列举ASM插桩在实际项目中的典型应用场景,并简要说明潜在风险。

    参考答案:

    • 场景:自动埋点、性能监控、方法耗时统计、热修复、自动日志注入等。
    • 风险:与混淆/签名冲突,兼容性差异,插桩过度可能导致包体积和性能异常,Android新版构建体系可能不再兼容。

五、编程题

请写出一个ASM插桩的伪代码流程:对所有包含@ASMTest注解的方法,在方法体开头插入一行打印日志的字节码。

public class MyMethodVisitor extends MethodVisitor {
    private boolean needInject = false;

    public MyMethodVisitor(int api, MethodVisitor mv, boolean needInject) {
        super(api, mv);
        this.needInject = needInject;
    }

    @Override
    public void visitCode() {
        super.visitCode();
        if (needInject) {
            // 插入打印指令
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("ASMTest method start!");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
    }
}

解析:

  • 需结合ClassVisitor遍历方法,检测@ASMTest注解,构造MethodVisitor传递needInject标志。
  • visitCode方法在方法体开始时调用,插入打印指令即可。