ASM 字节码插桩全流程解析
Posted 初一十五啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASM 字节码插桩全流程解析相关的知识,希望对你有一定的参考价值。
我们需要借助gradle插件来实现ASM字节码插桩。
1 准备工作
但凡涉及到gradle开发,我一般都是会在buildSrc文件夹下进行,还有没有伙伴不太了解buildSrc的,其实buildSrc是Android中默认的插件工程,在gradle编译的时候,会编译这个项目并配置到classpath下。这样的话在buildSrc中创建的插件,每个项目都可以引入。
在buildSrc中可以创建groovy目录(如果对groovy或者kotlin了解),也可以创建java目录,对于插件开发个人更便向使用groovy,因为更贴近gradle。
1.1 创建插件
创建插件,需要实现Plugin接口,在引入这个插件后,项目编译的时候,就会执行apply方法。
class ASMPlugin implements Plugin<Project>
@Override
void apply(Project project)
def ext = project.extensions.getByType(AppExtension)
if (ext != null)
ext.registerTransform(new ASMTransform())
在apply方法中,可以执行自定义的Task,也可以执行自定义的Transform(其实也可以看做是一种特殊的Task),这里我们自定义了插桩相关的Transform。
1.2 创建Transform
什么是Transform呢?就是在class文件打包生成dex文件的过程中,对class字节码做处理,最终生成新的dex文件,那么有什么方式能够对字节码操作呢?ASM是一种方式,使用Javassist也可以织入字节码。
class ASMTransform extends Transform
@Override
String getName()
return "ASMTransform"
@Override
Set<QualifiedContent.ContentType> getInputTypes()
return TransformManager.CONTENT_CLASS
@Override
Set<QualifiedContent.Scope> getScopes()
return TransformManager.SCOPE_FULL_PROJECT
@Override
boolean isIncremental()
return false
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException
inputs.each input ->
input.directoryInputs.each dic ->
/**这里会拿到两个路径,分别是java代码编译后的javac/debug/classes,以及kotlin代码编译后的 tmp/kotlin-classes/debug */
println("dic path == >$dic.file.path")
/**所有的class文件的根路径,我们已经拿到了,接下来就是分析这些文件夹下的class文件*/
findAllClass(dic.file)
/**这里一定不能忘记写*/
def dest = outputProvider.getContentLocation(dic.name, dic.contentTypes, dic.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(dic.file, dest)
input.jarInputs.each jar ->
/**这里也一定不能忘记写*/
def dest = outputProvider.getContentLocation(jar.name,jar.contentTypes,jar.scopes,Format.JAR)
FileUtils.copyFile(jar.file,dest)
/**
* 查找class文件
* @param file 可能是文件也可能是文件夹
*/
private void findAllClass(File file)
if (file.isDirectory())
file.listFiles().each
findAllClass(it)
else
modifyClass(file)
/**
* 进行字节码插桩
* @param file 需要插桩的字节码文件
*/
private void modifyClass(File file)
println("最终的class文件 ==> $file.absolutePath")
/**如果不是.class文件,抛弃*/
if (!file.absolutePath.endsWith(".class"))
return
/**BuildConfig.class文件以及R文件都抛弃*/
if (file.absolutePath.contains("BuildConfig.class") || file.absolutePath.contains("R"))
return
doASM(file)
/**
* 进行ASM字节码插桩
* @param file 需要插桩的class文件
*/
private void doASM(File file)
def fis = new FileInputStream(file)
def cr = new ClassReader(fis)
def cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)
cr.accept(new ASMClassVisitor(Opcodes.ASM9, cw), ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG)
/**重新覆盖*/
def bytes = cw.toByteArray()
def fos = new java.io.FileOutputStream(file.absolutePath)
fos.write(bytes)
fos.flush()
fos.close()
如果想要使用Transform,那么需要引入transform-api,其实在transform 1.5之后gradle就支持Transform了。
implementation 'com.android.tools.build:transform-api:1.5.0'
当执行Transform任务的时候,最终会执行到transform方法,在这个方法中可以获取TransformInput的输入,主要包括两种:文件夹和Jar包;对于Jar包,我们不需要处理,只需要拷贝到目标文件夹下即可。
对于文件夹我们是需要处理的,因为这里包含了我们要处理的.class文件,对于Java编译后的class文件是存在javac/debug/classes根文件夹下,对于kotlin编译后的class文件是存在temp/classes根文件下。
所以在整个编译的过程中,只要是.class文件都会执行doASM这个方法,在这个方法中就是我们在上节提到的对于字节码的插桩。
1.3 ASM字节码插桩
class ASMClassVisitor extends ClassVisitor
ASMClassVisitor(int api)
super(api)
@Override
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
println("visitMethod==>$name")
/**所有的方法都会在ASMMethodVisitor中插入字节码*/
def method = super.visitMethod(access, name, descriptor, signature, exceptions)
return new ASMMethodVisitor(api, method, access, name, descriptor)
ASMClassVisitor(int api, ClassVisitor classVisitor)
super(api, classVisitor)
@Override
FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value)
return super.visitField(access, name, descriptor, signature, value)
@Override
AnnotationVisitor visitAnnotation(String descriptor, boolean visible)
return super.visitAnnotation(descriptor, visible)
class ASMMethodVisitor extends AdviceAdapter
private def methodName
/**
* Constructs a new @link AdviceAdapter.
*
* @param api the ASM API version implemented by this visitor. Must be one of @link
* Opcodes#ASM4, @link Opcodes#ASM5, @link Opcodes#ASM6 or @link Opcodes#ASM7.
* @param methodVisitor the method visitor to which this adapter delegates calls.
* @param access the method's access flags (see @link Opcodes).
* @param name the method's name.
* @param descriptor the method's descriptor (see @link Type Type).
*/
protected ASMMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor)
super(api, methodVisitor, access, name, descriptor)
this.methodName = name
@Override
protected void onMethodEnter()
super.onMethodEnter()
visitFieldInsn(GETSTATIC,
"com/lay/learn/base_net/LoggUtils",
"INSTANCE",
"Lcom/lay/learn/base_net/LoggUtils;")
visitMethodInsn(INVOKEVIRTUAL, "com/lay/learn/base_net/LoggUtils", "start", "()V", false)
@Override
protected void onMethodExit(int opcode)
super.onMethodExit(opcode)
visitFieldInsn(GETSTATIC,
"com/lay/learn/base_net/LoggUtils",
"INSTANCE",
"Lcom/lay/learn/base_net/LoggUtils;")
visitLdcInsn(methodName)
visitMethodInsn(INVOKEVIRTUAL, "com/lay/learn/base_net/LoggUtils", "end", "(Ljava/lang/String;)V",false)
这里就不再细说了,贴上源码大家可以借鉴一下哈。
最终在编译的过程中,对所有的方法插入了我们自己的耗时计算逻辑,当运行之后
class MainActivity : AppCompatActivity()
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
虽然我们没有显示地在MainActivity的onCreate中插入耗时检测代码,但是在控制台中我们可以看到,onCreate方法耗时180ms
2022-12-28 19:50:19.243 13665-13665/com.lay.learn.asm E/LoggUtils: <init> 耗时==>0
2022-12-28 19:50:19.458 13665-13665/com.lay.learn.asm E/LoggUtils: onCreate 耗时==>180
1.4 插件配置
当我们完成一个插件之后,需要在META-INF文件夹下创建一个gradle-plugins文件夹,并在properties文件中声明插件全类名。
implementation-class=com.lay.asm.ASMPlugin
要注意插件id就是properties文件的名字。
这样只要某个工程中需要字节码插桩,只需要引入asm_plugin这个插件即可在编译的时候扫描整个工程。
plugins
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'asm_plugin'
附上buildSrc中的gradle配置文件
plugins
id 'groovy'
repositories
google()
mavenCentral()
dependencies
implementation gradleApi()
implementation localGroovy()
implementation 'org.apache.commons:commons-io:1.3.2'
implementation "com.android.tools.build:gradle:7.0.3"
implementation 'com.android.tools.build:transform-api:1.5.0'
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-util:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'
java
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
最后需要说一点就是,在Transform任务执行时,一定要将文件夹或者jar包传递到下一级的Transform中,否则会导致apk打包时缺少文件导致apk无法运行。
考虑到有一些小伙伴对Gradle 知识点可能掌握的不是很好,于是我整理了一个《Gradle 入门到精通》学习笔记,根据自己学习中所做的一些笔录来整的,主要也是方便后续好复习翻阅,省掉在去网上查找的时间,以免在度踩坑,如果大家有需要的可以 →直接通过点击此处← 进行参考学习!!!
Gradle基础
深度探索 Gradle 自动化构建技术
深入理解Gradle框架
Android Gradle Plugin 源码解析
以上是关于ASM 字节码插桩全流程解析的主要内容,如果未能解决你的问题,请参考以下文章