APP下载
报价宝  ›  科技  › 

想成为架构师?字节码插桩可以轻松掌握吗

报价宝 来源:baojiabao.com 发布时间:2019-08-20 07:48:00 09月11日更新
报价宝综合消息想成为架构师?字节码插桩可以轻松掌握吗

1 什么是插桩?

听到关于“插桩”的词语,第一眼觉得会很高深,那到底什么是插桩呢?用通俗的话来讲,插桩就是将一段程式码通过某种策略插入到另一段程式码,或替换另一段程式码。这里的程式码可以分为源代码和字节码,而我们所说的插桩一般指字节码插桩。

图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 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 inputs, Collection 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 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 {

@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这么简单;不过没关系,如果在插桩过程中遇到任何问题,都可以私信给我,我这里也有也一份关于字节码插桩的详细视讯教程,大家私信字节码插桩即可获取。同时,我也非常欢迎大家互相交流技术,共同成长。
文章标签: 报价宝 降噪耳机价格 耳机价格 红米手机价格 华为手机价格 小米手机价格 电视机价格 笔记本电脑价格 笔记本价格 汽车价格 报价宝 汽车价格 笔记本价格 小米手机价格 红米手机价格