Gradle插件实现插桩

最近在做App启动优化和卡顿优化的时候涉及到统计方法耗时,使用插桩的方式能够比较方便的解决使用代码硬编码的工作量。函数插桩还可以实现其他的功能,如无埋点统计上报、轻量级AOP等。

1.函数插桩

是什么函数插桩
插桩:目标程序代码中某些位置插入或修改成一些代码,从而在目标程序运行过程中获取某些程序状态并加以分析。简单来说就是在代码中插入代码。
那么函数插桩,便是在函数中插入或修改代码。
本文将介绍在Android编译过程中,往字节码里插入自定义的字节码,所以也可以称为字节码插桩。

2 前奏

实现字节码插桩有多种工具,了解了下AspectJ和ASM,AspectJ使用方法比较简单,作用在java编译阶段,扩展较低,不支持第三方库的插桩,但是框架整体比较重,对插桩后的文件体积也比较大, 但是有扩展库可以解决上述的缺陷,ASM虽然使用方法稍微复杂一点,但是比较轻量级,性能也好。
使用Gradle插件实现插桩还需要了解一下其他的知识
2.1 Android打包流程
2.2 自定义Gradle插件
2.3 ASM 相关资料

3. 实战

了解上述的知识后,就开始实战了,实现自定义Gradle插件 + ASM插桩。
Transform API 是在1.5.0-beta1版开始使用,利用Transform API,第三方的插件可以在.class文件转为dex文件之前,对一些.class 文件进行处理。Transform API 简化了这个处理过程,而且使用起来很灵活。所以我们可以自己实现一个Transform,使用ASM实现插桩。

3.1 创建插件

首先创建一个插件,然后注册一个Transform

1
2
3
4
5
6
7
class CodeInsert implements Plugin<Project> {
@Override
void apply(Project project) {
def android = project.extensions.findByType(AppExtension.class)
android.registerTransform(new CodeTransform(project))
}
}

3.2 创建Transform

然后看主要实现函数transform, 就是遍历编译完的class文件和遍历第三方库的jar文件,使用ASM对其进行处理。这里介绍一个插件——ASM Bytecode Outline,将方便直接转化为ASM的调用方法。

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
transformInvocation.inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput directoryInput ->
if (directoryInput.file.isDirectory()) {
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
if (name.endsWith(".class") && !(name == ("R.class"))
&& !name.startsWith("R\$") && !(name == ("BuildConfig.class"))) {

ClassReader reader = new ClassReader(file.bytes)
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
ClassVisitor visitor = new CodeClassVisitor(writer)
reader.accept(visitor, ClassReader.EXPAND_FRAMES)

byte[] code = writer.toByteArray()
def classPath = file.parentFile.absolutePath + File.separator + name
FileOutputStream fos = new FileOutputStream(classPath)
fos.write(code)
fos.close()
}
}
}

def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)


FileUtils.copyDirectory(directoryInput.file, dest)
}

final File rootOutput = new File(project.buildDir, "classes/${getName()}/")
input.jarInputs.each { JarInput jarInput ->
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
boolean success = false;
File output;
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
output = new File(rootOutput, jarName + "_trace.jar");
if (!output.getParentFile().exists()) {
output.getParentFile().mkdirs();
}
ZipOutputStream zipOutputStream;
ZipFile zipFile;
try {
zipOutputStream = new ZipOutputStream(new FileOutputStream(output));
zipFile = new ZipFile(jarInput.file);
println(jarInput.file.getAbsolutePath())
Enumeration<? extends ZipEntry> enumeration = zipFile.entries();
while (enumeration.hasMoreElements()) {
ZipEntry zipEntry = enumeration.nextElement();
String zipEntryName = zipEntry.getName();
println(zipEntryName)
if (zipEntryName.endsWith(".class") && !zipEntryName.contains("R\$") &&
!zipEntryName.contains("R.class") && !zipEntryName.contains("BuildConfig.class")) {
InputStream inputStream = zipFile.getInputStream(zipEntry);
ClassReader classReader = new ClassReader(inputStream);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new CodeClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
byte[] data = classWriter.toByteArray();
InputStream byteArrayInputStream = new ByteArrayInputStream(data);
ZipEntry newZipEntry = new ZipEntry(zipEntryName);
Util.addZipEntry(zipOutputStream, newZipEntry, byteArrayInputStream);
} else {
InputStream inputStream = zipFile.getInputStream(zipEntry);
ZipEntry newZipEntry = new ZipEntry(zipEntryName);
Util.addZipEntry(zipOutputStream, newZipEntry, inputStream);
}
}
success = true;
} catch (Exception e) {
println("error " + e.toString())
} finally {
try {
if (zipOutputStream != null) {
zipOutputStream.finish();
zipOutputStream.flush();
zipOutputStream.close();
}
if (zipFile != null) {
zipFile.close();
}
} catch (Exception e) {
println(e.toString())
}
}
}

def dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
if (success && output != null) {
println("success set jar " + output.getAbsolutePath())
FileUtils.copyFile(output, dest)
} else {
println("fail set jar " + jarInput.file.getAbsolutePath())
FileUtils.copyFile(jarInput.file, dest)
}
}
}
}

4 结语

本文内容涉及知识较多,这里只讲了插件的实现,在熟悉Android打包过程、字节码、Gradle Transform API、ASM等之前,阅读起来会很困难。不过,在了解并学习这些知识的之后,相信你对Android会有新的认识。
源码

参考资料
函数插桩(Gradle + ASM)
Android字节码插桩采坑笔记
打包Apk过程中的Transform API