字节码插桩 -- 你也可以轻松掌握
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 种形式
由于我们是 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) {ClassNode classNode = new ClassNode(Opcodes.ASM5)ClassReader classReader = new ClassReader(inputStream)//1 将读入的字节转为 classNodeclassReader.accept(classNode, 0)//2 对 classNode 的处理逻辑 Iterator<MethodNode> iterator = classNode.methods.iterator();while (iterator.hasNext()) {MethodNode node = iterator.next()if (node.name.startsWith("test")) {iterator.remove()}}ClassWriter classWriter = new ClassWriter(0)//3 将 classNode 转为字节数组 classNode.accept(classWriter)return classWriter.toByteArray()}
5 插件化 上面我们完成了字节码修改逻辑以及定义 Transform,但是并没有完成插件的定义。结合Transform API我们了解到,需要将我们自定义的 Transform 注册到插件中,如下
class MyPlugin implements Plugin<Project> {@Overridevoid apply(Project project) {project.android.registerTransform(new MyTransform(project))}}
6 提供可对外使用的插件 插件完成了,但是怎么才能对外使用呢?上面我们说到,我们采取 3 种插件形式之一的 buildSrc。我们上文中创建了 plugin.properties 文件。只需要在该文件中编辑实现类即可
implementation-class=MyPlugin
7 应用方应用插件 在应用方的 gradle 文件中做如下配置
评论