字节码插桩--你也可以轻松掌握

栏目: Groovy · 发布时间: 6年前

内容简介:听到关于“插桩”的词语,第一眼觉得会很高深,那到底什么是插桩呢?用通俗的话来讲,插桩就是将一段代码通过某种策略插入到另一段代码,或替换另一段代码。这里的代码可以分为源码和字节码,而我们所说的插桩一般指字节码插桩。 图1是Android开发者常见的一张图,我们编写的源码(.java)通过javac编译成字节码(.class),然后通过dx/d8编译成dex文件。我们下面要讲的插桩,就是在.class转为.dex之前,修改.class文件从而达到修改或替换代码的目的。 那有人肯定会有这样的疑问?既然插桩是插入或

听到关于“插桩”的词语,第一眼觉得会很高深,那到底什么是插桩呢?用通俗的话来讲,插桩就是将一段代码通过某种策略插入到另一段代码,或替换另一段代码。这里的代码可以分为源码和字节码,而我们所说的插桩一般指字节码插桩。 图1是Android开发者常见的一张图,我们编写的源码(.java)通过javac编译成字节码(.class),然后通过dx/d8编译成dex文件。

字节码插桩--你也可以轻松掌握

我们下面要讲的插桩,就是在.class转为.dex之前,修改.class文件从而达到修改或替换代码的目的。 那有人肯定会有这样的疑问?既然插桩是插入或替换代码,那为何我不自己直接插入或替换呢?为何还要用这么“复杂”的工具?别着急,第二个问题将会给你答案。

2 插桩的应用场景有哪些?

技术是服务于业务的,一个无法推进业务进步的技术并不值得我们学习。在上面,我们对插桩的理解是:插入,替换代码。那么,结合这个核心主线我们来挖掘插桩能被应用的场景有哪些?

  • ####代码插入 我们所熟悉的ButterKnife,Dagger这些常用的框架,也是在编译期间生成了代码,简化了 程序员 的操作。假设有这么一个需求,要监控某些或者所有方法的执行耗时?你会怎么做呢?如果你监控的方法只有十几个或者几十个,那么也许通过程序员自身的编码就能轻松解决;但是如果监控的方法达到百千甚至万级别,你还通过编码来解决?那么程序员存在的价值在哪里?面对这样的重复劳动问题,最先想到的就应该是自动化,也就是我们今天所讲的插桩。通过插桩,我们扫描每一个class文件,并针对特定规则进行字节码修改从而达到监控每个方法耗时的目的。关于如何实现这样的需求,后面我会详细讲述。
  • ####代码替换 如果遇到这么一个需求,需要将项目中所有使用某个方法(如Dialog.show())的地方替换成自己包装的方法(MyDialog.show()),那么你该如何解决呢?有人会说,直接使用快捷键就能全局替换。那么有两个问题 1 如果有其他类定义了show()方法,并被调用了,直接使用快捷键是否会被错误替换? 2 如果其他引用包使用了该方法,你怎么替换呢? 没关系,插桩同样可以解决你的问题。 综合上面所说的两点,其实很多业务场景都使用了插桩技术,比如无痕埋点,性能监控等。

3 掌握插桩应该具备的基础知识有哪些?

上面讲了插桩的应用场景,是否现在想跃跃欲试呢?别着急,想掌握好插桩技术,练就扎实的插桩功底,我们是需要具备一些基础知识的。

  • 熟练掌握字节码相关技术。可参考 一文让你明白 Java 字节码

  • Gradle自定义插件,直接参考官网 Writing Custom plugins

  • 如果你想运用在Android项目中,那么还需要掌握Transform API, 这是android在将class转成dex之前给我们预留的一个接口,在该接口中我们可以通过插件形式来修改class文件。

  • 字节码修改工具。如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.Plugin
   compile 'com.android.tools.build:gradle:3.3.2'//使用自定义transform时候,需要引用com.android.build.api.transform.Transform
   compile '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 project
   MyTransform(Project project) {
       this.project = project
   }
   @Override
   String getName() {
       return "MyTransform"
   }
   //设置输入类型,我们是针对class文件处理
   @Override
   Set<QualifiedContent.ContentType> getInputTypes() {
       return TransformManager.CONTENT_CLASS
   }
   //设置输入范围,我们选择整个项目
   @Override
   Set<? super QualifiedContent.Scope> getScopes() {
       return TransformManager.SCOPE_FULL_PROJECT
   }
   @Override
   boolean isIncremental() {
       return true
   }
   //重点就是该方法,我们需要将修改字节码的逻辑就从这里开始
   @Override
   void 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个步骤: 1 将输入流转化为ClassNode 2 处理ClassNode,这里就是我们的业务逻辑所在 3 将ClassNode转为字节数组输出 当然还有其他文件的IO操作,这里因为篇幅限制未贴出,如若需要demo,可以私信。
static byte[] modifyClass(InputStream inputStream) {
       ClassNode classNode = new ClassNode(Opcodes.ASM5)
       ClassReader classReader = new ClassReader(inputStream)
       //1 将读入的字节转为classNode
       classReader.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> {
    @Override
    void apply(Project project) {
        project.android.registerTransform(new MyTransform(project))
    }
}
复制代码
  • 6 提供可对外使用的插件 插件完成了,但是怎么才能对外使用呢?上面我们说到,我们采取3种插件形式之一的buildSrc。我们上文中创建了plugin.properties文件。只需要在该文件中编辑实现类即可
implementation-class=MyPlugin
复制代码
  • 7 应用方应用插件 在应用方的gradle文件中做如下配置
apply plugin: 'plugin'
复制代码

上面代码我们注意到,plugin这个插件和plugin.properties的文件名是一样的。是的,应用方应用的插件名和我们定义的properties文件名保持一致。

  • 8 结果展示 源代码如下,经过我们插件处理之后,编译后的字节码应该没有了testDemo方法。
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(android.R.layout.activity_list_item);
    }
    public void testDemo() {
        System.out.println("demo test");
    }
}
复制代码

那么,处理后的字节码在哪呢?在*$project/build/intermediates/transforms/MyTransform/...* MyTransform是我自定义Transform的类名,下面有debug和release包。继续下去大家应该能找到对应的类。

字节码插桩--你也可以轻松掌握
上图我们看到,已经没有的testDemo方法。 成功!

6 结束语

通过上面实战练习,相信你已经初步掌握了插桩的基本技术,但是这还远远不够;在项目中会遇到各式各样的问题,现实情况可能没有demo这么简单;不过没关系,如果在插桩过程中遇到任何问题,都可以私信给我,我将尽我所能的给你提供最优质的免费咨询服务。同时,我也非常欢迎大家互相交流技术,共同成长。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Data Mining

Web Data Mining

Bing Liu / Springer / 2011-6-26 / CAD 61.50

Web mining aims to discover useful information and knowledge from Web hyperlinks, page contents, and usage data. Although Web mining uses many conventional data mining techniques, it is not purely an ......一起来看看 《Web Data Mining》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

MD5 加密
MD5 加密

MD5 加密工具