手把手教大家用Transform API和ASM实现一个防快速点击案例

class MethodTimeTransform extends Transform {

@Override

String getName() {

return “MethodTimeTransform”

}

@Override

Set<QualifiedContent.ContentType> getInputTypes() {

//需要处理的数据类型,这里表示class文件

return TransformManager.CONTENT_CLASS

}

@Override

Set<? super QualifiedContent.Scope> getScopes() {

//作用范围

return TransformManager.SCOPE_FULL_PROJECT

}

@Override

boolean isIncremental() {

//是否支持增量编译

return true

}

@Override

void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

super.transform(transformInvocation)

//TransformOutputProvider管理输出路径,如果消费型输入为空,则outputProvider也为空

TransformOutputProvider outputProvider = transformInvocation.outputProvider

//transformInvocation.inputs的类型是Collection,可以从中获取jar包和class文件夹路径。需要输出给下一个任务

transformInvocation.inputs.each { input -> //这里的input是TransformInput

input.jarInputs.each { jarInput ->

//处理jar

processJarInput(jarInput, outputProvider)

}

input.directoryInputs.each { directoryInput ->

//处理源码文件

processDirectoryInput(directoryInput, outputProvider)

}

}

}

void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {

File dest = outputProvider.getContentLocation(jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR)

//将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的

println(“拷贝文件 $dest -----”)

FileUtils.copyFile(jarInput.file, dest)

}

void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {

File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format

.DIRECTORY)

//将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的

println(“拷贝文件夹 $dest -----”)

FileUtils.copyDirectory(directoryInput.file, dest)

}

}

  1. getName(): 表示当前Transform名称,这个名称会被用来创建目录,它会出现在app/build/intermediates/transforms目录下面.

  2. getInputTypes(): 需要处理的数据类型,用于确定我们需要对哪些类型的结果进行转换,比如class,资源文件等:

  • CONTENT_CLASS:表示需要处理java的class文件

  • CONTENT_JARS:表示需要处理java的class与资源文件

  • CONTENT_RESOURCES:表示需要处理java的资源文件

  • CONTENT_NATIVE_LIBS:表示需要处理native库的代码

  • CONTENT_DEX:表示需要处理DEX文件

  • CONTENT_DEX_WITH_RESOURCES:表示需要处理DEX与java的资源文件

  1. getScopes(): 表示Transform要操作的内容范围(上面demo里面使用的SCOPE_FULL_PROJECT是Scope的集合,包含了Scope.PROJECT,Scope.SUB_PROJECTS,Scope.EXTERNAL_LIBRARIES这几个东西.当然,TransformManager里面还有一些其他集合,这里不做举例).
  • PROJECT: 只有项目内容

  • SUB_PROJECTS: 只有子项目

  • EXTERNAL_LIBRARIES: 只有外部库

  • TESTED_CODE: 测试代码

  • PROVIDED_ONLY: 只提供本地或远程依赖项

  1. isIncremental(): 是否支持增量更新
  • 如果返回true,则TransformInput会包含一份修改的文件列表

  • 如果是false,则进行全量编译,删除上一次输出内容

  1. transform(): 进行具体转换逻辑.
  • 消费型Transform: 在transform方法中,我们需要将每个jar包和class文件复制到dest路径,这个dest路径就是下一个Transform的输入数据.在复制的时候,我们可以将jar和class文件的字节码做一些修改,再进行复制. 可以看出,如果我们注册了Transform,但是又不将内容复制到下一个Transform需要的输入路径的话,就会出问题,比如少了一些class之类的.上面的demo中仅仅是将所有的输入文件拷贝到目标目录下,并没有对字节码文件进行任何处理.

  • 引用型Transform: 当前Transform可以读取这些输入,而不需要输出给下一个Transform.

可以看出,最关键的核心代码就是transform()方法里面,我们需要做一些class文件字节码的修改,才能让Transform发挥其效果.

道理是这个道理,但是字节码那玩意儿想改就能改么? 忘记字节码是什么的小伙伴可以看我之前发的文章 Java字节码解读 复习一下. 字节码比较复杂,连"读懂"都非常非常困难,还让我去改它,那更是难上加难.

不过,幸好咱们可以借助后面介绍的ASM工具进行方便的修改字节码工作.

1.3 增量编译

就是Transform中的isIncremental()方法返回值,如果是false的话,则表示不开启增量编译,每次都得处理每个文件,非常非常拖慢编译时间. 我们可以借助该方法,返回值改成true,开启增量编译.当然,开启了增量编译之后需要检查每个文件的Status,然后根据这个文件的Status进行不同的操作.

具体的Status如下:

  • NOTCHANGED: 当前文件不需要处理,连复制操作也不用

  • ADDED: 正常处理,输出给下一个任务

  • CHANGED: 正常处理,输出给下一个任务

  • REMOVED: 移除outputProvider获取路径对应的文件

来看一下代码如何实现,咱将上面的dmeo代码简单改改:

@Override

void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

super.transform(transformInvocation)

printCopyRight()

//TransformOutputProvider管理输出路径,如果消费型输入为空,则outputProvider也为空

TransformOutputProvider outputProvider = transformInvocation.outputProvider

//当前是否是增量编译,由isIncremental方法决定的

// 当上面的isIncremental()写的返回true,这里得到的值不一定是true,还得看当时环境.比如clean之后第一次运行肯定就不是增量编译嘛.

boolean isIncremental = transformInvocation.isIncremental()

if (!isIncremental) {

//不是增量编译则删除之前的所有文件

outputProvider.deleteAll()

}

//transformInvocation.inputs的类型是Collection,可以从中获取jar包和class文件夹路径。需要输出给下一个任务

transformInvocation.inputs.each { input -> //这里的input是TransformInput

input.jarInputs.each { jarInput ->

//处理jar

processJarInput(jarInput, outputProvider, isIncremental)

}

input.directoryInputs.each { directoryInput ->

//处理源码文件

processDirectoryInput(directoryInput, outputProvider, isIncremental)

}

}

}

/**

  • 处理jar

  • 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的

*/

void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) {

def status = jarInput.status

File dest = outputProvider.getContentLocation(jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR)

if (isIncremental) {

switch (status) {

case Status.NOTCHANGED:

break

case Status.ADDED:

case Status.CHANGED:

transformJar(jarInput.file, dest)

break

case Status.REMOVED:

if (dest.exists()) {

FileUtils.forceDelete(dest)

}

break

}

} else {

transformJar(jarInput.file, dest)

}

}

void transformJar(File jarInputFile, File dest) {

//println(“拷贝文件 $dest -----”)

FileUtils.copyFile(jarInputFile, dest)

}

/**

  • 处理源码文件

  • 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的

*/

void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) {

File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format

.DIRECTORY)

FileUtils.forceMkdir(dest)

println(“isIncremental = $isIncremental”)

if (isIncremental) {

String srcDirPath = directoryInput.getFile().getAbsolutePath()

String destDirPath = dest.getAbsolutePath()

Map<File, Status> fileStatusMap = directoryInput.getChangedFiles()

for (Map.Entry<File, Status> changedFile : fileStatusMap.entrySet()) {

Status status = changedFile.getValue()

File inputFile = changedFile.getKey()

String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath)

File destFile = new File(destFilePath)

switch (status) {

case Status.NOTCHANGED:

break

case Status.ADDED:

case Status.CHANGED:

FileUtils.touch(destFile)

transformSingleFile(inputFile, destFile)

break

case Status.REMOVED:

if (destFile.exists()) {

FileUtils.forceDelete(destFile)

}

break

}

}

} else {

transformDirectory(directoryInput.file, dest)

}

}

void transformSingleFile(File inputFile, File destFile) {

println(“拷贝单个文件”)

FileUtils.copyFile(inputFile, destFile)

}

void transformDirectory(File directoryInputFile, File dest) {

println(“拷贝文件夹 $dest -----”)

FileUtils.copyDirectory(directoryInputFile, dest)

}

根据是否为增量更新,如果不是,则删除之前的所有文件.然后对每个文件进行状态判断,根据其状态来决定到底是该删除,或者复制.开启增量编译之后,速度会有特别大的提升.

1.4 并发编译

毕竟是在电脑上进行编译,尽管压榨电脑性能,我们把并发编译给搞起.说来也轻巧,就下面几行代码就行

private WaitableExecutor mWaitableExecutor = WaitableExecutor.useGlobalSharedThreadPool()

transformInvocation.inputs.each { input -> //这里的input是TransformInput

input.jarInputs.each { jarInput ->

//处理jar

mWaitableExecutor.execute(new Callable() {

@Override

Object call() throws Exception {

//多线程

processJarInput(jarInput, outputProvider, isIncremental)

return null

}

})

}

//处理源码文件

input.directoryInputs.each { directoryInput ->

//多线程

mWaitableExecutor.execute(new Callable() {

@Override

Object call() throws Exception {

processDirectoryInput(directoryInput, outputProvider, isIncremental)

return null

}

})

}

}

//等待所有任务结束

mWaitableExecutor.waitForTasksWithQuickFail(true)

增加的代码不多,其他都是之前的.就是让处理逻辑的地方放线程里面去执行,然后得等这些线程都处理完成才结束任务.

到这里Transform基本的API也将介绍完了,原理(系统有一些列Transform用于在class转dex的过程中的处理逻辑,我们也可以自定义Transform参与其中,这个Transform最终其实是在一个Task里面执行的.)的话也知晓了个大概,接下来我们看看如何利用ASM修改字节码实现炫酷的功能吧.

2. ASM


2.1 介绍

ASM官网

官网上是这样介绍ASM的: ASM是一个通用的Java字节码操作和分析框架。它可以直接以二进制形式用于修改现有类或动态生成类。ASM提供了一些常见的字节码转换和分析算法,可从中构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但是侧重于 性能。因为它的设计和实现是尽可能的小和尽可能快,所以它非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。(可能翻译得不是很准确,英文好的同学可以去官网看原话)

2.2 引入ASM

下面是我的demo中的buildSrc里面build.gradle配置.它包含了Plugin+Transform+ASM的所有依赖,放心拿去用.

dependencies {

implementation gradleApi()

implementation localGroovy()

//常用io操作

implementation “commons-io:commons-io:2.6”

// Android DSL Android编译的大部分gradle源码

implementation ‘com.android.tools.build:gradle:3.6.2’

implementation ‘com.android.tools.build:gradle-api:3.6.2’

//ASM

implementation ‘org.ow2.asm:asm:7.1’

implementation ‘org.ow2.asm:asm-util:7.1’

implementation ‘org.ow2.asm:asm-commons:7.1’

}

2.3 ASM基本使用

在使用之前我们先来看一些常用的对象

  • ClassReader : 按照Java虚拟机规范中定义的方式来解析class文件中的内容,在遇到合适的字段时调用ClassVisitor中相应的方法

  • ClassVisitor : Java中类的访问者,提供一系列方法由ClassReader调用.它是一个抽象类,在使用时需要继承此类.

  • ClassWriter : 它是一个继承了ClassVisitor的类,主要负责将ClassReader传递过来的数据写到一个字节流中.在传递数据完成之后,可以通过它的toByteArray方法获得完整的字节流.

  • ModuleVisitor : Java中模块的访问者,作为ClassVisitor.visitModule方法的返回值,要是不关心模块的使用情况,可以返回一个null.

  • AnnotationVisitor : Java中注解的访问者,作为ClassVisitor.visitTypeAnnotation的返回值,不关心注解使用情况也是可以返回null.

  • FieldVisitor : Java中字段的访问者,作为ClassVisitor.visitField的返回值,不关心字段使用情况也是可以返回null.

  • MethodVisitor:Java中方法的访问者,作为ClassVisitor.visitMethod的返回值,不关心方法使用情况也是可以返回null.

上面这些对象先简单过一下,眼熟就行,待会儿会使用到这些对象.

大体工作流程: 通过ClassReader读取class字节码文件,然后ClassReader将读取到的数据通过一个ClassVisitor(上面的ClassWriter其实就是一个ClassVisitor)将数据表现出来.表现形式: 将字节码的每个细节按顺序通过接口的方式传递给ClassVisitor.就比如说,访问到了class文件的xx方法,就会回调ClassVisitor的visitMethod方法;访问到了class文件的属性,就会回调ClassVisitor的visitField方法.

ClassWriter是一个继承了ClassVisitor的类,它保存了这些由ClassReader读取出来的字节流数据,最后通过它的toByteArray方法获得完整的字节流.

上面的概念比较生硬,咱们先来写一个简单的复制class文件的方法:

private void copyFile(File inputFile, File outputFile) {

FileInputStream inputStream = new FileInputStream(inputFile)

FileOutputStream outputStream = new FileOutputStream(outputFile)

//1. 构建ClassReader对象

ClassReader classReader = new ClassReader(inputStream)

//2. 构建ClassVisitor的实现类ClassWriter

ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)

//3. 将ClassReader读取到的内容回调给ClassVisitor接口

classReader.accept(classWriter, ClassReader.EXPAND_FRAMES)

//4. 通过classWriter对象的toByteArray方法拿到完整的字节流

outputStream.write(classWriter.toByteArray())

inputStream.close()

outputStream.close()

}

看到这里,可能有的同学已经有点感觉了.ClassReader对象就是专门负责读取字节码文件的,而ClassWriter就是一个继承了ClassVisitor的类,当ClassReader读取字节码文件的时候,数据会通过ClassVisitor回调回来.咱们可以自定义一个ClassWriter用来接收读取到的字节数据,接收数据的同时,咱们再插入一点东西到这些数据的前面或者后面,最后通过ClassWriter的toByteArray方法将这些字节码数据导出,写入新的文件,这就是我们所说的插桩了.

现在咱们举个栗子,到底插桩能有啥用?就实现一个简单的需求吧,在每个方法的最前面插入一句打印Hello World!的代码.

修改前的代码如下所示:

private void test() {

System.out.println(“test”);

}

预期修改后的代码:

private void test() {

System.out.println(“Hello World!”);

System.out.println(“test”);

}

将上面的复制文件的代码简单改改

void traceFile(File inputFile, File outputFile) {

FileInputStream inputStream = new FileInputStream(inputFile)

FileOutputStream outputStream = new FileOutputStream(outputFile)

ClassReader classReader = new ClassReader(inputStream)

ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)

classReader.accept(new HelloClassVisitor(classWriter)), ClassReader.EXPAND_FRAMES)

outputStream.write(classWriter.toByteArray())

inputStream.close()

outputStream.close()

}

唯一有变化的地方就是classReader的accept方法传入的ClassVisitor对象变了,咱自定义了一个HelloClassVisitor.

class HelloClassVisitor extends ClassVisitor {

HelloClassVisitor(ClassVisitor cv) {

//这里需要指定一下版本Opcodes.ASM7

super(Opcodes.ASM7, cv)

}

@Override

MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {

def methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions)

return new HelloMethodVisitor(api, methodVisitor, access, name, descriptor)

}

}

我们自定义了一个ClassVisitor,它将ClassWriter传入其中.在ClassVisitor的实现中,只要传入了classVisitor对象,那么就会将功能委托给这个classVisitor对象.相当于我传入的这个ClassWriter就读取到了字节码,最后toByteArray就是所有的字节码.多说无益,看看代码:

public abstract class ClassVisitor {

/** The class visitor to which this visitor must delegate method calls. May be null. */

protected ClassVisitor cv;

public ClassVisitor(final int api, final ClassVisitor classVisitor) {

if (api != Opcodes.ASM7 && api != Opcodes.ASM6 && api != Opcodes.ASM5 && api != Opcodes.ASM4) {

throw new IllegalArgumentException("Unsupported api " + api);

}

this.api = api;

this.cv = classVisitor;

}

public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {

if (cv != null) {

return cv.visitAnnotation(descriptor, visible);

}

return null;

}

public MethodVisitor visitMethod(

final int access,

final String name,

final String descriptor,

final String signature,

final String[] exceptions) {

if (cv != null) {

return cv.visitMethod(access, name, descriptor, signature, exceptions);

}

return null;

}

}

有了我们传入的ClassWriter,咱们在自定义ClassVisitor的时候,只需要关注需要修改的地方即可.咱们是想对方法进行插桩,自然就得关心visitMethod方法,该方法会在ClassReader阅读class文件里面的方法时会回调.这里我们首先是在HelloClassVisitor的visitMethod中调用了ClassVisitor的visitMethod方法,拿到MethodVisitor对象.

而MethodVisitor是和ClassVisitor是类似的,在ClassReader阅读方法的时候会回调这个类里面的visitParameter(访问方法参数),visitAnnotationDefault(访问注解的默认值),visitAnnotation(访问注解)等等.

所以为了能够对方法插桩,咱们需要再包一层,自己实现一下MethodVisitor,我们将ClassWriter.visitMethod返回的MethodVisitor传入自定义的MethodVisitor,并在方法刚开始的地方进行插桩.AdviceAdapter是一个继承自MethodVisitor的类,它能够方便的回调方法进入(onMethodEnter)和方法退出(onMethodExit). 我们只需要在方法进入,也就是onMethodEnter方法里面进行插桩即可.

class HelloMethodVisitor extends AdviceAdapter {

HelloMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {

super(api, methodVisitor, access, name, descriptor)

}

//方法进入

@Override

最后

目前已经更新的部分资料:



《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

而MethodVisitor是和ClassVisitor是类似的,在ClassReader阅读方法的时候会回调这个类里面的visitParameter(访问方法参数),visitAnnotationDefault(访问注解的默认值),visitAnnotation(访问注解)等等.

所以为了能够对方法插桩,咱们需要再包一层,自己实现一下MethodVisitor,我们将ClassWriter.visitMethod返回的MethodVisitor传入自定义的MethodVisitor,并在方法刚开始的地方进行插桩.AdviceAdapter是一个继承自MethodVisitor的类,它能够方便的回调方法进入(onMethodEnter)和方法退出(onMethodExit). 我们只需要在方法进入,也就是onMethodEnter方法里面进行插桩即可.

class HelloMethodVisitor extends AdviceAdapter {

HelloMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {

super(api, methodVisitor, access, name, descriptor)

}

//方法进入

@Override

最后

目前已经更新的部分资料:

[外链图片转存中…(img-ISU4b1eb-1715311844508)]
[外链图片转存中…(img-Fl1nXbCa-1715311844508)]
[外链图片转存中…(img-iiaDFYeD-1715311844509)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值