字节码插桩实践

Gradle Plugin

首先我们需要创建一个插件来注册我们的Transform,步骤如下

创建module

Android Module项目,类型选择Android Library,将Module里的内容删除,只保留build.gradle文件和src/main目录,同时移除build.gradle文件里的内容

修改build.gradle

gradle插件可以使用java/groovy/kotlin实现,我们选择java。修改build.gradle为如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apply plugin: 'java'
apply plugin: 'maven'

dependencies{
// gradle sdk
compile gradleApi()
// groovy sdk
compile localGroovy()
compile 'com.android.tools.build:gradle:1.5.0'

implementation'com.android.tools.build:gradle-api:3.1.4'
implementation'com.android.tools.build:gradle-core:1.5.0'
}

repositories{
mavenCentral()
}

创建插件类

在main目录下建立java目录,然后新建package,最后创建类PluginImpl

1
2
3
4
5
6
public class PluginImpl implements Plugin<Project>{

public void apply(Project project){
// 执行plugin的入口
}
}

定义插件

定义插件名称.在resources/META-INF/gradle-plugins目录下新建一个properties文件,注意该文件的命名就是你使用插件的名字,比如tt.properties,那么你在其他build.gradle文件中使用自定义的插件时候则需写成:

1
apply plugin: 'tt'

tt.properties文件内容为

1
implementation-class=com.wulinpeng.PluginImpl

表明该Plugin的实现类

插件发布到本地Maven仓库

在我们定义的module下的build.gralde中添加如下代码

1
2
3
4
5
6
7
8
9
10
11
uploadArchives {
repositories {
mavenDeployer {
pom.groupId = 'com.wulinpeng'
pom.artifactId = 'tt'
pom.version = 1.0
// maven本地仓库的目录
repository(url: uri('../TTPlugin'))
}
}
}

运行uploadArchives后项目下多出一个TTPluginn目录,里面存着这个gradle插件。

插件的使用

要想使用插件我们需要在项目根目录下的gradle.build的文件中加入如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
buildscript {
repositories {
// maven插件目录
maven{
url uri('TTPlugin')
}
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
// 使用自定义插件
classpath 'com.wulinpeng:tt:1.0'
}
}

最后在app下的build.gradle中添加apply plugin: 'tt'就可以了

Transform API

在插件中注册Transform

1
2
3
4
5
6
public class PluginImpl implements Plugin<Project>{

public void apply(Project project){
project.getExtensions().findByType(AppExtension.class).registerTransform(new TTTransform());
}
}

JarInput & DirectoryInput处理

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/**
* author:wulinpeng
* date:2020-01-25 22:48
* desc:
*/
public abstract class BaseTransform extends Transform {

@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
}

public void triggerScan(TransformInvocation transformInvocation, BytecodeAdapter adapter) throws TransformException, InterruptedException, IOException {
//消费型输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
Collection<TransformInput> inputs = transformInvocation.getInputs();
//引用型输入,无需输出。
Collection<TransformInput> 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);
dealJar(transformInvocation.getContext(), jarInput.getFile(), adapter);
FileUtils.copyFile(jarInput.getFile(), dest);
}
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY);
//将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.getAllFiles(directoryInput.getFile()).filter(new Predicate<File>() {

@Override
public boolean apply(@Nullable File input) {
return input.getName().endsWith(SdkConstants.DOT_CLASS);
}
}).forEach(new Consumer<File>() {
@Override
public void accept(File file) {
try {
// 修改字节码
byte[] classCode = adapter.acceptBytecode(IOUtils.toByteArray(new FileInputStream(file)));
FileOutputStream outputStream = new FileOutputStream(file);
outputStream.write(classCode);
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
// 将file copy到目标目录
try {
FileUtils.copyDirectory(directoryInput.getFile(), dest);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
}

/**
* 做一次input 到 dest的copy
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
public void flulsh(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Collection<TransformInput> inputs = transformInvocation.getInputs();
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);
FileUtils.copyFile(jarInput.getFile(), dest);
}
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY);
// 将file copy到目标目录
FileUtils.copyDirectory(directoryInput.getFile(), dest);
}
}
}

public static void dealJar(Context context, File jarfile, BytecodeAdapter adapter) {
File modifyFile = modifyJar(jarfile, context.getTemporaryDir(), true, adapter);
if (jarfile.exists()) {
jarfile.delete();
}
try {
FileUtils.copyFile(modifyFile, jarfile);
} catch (IOException e) {
e.printStackTrace();
}
}

static File modifyJar(File jarFile, File tempDir, boolean nameHex, BytecodeAdapter adapter) {
try {
JarFile file = new JarFile(jarFile);
String hexName = "";
if (nameHex) {
hexName = DigestUtils.md5Hex(jarFile.getAbsolutePath()).substring(0, 8);
}
File outputJar = new File(tempDir, hexName + jarFile.getName());
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(outputJar));
Enumeration enumeration = file.entries();
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement();
InputStream inputStream = file.getInputStream(jarEntry);
String entryName = jarEntry.getName();
ZipEntry zipEntry = new ZipEntry(entryName);
jarOutputStream.putNextEntry(zipEntry);
byte[] modifiedClassBytes = null;
byte[] sourceClassBytes = IOUtils.toByteArray(inputStream);
if (shouldModifyClass(entryName)) {
modifiedClassBytes = adapter.acceptBytecode(sourceClassBytes);
jarOutputStream.write(modifiedClassBytes);
} else {
jarOutputStream.write(sourceClassBytes);
}
jarOutputStream.closeEntry();
}
jarOutputStream.close();
file.close();
return outputJar;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

private static boolean shouldModifyClass(String entryName) {
if (!entryName.endsWith(".class")) {
return false;
}
if (entryName.startsWith("android") || entryName.startsWith("java") || entryName.startsWith("kotlin")) {
return false;
} else {
return true;
}
}

interface BytecodeAdapter {
byte[] acceptBytecode(byte[] bytecode);
}
}

ASM