ASM 是一个功能比较齐全的 Java 字节码操作与分析框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接 产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类的行为。
更多细节可以去 [ASM 官网]( )
看看。
笔者写 Demo 的时候最新的版本是 7.0。
ASM 提供一种基于 Visitor 的 API,通过接口的方式,分离读 class 和写 class 的逻辑,提供一个 ClassReader 负责读取class字节码,然后传递给 Class Visitor 接口,Class Visitor 接口提供了很多 visitor 方法,比如 visit class,visit method 等,这个过程就像 ClassReader 带着 ClassVisitor 游览了 class 字节码的每一个指令。
光有读还不够,如果我们要修改字节码,ClassWriter 就出场了。ClassWriter 其实也是继承自 ClassVisitor 的,所做的就是保存字节码信息并最终可以导出,那么如果我们可以代理 ClassWriter 的接口,就可以干预最终生成的字节码了。
好,还是废话少说,直接上代码。
先看一下插件目录的结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4L5xW0Mo-1637198949211)(https://user-gold-cdn.xitu.io/2019/4/27/16a5d1ecb4143b27?imageView2/0/w/1280/h/960/ignore-error/1)]
这里新建了 AsmTransform 插件,以及 class visitor 的 adapter(TestMethodClassAdapter),使得在 visit method 的时候可以调用自定义的 TestMethodVisitor。
同时,buildSrc 的 build.gradle 中也要引入 ASM 依赖
// ASM 相关
implementation ‘org.ow2.asm:asm:7.1’
implementation ‘org.ow2.asm:asm-util:7.1’
implementation ‘org.ow2.asm:asm-commons:7.1’
下面先来看一下 AsmTransform
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import me.sure.asm.TestMethodClassAdapter
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
class AsmTransform extends Transform {
Project project
AsmTransform(Project project) {
this.project = project
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println("===== ASM Transform =====")
println("
t
r
a
n
s
f
o
r
m
I
n
v
o
c
a
t
i
o
n
.
i
n
p
u
t
s
"
)
p
r
i
n
t
l
n
(
"
{transformInvocation.inputs}") println("
transformInvocation.inputs")println("{transformInvocation.referencedInputs}")
println("
t
r
a
n
s
f
o
r
m
I
n
v
o
c
a
t
i
o
n
.
o
u
t
p
u
t
P
r
o
v
i
d
e
r
"
)
p
r
i
n
t
l
n
(
"
{transformInvocation.outputProvider}") println("
transformInvocation.outputProvider")println("{transformInvocation.incremental}")
//当前是否是增量编译
boolean isIncremental = transformInvocation.isIncremental()
//消费型输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
Collection inputs = transformInvocation.getInputs()
//引用型输入,无需输出。
Collection referencedInputs = transformInvocation.getReferencedInputs()
//OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
for (TransformInput input : inputs) {
for (JarInput jarInput : input.getJarInputs()) {
File dest = outputProvider.getContentLocation(
jarInput.getFile().getAbsolutePath(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR)
//将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
transformJar(jarInput.getFile(), dest)
}
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
println("== DI = " + directoryInput.file.listFiles().toArrayString())
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY)
//将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
//FileUtils.copyDirectory(directoryInput.getFile(), dest)
transformDir(directoryInput.getFile(), dest)
}
}
}
@Override
String getName() {
return AsmTransform.simpleName
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return true
}
private static void transformJar(File input, File dest) {
println("=== transformJar ===")
FileUtils.copyFile(input, dest)
}
private static void transformDir(File input, File dest) {
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
FileUtils.forceMkdir(dest)
String srcDirPath = input.getAbsolutePath()
String destDirPath = dest.getAbsolutePath()
println("=== transform dir = " + srcDirPath + ", " + destDirPath)
for (File file : input.listFiles()) {
String destFilePath = file.absolutePath.replace(srcDirPath, destDirPath)
File destFile = new File(destFilePath)
if (file.isDirectory()) {
transformDir(file, destFile)
} else if (file.isFile()) {
FileUtils.touch(destFile)
transformSingleFile(file, destFile)
}
}
}
private static void transformSingleFile(File input, File dest) {
println("=== transformSingleFile ===")
weave(input.getAbsolutePath(), dest.getAbsolutePath())
}
private static void weave(String inputPath, String outputPath) {
try {
FileInputStream is = new FileInputStream(inputPath)
ClassReader cr = new ClassReader(is)
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES)
TestMethodClassAdapter adapter = new TestMethodClassAdapter(cw)
cr.accept(adapter, 0)
FileOutputStream fos = new FileOutputStream(outputPath)
fos.write(cw.toByteArray())
fos.close()
} catch (IOException e) {
e.printStackTrace()
}
}
}
我们的 InputTypes 是 CONTENT_CLASS, 表明是 class 文件,Scope 先无脑选择 SCOPE_FULL_PROJECT 在 transform 方法中主要做的事情就是把 Inputs 保存到 outProvider 提供的位置去。生成的位置见下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jZfkl8mu-1637198949228)(https://user-gold-cdn.xitu.io/2019/4/27/16a5d2602bb94540?imageView2/0/w/1280/h/960/ignore-error/1)]
对照代码,主要有两个 transform 方法,一个 transformJar 就是简单的拷贝,另一个 transformSingleFile,我们就是在这里用 ASM 对字节码进行修改的。 关注一下 weave 方法,可以看到我们借助 ClassReader 从 inputPath 中读取输入流,在 ClassWriter 之前用一个 adapter 进行了封装,接下来就让我们看看 adapter 做了什么。
public class TestMethodClassAdapter extends ClassVisitor implements Opcodes {
public TestMethodClassAdapter(ClassVisitor classVisitor) {
super(ASM7, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return (mv == null) ? null : new TestMethodVisitor(mv);
}
}
这个 adapter 接收一个 classVisitor 作为输入(即 ClassWriter),在 visitMethod 方法时使用自定义的 TestMethodVisitor 进行访问,再看看 TestMethodVisitor:
public class TestMethodVisitor extends MethodVisitor {
public TestMethodVisitor(MethodVisitor methodVisitor) {
super(ASM7, methodVisitor);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
System.out.println("== TestMethodVisitor, owner = " + owner + “, name = " + name);
//方法执行之前打印
mv.visitLdcInsn(” before method exec");
mv.visitLdcInsn(" [ASM 测试] method in " + owner + " ,name=" + name);
mv.visitMethodInsn(INVOKESTATIC,
“android/util/Log”, “i”, “(Ljava/lang/String;Ljava/lang/String;)I”, false);
mv.visitInsn(POP);
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
//方法执行之后打印
mv.visitLdcInsn(" after method exec");
mv.visitLdcInsn(" method in " + owner + " ,name=" + name);
mv.visitMethodInsn(INVOKESTATIC,
“android/util/Log”, “i”, “(Ljava/lang/String;Ljava/lang/String;)I”, false);
mv.visitInsn(POP);
}
}
TestMethodVisitor 重写了 visitMethodInsn 方法,在默认方法前后插入了一些 “字节码”,这些字节码近似 bytecode,可以认为是 ASM 格式的 bytecode。具体做的事情其实就是分别输出了两条日志:
Log.i("before m
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
ethod exec", “[ASM 测试] method in” + owner + “, name=” + name);
Log.i(“after method exec”, “method in” + owner + “, name=” + name);
话说这么啰哩啰嗦的写一堆就是干这么点儿事儿啊,写起来也太麻烦了吧。 别担心,ASM 提供了一款的插件,可以转化源码为 ASM bytecode。地址在[这里]( )
找一个简单的方法试一下,见下图:
[ASM 测试] method in" + owner + “, name=” + name);
Log.i(“after method exec”, “method in” + owner + “, name=” + name);
话说这么啰哩啰嗦的写一堆就是干这么点儿事儿啊,写起来也太麻烦了吧。 别担心,ASM 提供了一款的插件,可以转化源码为 ASM bytecode。地址在[这里]( )
找一个简单的方法试一下,见下图: