个人博客

http://www.milovetingting.cn

ASM字节码插桩

前言

热修复的多Dex加载方案中,对于5.0以下的系统存在CLASS_ISPREVERIFIED的问题,而解决这个问题的一个方案是:通过ASM插桩,在类的构造方法里引入一个其它dex里的类,从而避免被打上CLASS_ISPREVERIFIED标签。热修复可以参考其它资料或者前面写的一篇文章。本文主要介绍ASM插桩,主要参考 https://juejin.im/post/5c6eaa066fb9a049fc042048

ASM框架

ASM是一个可以分析和操作字节码的框架,通过它可以动态地修改字节码内容。使用ASM可以实现无埋点统计、性能监控等。

什么是字节码插桩

Android编译过程中,往字节码插入自定义的字节码。

插桩时机

Android打包要经过:java文件–class文件–dex文件,通过Gradle提供的Transform API,可以在编译成dex文件前,得到class文件,然后通过ASM修改字节码,即字节码插桩。

实现

下面通过自定义Gradle插件来处理class文件来实现插桩。

自定义Gradle插件

具体自定义Gradle插件的步骤,这里不再详细介绍,可以参考之前的一篇文章或者自行查阅其它资料。

处理Class

插件分为插件部分(src/main/groovy)、ASM部分(src/main/java)

ASM插桩

ASMPlugin类继承自Transform并实现Plugin接口,在apply的方法里注册,transform里回调并处理class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ASMPlugin extends Transform implements Plugin<Project> {

@Override
void apply(Project project) {
def android = project.extensions.getByType(AppExtension)
android.registerTransform(this)
}

@Override
String getName() {
return "ASMPlugin"
}

@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}

@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}

@Override
boolean isIncremental() {
return false
}

@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
//处理class
}
}

主要的逻辑处理都在transform方法里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
println('--------------------ASMPlugin transform start--------------------')
def startTime = System.currentTimeMillis()
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//删除旧的输出
if (outputProvider != null) {
outputProvider.deleteAll()
}
//遍历inputs
inputs.each { input ->
//遍历directoryInputs
input.directoryInputs.each {
directoryInput -> handleDirectoryInput(directoryInput, outputProvider)
}
//遍历jarInputs
input.jarInputs.each {
jarInput -> handleJarInput(jarInput, outputProvider)
}
}
def time = (System.currentTimeMillis() - startTime) / 1000
println('-------------------- ASMPlugin transform end --------------------')
println("ASMPlugin cost $time s")
}

在transform里处理class文件和jar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* 处理目录下的class文件
* @param directoryInput
* @param outputProvider
*/
static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
//是否为目录
if (directoryInput.file.isDirectory()) {
//列出目录所有文件(包含子文件夹,子文件夹内文件)
directoryInput.file.eachFileRecurse {
file ->
def name = file.name
if (isClassFile(name)) {
println("-------------------- handle class file:<$name> --------------------")
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new ActivityClassVisitor(classWriter)
classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES)
byte[] bytes = classWriter.toByteArray()
FileOutputStream fileOutputStream = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)
fileOutputStream.write(bytes)
fileOutputStream.close()
}
}
}
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}

/**
* 处理Jar中的class文件
* @param jarInput
* @param outputProvider
*/
static void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
//重名名输出文件,因为可能同名,会覆盖
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.absolutePath)
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tempFile = new File(jarInput.file.parent + File.separator + "temp.jar")
//避免上次的缓存被重复插入
if (tempFile.exists()) {
tempFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempFile))
//保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = enumeration.nextElement()
String entryName = jarEntry.name
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(zipEntry)
if (isClassFile(entryName)) {
println("-------------------- handle jar file:<$entryName> --------------------")
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new ActivityClassVisitor(classWriter)
classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES)
byte[] bytes = classWriter.toByteArray()
jarOutputStream.write(bytes)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + "_" + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tempFile, dest)
tempFile.delete()
}
}

/**
* 判断是否为需要处理class文件
* @param name
* @return
*/
static boolean isClassFile(String name) {
return (name.endsWith(".class") && !name.startsWith("R\$")
&& "R.class" != name && "BuildConfig.class" != name && name.contains("Activity"))
}

在handleDirectoryInput和handleJarInput调用了我们自己定义在src/main/java里的ClassVisitor,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class ActivityClassVisitor extends ClassVisitor implements Opcodes {

private String mClassName;

private static final String CLASS_NAME_ACTIVITY = "androidx/appcompat/app/AppCompatActivity";

private static final String METHOD_NAME_ONCREATE = "onCreate";

private static final String METHOD_NAME_ONDESTROY = "onDestroy";

public ActivityClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}

@Override
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) {
mClassName = name;
super.visit(version, access, name, signature, superName, interfaces);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
if (CLASS_NAME_ACTIVITY.equals(mClassName)) {
if (METHOD_NAME_ONCREATE.equals(name)) {
System.out.println("-------------------- ActivityClassVisitor,visit method:" + name +
" --------------------");
return new ActivityOnCreateMethodVisitor(Opcodes.ASM5, methodVisitor);
} else if (METHOD_NAME_ONDESTROY.equals(name)) {
System.out.println("-------------------- ActivityClassVisitor,visit method:" + name +
" --------------------");
return new ActivityOnDestroyMethodVisitor(Opcodes.ASM5, methodVisitor);
}
}
return methodVisitor;
}
}

这里为简化操作,只处理了Activity的onCreate和onDestroy方法。在visitMethod方法里又调用了具体的MethodVisitor。如果对字节码不是特别了解的,可以通过在Android Studio中安装ASM Bytecode Outline插件来辅助。

具体使用:

安装完成ASM Bytecode Outline后,重启Android Studio,然后在相应的Java文件中右键,选择Show Bytecode outline

ASM插桩2

稍待一会后,会生成相应的字节码,在打开的面板中选择ASMified标签

ASM插桩2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class ActivityOnCreateMethodVisitor extends MethodVisitor {

public ActivityOnCreateMethodVisitor(int api, MethodVisitor mv) {
super(api, mv);
}

@Override
public void visitCode() {
mv.visitLdcInsn("ASMPlugin");
mv.visitLdcInsn("-------------------- MainActivity onCreate --------------------");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" +
"Ljava/lang/String;)I", false);
mv.visitInsn(POP);

super.visitCode();
}

@Override
public void visitInsn(int opcode) {
super.visitInsn(opcode);
}
}

public class ActivityOnDestroyMethodVisitor extends MethodVisitor {

public ActivityOnDestroyMethodVisitor(int api, MethodVisitor mv) {
super(api, mv);
}

@Override
public void visitCode() {
super.visitCode();

mv.visitLdcInsn("ASMPlugin");
mv.visitLdcInsn("-------------------- MainActivity onDestroy --------------------");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" +
"Ljava/lang/String;)I", false);
mv.visitInsn(POP);
}

@Override
public void visitInsn(int opcode) {
super.visitInsn(opcode);
}
}

在visitCode和visitInsn方法里执行具体的操作。

在处理Class过程中,可能会出现各种问题,可以通过调试插件来定位问题。可以参考上一篇文章来调试插件。

引用插件

在app模块引用插件,这里不再详细介绍,可以参考前面的文章

将应用运行在手机上,打开后,可以看到日志输出:

1
2
02-25 17:29:45.885 31237 31237 I ASMPlugin: -------------------- MainActivity onCreate --------------------
02-25 17:29:50.646 31237 31237 I ASMPlugin: -------------------- MainActivity onDestroy --------------------

结语

这篇文章只是实现了简单的ASM插桩。可以查阅其它资料,了解更多关于字节码、ASM相关的内容。

源码地址:https://github.com/milovetingting/Samples/tree/master/ASM