Android编译插桩ASM技术探究(一)
1,概念介绍
先搞明白什么是编译插桩。我们写的Java/Kotlin代码,要经过一系列加工才能变成机器上能跑的APK,apk文件编译过程如下,
Java代码 → javac编译 → Class文件 → 打包成Dex → 生成APK
编译插桩就像在"Class文件"和"Dex"之间加了个质检员,它会:
- 拦下所有Class文件
- 按你的要求修改(比如加日志、加统计)
- 再覆盖重新生成Dex
插桩技术优点:
- 不用改源码,业务代码干干净净
- 统一处理,避免漏改、错改
- 一次开发,全项目生效
2,ASM
要修改Class文件,就得懂字节码。但字节码这东西,人类看了脑壳疼(不信你看下面这段):
public void test() {0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: return
}
这时候ASM就登场了。它是一个操作字节码的框架,能帮你:
- 读懂Class文件(不用自己解析字节码)
- 修改Class文件(不用记那些鬼画符一样的指令)
- 生成新的Class文件(徒手撸字节码什么的,不存在的)
3,ASM开发步骤
3.1 搭建插桩环境
插桩通常通过Gradle插件实现,步骤如下:
- 创建一个Android Library模块(比如叫asm-plugin)
- 在build.gradle里引入必要依赖:
//gradle sdkimplementation gradleApi()//groovy sdkimplementation localGroovy() implementation 'org.ow2.asm:asm:5.0.3'implementation 'org.ow2.asm:asm-commons:5.0.3'
3.2 自定义Transform
Transform是Android Gradle提供的用于处理Class文件的接口,我们的插桩逻辑就放在这里。
先定义一个Transform:
public class ASMTransform extends Transform {// 给Transform起个名字@Overridepublic String getName() {return "ASMTransform";}// 告诉Gradle我们要处理哪些类型的文件@Overridepublic Set<QualifiedContent.ContentType> getInputTypes() {return Collections.singleton(QualifiedContent.DefaultContentType.CLASSES);}// 告诉Gradle我们要处理哪些范围的文件@Overridepublic Set<QualifiedContent.Scope> getScopes() {return Sets.immutableEnumSet(QualifiedContent.Scope.PROJECT, QualifiedContent.Scope.EXTERNAL_LIBRARIES);}// 是否支持增量编译(提升编译速度)@Overridepublic boolean isIncremental() {return true;}// 核心方法:处理Class文件@Overridepublic void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {// 1. 遍历所有输入的Class文件invocation.getInputs().forEach(input -> {// 处理项目自身的Classinput.getDirectoryInputs().forEach(dirInput -> {processDir(dirInput.getFile());// 把处理后的文件输出到下一个流程File dest = invocation.getOutputProvider().getContentLocation(dirInput.getName(), dirInput.getContentTypes(), dirInput.getScopes(), Format.DIRECTORY);FileUtils.copyDirectory(dirInput.getFile(), dest);});// 处理第三方库的Class(Jar包)input.getJarInputs().forEach(jarInput -> {processJar(jarInput.getFile());// 输出处理后的JarFile dest = invocation.getOutputProvider().getContentLocation(jarInput.getName(), jarInput.getContentTypes(),jarInput.getScopes(), Format.JAR);FileUtils.copyFile(jarInput.getFile(), dest);});});}// 处理目录中的Class文件private void processDir(File dir) {if (dir.isDirectory()) {for (File file : dir.listFiles()) {if (file.isDirectory()) {processDir(file);} else if (file.getName().endsWith(".class")) {// 用ASM处理单个Class文件modifyClass(file);}}}}// 处理Jar包中的Class文件(略)private void processJar(File jarFile) { ... }
}
3.3 给所有方法加耗时统计
现在到了最关键的部分:用ASM修改Class文件。我们的目标是——给所有方法前后加上耗时统计,就像这样:
// 原方法
public void login(String username) {// 登录逻辑
}// 插桩后
public void login(String username) {long start = System.currentTimeMillis();// 登录逻辑long end = System.currentTimeMillis();Log.d("耗时", "login: " + (end - start) + "ms");
}
实现这个需求的ASM代码:
private void modifyClass(File classFile) {try {// 1. 读取原Class文件ClassReader cr = new ClassReader(Files.readAllBytes(classFile.toPath()));// 2. 准备写入修改后的Class文件ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);// 3. 自定义ClassVisitor处理类ClassVisitor cv = new ClassVisitor(Opcodes.ASM9, cw) {// 当访问到方法时回调@Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {// 获取原始方法的MethodVisitorMethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);// 过滤掉构造方法和静态代码块if (name.equals("<init>") || name.equals("<clinit>")) {return mv;}// 返回自定义的MethodVisitor,用于修改方法return new MethodVisitor(Opcodes.ASM9, mv) {// 方法开始时调用(Code指令前)@Overridepublic void visitCode() {// 插入: long start = System.currentTimeMillis();mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);mv.visitVarInsn(Opcodes.LSTORE, 1); // 存储到局部变量1super.visitCode(); // 执行原方法的Code指令}// 方法结束时调用(return指令前)@Overridepublic void visitInsn(int opcode) {// 只在返回指令前插入代码if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {// 插入: long end = System.currentTimeMillis();mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);mv.visitVarInsn(Opcodes.LSTORE, 3); // 存储到局部变量3// 插入: Log.d("耗时", "方法名: " + (end - start) + "ms");mv.visitLdcInsn("耗时"); // 日志标签// 拼接字符串:"方法名: " + (end - start) + "ms"mv.visitLdcInsn(name + ": ");mv.visitVarInsn(Opcodes.LLOAD, 3); // 加载endmv.visitVarInsn(Opcodes.LLOAD, 1); // 加载startmv.visitInsn(Opcodes.LSUB); // end - startmv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Long", "toString", "(J)Ljava/lang/String;", false);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "concat", "(Ljava/lang/String;)Ljava/lang/String;", false);mv.visitLdcInsn("ms");mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "concat", "(Ljava/lang/String;)Ljava/lang/String;", false);// 调用Log.dmv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);mv.visitInsn(Opcodes.POP); // 消费返回值}super.visitInsn(opcode); // 执行原返回指令}};}};// 开始处理Class文件cr.accept(cv, ClassReader.EXPAND_FRAMES);// 4. 把修改后的字节码写回文件Files.write(classFile.toPath(), cw.toByteArray());} catch (Exception e) {e.printStackTrace();}
}
3.4 注册插件,让插桩生效
最后一步,把我们的Transform注册到Gradle插件中:
public class ASMPlugin implements Plugin<Project> {@Overridepublic void apply(Project project) {// 获取Android扩展AppExtension android = project.getExtensions().getByType(AppExtension.class);// 注册我们的Transformandroid.registerTransform(new ASMTransform());}
}
在resources/META-INF/gradle-plugins/asmplugin.properties中声明插件:
implementation-class=com.example.ASMPlugin
然后在app模块的build.gradle中应用插件:
plugins {id 'com.android.application'id 'asmplugin' // 应用我们的插件
}
这样,每次编译时,ASM就会自动给所有方法加上耗时统计了!