Android中使用 Javassist

Javassist

Javassist 是一个执行字节码操作的库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。

Javassist 可以绕过编译,直接操作字节码,从而实现代码注入,所以使用 Javassist 的时机就是在构建工具 Gradle 将源文件编译成 .class 文件之后,在将 .class 打包成 dex 文件之前。

Gradle

Android Studio 项目是使用 Gradle 构建的,构建工具 Gradle 可以看做是一个脚本,包含一系列的Task,依次执行这些 Task 后,项目就打包成功了。

而 Task 有一个重要的概念,那就是 inputsoutputs
Task 通过 inputs 拿到一些东西,处理完毕之后就输出 outputs ,而下一个 Task 的 inputs 则是上一个 Task 的outputs。

例如:一个 Task 的作用是将 java 编译成 class,这个 Task 的 inputs 就是 java 文件的保存目录,outputs 这是编译后的 class 的输出目录,它的下一个 Task 的 inputs 就会是编译后的 class 的保存目录了。


Gradle 由一个个 Task 组成,而这些 Task 都是由 Plugin 来定义的。

比如:

apply plugin : 'com.android.application' 这个 插件定义了将 Module 编译成 application 的一系列 Task。

apply plugin : 'com.android.library' 这个 插件定义了将 Module 编译成 library 的一系列 Task。

不同的 Plugin 提供了不同的 Task 来实际不同的功能。

可以简单的理解为: Gradle只是一个框架,真正起作用的是plugin。而plugin的主要作用是往Gradle脚本中添加Task


我们需要在整个 Gradle 工作的过程中,找到合适的时机来插入自定义的 Plugin,然后在 Plugin 中使用 Javassist 对字节进行操作 ,所以使用 Javassit 的前提是掌握自定义 Gradle 插件,不清楚的可以看前面的 自定义 Gradle 插件 ,这里不多介绍。

在前面说了 Javassist 工作的时机,这个时机在 Gradle1.5 之前并不容易掌握的,从1.5.0-beta1开始,android的gradle插件引入了com.android.build.api.transform.Transform,可以点击 http://tools.android.com/tech-docs/new-build-system/transform-api 查看相关内容,我们刚好就可以利用这个 API 来使用 Javassist 。

transform


流程

1.在 插件的module 中添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
apply plugin: 'groovy'
dependencies {
compile gradleApi() //gradle sdk
compile localGroovy() //groovy sdk
compile 'com.android.tools.build:gradle:2.2.0'
compile 'org.javassist:javassist:3.20.0-GA'
}
repositories {
jcenter()
}

2.在自定义的插件中调用 自定义Transform

1
2
3
4
5
6
7
8
9
10
public class JavassistPlugin implements Plugin<Project> {
void apply(Project project) {
def log = project.logger
log.error "========================";
log.error "Javassist开始修改Class!";
log.error "========================";
project.android.registerTransform(new JavassistTransform(project))
}
}

3.自定义 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
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
package com.deemons.bus
import com.android.build.api.transform.*
import com.google.common.collect.Sets
import javassist.ClassPool
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
public class JavassistTransform extends Transform {
Project project
public JavassistTransform(Project project) { // 构造函数,我们将Project保存下来备用
this.project = project
}
@Override
String getName() {// 设置我们自定义的Transform对应的Task名称
return "JavassistTrans"
}
@Override
// 指定输入的类型,通过这里的设定,可以指定我们要处理的文件类型这样确保其他类型的文件不会传入
Set<QualifiedContent.ContentType> getInputTypes() {
return Sets.immutableEnumSet(QualifiedContent.DefaultContentType.CLASSES)
}
@Override
// 指定Transform的作用范围
Set<QualifiedContent.Scope> getScopes() {
return Sets.immutableEnumSet(QualifiedContent.Scope.PROJECT, QualifiedContent.Scope.PROJECT_LOCAL_DEPS,
QualifiedContent.Scope.SUB_PROJECTS, QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS,
QualifiedContent.Scope.EXTERNAL_LIBRARIES)
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental)
throws IOException, TransformException, InterruptedException {
def startTime = System.currentTimeMillis();
// Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历
inputs.each { TransformInput input ->
try {
//对 jar包 类型的inputs 进行遍历
input.jarInputs.each {
//这里处理自定义的逻辑
MyInject.injectDir(it.file.getAbsolutePath(), "com", project)
// 重命名输出文件(同目录copyFile会冲突)
String outputFileName = it.name.replace(".jar", "") + '-' + it.file.path.hashCode()
def output = outputProvider.getContentLocation(outputFileName, it.contentTypes, it.scopes, Format.JAR)
FileUtils.copyFile(it.file, output)
}
} catch (Exception e) {
project.logger.err e.getMessage()
}
//对类型为“文件夹”的input进行遍历
input.directoryInputs.each { DirectoryInput directoryInput ->
//文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等
//这里处理自定义的逻辑
MyInject.injectDir(directoryInput.file.absolutePath, "com", project)
// 获取output目录
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
// 将input的目录复制到output指定目录
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
ClassPool.getDefault().clearImportedPackages();
project.logger.error("JavassistTransform cast :" + (System.currentTimeMillis() - startTime) / 1000 + " secs");
}
}

以上其实都算是模板了,具体怎样使用 javassist ,后面有参考链接,源码里面也有示例,这里就不介绍了。

源码参考 Demo :JavassistDemo

参考

通过自定义Gradle插件修改编译后的class文件

Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)

安卓AOP实战:Javassist强撸EventBus

Javassist 使用指南(一)

Javassist 使用指南(二)

Javassist 使用指南(三)


坚持分享技术,但行好事,莫问前程 ~^o^~