字节码插桩 -- 你也可以轻松掌握,2021 年 Android 工作或许更难找
字节码修改工具。如 AspectJ,ASM,javasisst。这里我推荐使用 ASM,关于 ASM 相关知识,在下一章我给大家简单介绍。同样大家可以参考 Asm官方文档
groovy 语言基础
如果你具备了上面 5 块知识,那么恭喜你,会很顺利的完成字节码插桩技术了。下面,我通过实战一个很简单的例子,带领大家一起领略插桩的风采。
4 使用 ASM 进行字节码插桩
1 什么是 ASM?
ASM 是生成和转换已编译的 Java 类工具,就是我们插桩需要使用的工具。
2 两种 API?
ASM 提供了两种 API 来生成和转换已编译类,一个是核心 API,以基于事件形式来表示类;另一个是树 API,以基于对象形式来表示类。
3 基于事件形式
我们通过上面的基础知识,了解到类的结构,类包含字段,方法,指令等;基于事件的 API 把类看作是一系列事件来表示,每一个类的事件表示一个类的元素。类似解析 XML 的 SAX
4 基于对象形式
基于对象的 API 将类表示成一棵对象树,每个对象表示类的一部分。类似解析 XML 的 DOM
5 优缺点比较
通过上面表格,我们清楚的了解到:
事件 API 内存占用少于对象 API,因为事件 API 不需要在内存中创建和存储对象树
事件 API 实现难度比对象 API 大,因为事件 API 在任意时刻类中只有一个元素可使用,但是对象 API 能获得整个类。
那么接下来,我们就通过比较容易实现的对象 API 入手,一起完成上面的需求。 我们 Android 的构建工具是 Gradle,因此我们结合 transform 和 Gradle 插件方式来完成该需求,接下来我们来看看 gradle 官方提供的 3 种插件形式 6 Gradle 插件的 3 种形式
| 插件形式 | 说明 ||
--- | --- || Build script | 直接在 build script 中写插件代码,不可复用 || buildSrc | 独立项目结构,只能在本构建体系中复用,无法提供给其他项目 || Standalone | 独立项目结构,发布到仓库,可以复用 |
由于我们是 demo,并不需要共享给其他项目,因此采用 buildSrc 方式即可,但是正常项目中都采用 Standalone 形式。
5 插桩实践
目标 : 删除所有以 test 开头的方法
接下来我们来完成一个非常小的需求,删除所有以 test 开头的方法。为什么说这是一个小需求,因为这并不涉及指令的操作,所有操作通过方法名完成即可。通过完成这个 demo,只是抛砖引玉。如若后期需要,可以逐步深入到指令级别替换。 接下来的步骤就是创建 demo 的过程
1 新建 buildSrc 目录,用来存放源代码位置。针对不同语言可以新建不同目录。
如上图所示的是 buildSrc 的结构。
2 在 buildSrc 的 gradle 文件中我们需要配置如下代码
apply plugin: 'groovy'dependencies {compile gradleApi()//在使用自定义插件时候,一定要引用 org.gradle.api.Plugincompile 'com.android.tools.build:gradle:3.3.2'//使用自定义 transform 时候,需要引用 com.android.build.api.transform.Transformcompile 'org.ow2.asm:asm:6.0'compile 'commons-io:commons-io:2.6'}repositories {mavenCentral()jcenter()google()}
3 重写 Transform API 在 groovy 目录下新建一个 groovy 类并继承 Transform,注意导包 com.android.build.api.transform,并实现抽象方法和 transform 方法,如下
class MyTransform extends Transform {Project projectMyTransform(Project project) {this.project = project}@OverrideString getName() {return "MyTransform"}//设置输入类型,我们是针对 class 文件处理 @OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}//设置输入范围,我们选择整个项目 @OverrideSet<? super QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT}@Overrideboolean isIncremental() {return true}//重点就是该方法,我们需要将修改字节码的逻辑就从这里开始 @Overridevoid transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {inputs.each {TransformInput input ->input.getJarInputs().each {//处理 jar 文件,代码太多,这里暂时不贴}input.getDirectoryInputs().each {//处理目录文件,这里的 ASMHelper.transformClass()是修改字节码逻辑 def destDir = transformInvocation.outputProvider.getContentLocation("${dir.name}_transformed",dir.contentTypes,dir.scopes,Format.DIRECTORY)if (dir.file) {def modifiedRecord = [:]dir.file.traverse(type: FileType.FILES, nameFilter: ~/.*.class/) {File classFile ->def className = classFile.absolutePath.replace(dir.getFile().getAbsolutePath(), "")if (!ASMHelper.filter(className)) {def transformedClass = ASMHelper.transformClass(classFile, dir.file, transformInvocation.context.temporaryDir)modifiedRecord[(className)] = transformedClass}}FileUtils.copyDirectory(dir.file, destDir)modifiedRecord.each { name, file ->def targetFile = new File(destDir.absolutePath, name)if (targetFile.exists()) {targetFile.delete()}FileUtils.copyFile(file, targetFile)}modifiedRecord.clear()}}}}
4 实现字节码修改逻辑 Transform 我们已经定义完成,接下来就要针对读入的字节码进行修改。我们采用对象 API 进行解析 class 文件。一共就是 3 个步骤:
将输入流转化为 ClassNode
处理 ClassNode,这里就是我们的业务逻辑所在
将 ClassNode 转为字节数组输出 当然还有其他文件的 IO 操作,这里因为篇幅限制未贴出,如若需要 demo,可以私信。
static byte[] modifyClass(InputStream inputStream) {
评论