百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

Android Transform + ASM 初探(android studio for arm)

toyiye 2024-08-24 00:37 5 浏览 0 评论

背景

随着项目中对 APM (Application Performance Management) 越来越关注,诸如像 Debug 日志,运行耗时监控等都会陆陆续续加入到源码中,随着功能的增多,这些监控日志代码在某种程度上会影响甚至是干扰业务代码的阅读,笔者于是查阅有没有一些可以自动化在代码中插入日志的方法,“插桩”就映入眼帘了,本质的思想都是 AOP,在编译或运行时动态注入代码。本文选了一种在编译期间修改字节码的方法,实现在方法执行前后插入日志代码的方式进行一些初步的试探,目的旨在学习这个流程。

概述

交待完背景后,先对接下来要讲的内容做一个简要的说明。因为是编译期间搞事情,所以首先要在编译期间找一个时间点,这也就是标题前半部分 Transform 的内容;找到“作案”地点后,接下来就是“作案对象”了,这里选择的是对编译后的 .class 字节码下手,要到的工具就是后半部分要介绍的 ASM 了。至此,希望读者能对本文要讲的内容有一个初步的印象了。

Transform



image.png

官方出品的编译打包签名流程,我们要搞事情的位置就是 Java Compiler 编译成 .class Files 之到打包为 .dex Files 这之间。Google 官方在 Android Gradle 的 1.5.0 版本以后提供了 Transfrom API, 允许第三方自定义插件在打包 dex 文件之前的编译过程中操作 .class 文件,所以这里先要做的就是实现一个自定义的 Transform 进行.class文件遍历拿到所有方法,修改完成对原文件进行替换。

下面说一下如何引入 Transform 依赖,在 Android gradle 插件 1.5 版本以前,是有一个单独的 transform api 的;从 2.0 版本开始,就直接并入到 gradle api 中了。

Gradle 1.5:

Compile ‘com.android.tools.build:transfrom-api:1.5.0’

Gradle 2.0 开始:

implementation 'com.android.tools.build:gradle-api:3.0.1'

每个 Transform 其实都是一个 Gradle task,他们链式组合,前一个的输出作为下一个的输入,而我们自定义的 Transform 是作为第一个 task 最先执行的。

本文是基于 buildSrc 的方式定义 Gradle 插件的,因为只在 Demo 项目中应用,所以 buildSrc 的方式就够了。需要注意一点的是,buildSrc 方式要求 library module 的名称必须为 buildSrc,在实现中注意一下。

废话少说,直接上图:

buildSrc module:



在 buildSrc 中自定义一个基于 Groovy 的插件



[图片上传中...(image.png-d105c7-1556521723794-0)]

在主项目 App 的 build.gradle 中引入自定义的 AsmPlugin

apply plugin: AsmPlugin

最后,在 settings.gradle 中加入 buildSrc module

include ':app', ':buildSrc'

至此,我们就完成了一个自定义的插件,功能十分简陋,只是在控制台输出 “hello gradle plugin",让我们编译一下看看这个插件到底有没有生效。


好了,看到控制台的输出表明我们自定义的插件生效了,“作案地方”就此埋伏完毕。

后面会定义一个 AsmTransform,注册到 AsmPlugin 中,具体代码会在介绍 ASM 的时候贴出来。

ASM

有了搞事情的时机,怎么去修改字节码呢?此时神器 ASM 就出场了。

ASM 是一个功能比较齐全的 Java 字节码操作与分析框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接 产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类的行为。

更多细节可以去 ASM 官网 看看。

笔者写 Demo 的时候最新的版本是 7.0。

ASM 提供一种基于 Visitor 的 API,通过接口的方式,分离读 class 和写 class 的逻辑,提供一个 ClassReader 负责读取class字节码,然后传递给 Class Visitor 接口,Class Visitor 接口提供了很多 visitor 方法,比如 visit class,visit method 等,这个过程就像 ClassReader 带着 ClassVisitor 游览了 class 字节码的每一个指令。

光有读还不够,如果我们要修改字节码,ClassWriter 就出场了。ClassWriter 其实也是继承自 ClassVisitor 的,所做的就是保存字节码信息并最终可以导出,那么如果我们可以代理 ClassWriter 的接口,就可以干预最终生成的字节码了。

好,还是废话少说,直接上代码。

先看一下插件目录的结构



这里新建了 AsmTransform 插件,以及 class visitor 的 adapter(TestMethodClassAdapter),使得在 visit method 的时候可以调用自定义的 TestMethodVisitor。

同时,buildSrc 的 build.gradle 中也要引入 ASM 依赖

// ASM 相关
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-util:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'

下面先来看一下 AsmTransform

import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import me.sure.asm.TestMethodClassAdapter
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
class AsmTransform extends Transform {
 Project project
 AsmTransform(Project project) {
 this.project = project
 }
 @Override
 void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
 super.transform(transformInvocation)
 println("===== ASM Transform =====")
 println("${transformInvocation.inputs}")
 println("${transformInvocation.referencedInputs}")
 println("${transformInvocation.outputProvider}")
 println("${transformInvocation.incremental}")
 //当前是否是增量编译
 boolean isIncremental = transformInvocation.isIncremental()
 //消费型输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
 Collection<TransformInput> inputs = transformInvocation.getInputs()
 //引用型输入,无需输出。
 Collection<TransformInput> referencedInputs = transformInvocation.getReferencedInputs()
 //OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
 TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
 for (TransformInput input : inputs) {
 for (JarInput jarInput : input.getJarInputs()) {
 File dest = outputProvider.getContentLocation(
 jarInput.getFile().getAbsolutePath(),
 jarInput.getContentTypes(),
 jarInput.getScopes(),
 Format.JAR)
 //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了 
 transformJar(jarInput.getFile(), dest)
 }
 for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
 println("== DI = " + directoryInput.file.listFiles().toArrayString())
 File dest = outputProvider.getContentLocation(directoryInput.getName(),
 directoryInput.getContentTypes(), directoryInput.getScopes(),
 Format.DIRECTORY)
 //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
 //FileUtils.copyDirectory(directoryInput.getFile(), dest)
 transformDir(directoryInput.getFile(), dest)
 }
 }
 }
 @Override
 String getName() {
 return AsmTransform.simpleName
 }
 @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
 }
 private static void transformJar(File input, File dest) {
 println("=== transformJar ===")
 FileUtils.copyFile(input, dest)
 }
 private static void transformDir(File input, File dest) {
 if (dest.exists()) {
 FileUtils.forceDelete(dest)
 }
 FileUtils.forceMkdir(dest)
 String srcDirPath = input.getAbsolutePath()
 String destDirPath = dest.getAbsolutePath()
 println("=== transform dir = " + srcDirPath + ", " + destDirPath)
 for (File file : input.listFiles()) {
 String destFilePath = file.absolutePath.replace(srcDirPath, destDirPath)
 File destFile = new File(destFilePath)
 if (file.isDirectory()) {
 transformDir(file, destFile)
 } else if (file.isFile()) {
 FileUtils.touch(destFile)
 transformSingleFile(file, destFile)
 }
 }
 }
 private static void transformSingleFile(File input, File dest) {
 println("=== transformSingleFile ===")
 weave(input.getAbsolutePath(), dest.getAbsolutePath())
 }
 private static void weave(String inputPath, String outputPath) {
 try {
 FileInputStream is = new FileInputStream(inputPath)
 ClassReader cr = new ClassReader(is)
 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES)
 TestMethodClassAdapter adapter = new TestMethodClassAdapter(cw)
 cr.accept(adapter, 0)
 FileOutputStream fos = new FileOutputStream(outputPath)
 fos.write(cw.toByteArray())
 fos.close()
 } catch (IOException e) {
 e.printStackTrace()
 }
 }
}

我们的 InputTypes 是 CONTENT_CLASS, 表明是 class 文件,Scope 先无脑选择 SCOPE_FULL_PROJECT

在 transform 方法中主要做的事情就是把 Inputs 保存到 outProvider 提供的位置去。生成的位置见下图:



对照代码,主要有两个 transform 方法,一个 transformJar 就是简单的拷贝,另一个 transformSingleFile,我们就是在这里用 ASM 对字节码进行修改的。

关注一下 weave 方法,可以看到我们借助 ClassReader 从 inputPath 中读取输入流,在 ClassWriter 之前用一个 adapter 进行了封装,接下来就让我们看看 adapter 做了什么。

public class TestMethodClassAdapter extends ClassVisitor implements Opcodes {
 public TestMethodClassAdapter(ClassVisitor classVisitor) {
 super(ASM7, classVisitor);
 }
 @Override
 public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
 MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
 return (mv == null) ? null : new TestMethodVisitor(mv);
 }
}

这个 adapter 接收一个 classVisitor 作为输入(即 ClassWriter),在 visitMethod 方法时使用自定义的 TestMethodVisitor 进行访问,再看看 TestMethodVisitor:

public class TestMethodVisitor extends MethodVisitor {
 public TestMethodVisitor(MethodVisitor methodVisitor) {
 super(ASM7, methodVisitor);
 }
 @Override
 public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
 System.out.println("== TestMethodVisitor, owner = " + owner + ", name = " + name);
 //方法执行之前打印
 mv.visitLdcInsn(" before method exec");
 mv.visitLdcInsn(" [ASM 测试] method in " + owner + " ,name=" + name);
 mv.visitMethodInsn(INVOKESTATIC,
 "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
 mv.visitInsn(POP);
 super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
 //方法执行之后打印
 mv.visitLdcInsn(" after method exec");
 mv.visitLdcInsn(" method in " + owner + " ,name=" + name);
 mv.visitMethodInsn(INVOKESTATIC,
 "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
 mv.visitInsn(POP);
 }
}

TestMethodVisitor 重写了 visitMethodInsn 方法,在默认方法前后插入了一些 “字节码”,这些字节码近似 bytecode,可以认为是 ASM 格式的 bytecode。具体做的事情其实就是分别输出了两条日志:

Log.i("before method exec", "[ASM 测试] method in" + owner + ", name=" + name);
Log.i("after method exec", "method in" + owner + ", name=" + name);

话说这么啰哩啰嗦的写一堆就是干这么点儿事儿啊,写起来也太麻烦了吧。

别担心,ASM 提供了一款的插件,可以转化源码为 ASM bytecode。地址在这里

找一个简单的方法试一下,见下图:



左边是源码,test 方法也是只打了一条日志,右图是插件翻译出来的“ASMified” 代码,如果想看 bytecode,也是有的哈。

最后让我们看看编译后的 AsmTest.class 变成了什么样



可以看到,不单在 test() 方法中原本的日志前后新加入日志,连构造函数方法前后都加了,这是因为对 visitorMethod 方法没有进行任何区分和限制,所以任何方法调用前后都被“插桩”了。

创作不易喜欢请点击+关注哦



相关推荐

# Python 3 # Python 3字典Dictionary(1)

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值(key=>value)对用冒号(:)分割,每个对之间用逗号(,)分割,整个字典包括在花括号({})中,格式如...

Python第八课:数据类型中的字典及其函数与方法

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值...

Python中字典详解(python 中字典)

字典是Python中使用键进行索引的重要数据结构。它们是无序的项序列(键值对),这意味着顺序不被保留。键是不可变的。与列表一样,字典的值可以保存异构数据,即整数、浮点、字符串、NaN、布尔值、列表、数...

Python3.9又更新了:dict内置新功能,正式版十月见面

机器之心报道参与:一鸣、JaminPython3.8的热乎劲还没过去,Python就又双叒叕要更新了。近日,3.9版本的第四个alpha版已经开源。从文档中,我们可以看到官方透露的对dic...

Python3 基本数据类型详解(python三种基本数据类型)

文章来源:加米谷大数据Python中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。在Python中,变量就是变量,它没有类型,我们所说的"类型"是变...

一文掌握Python的字典(python字典用法大全)

字典是Python中最强大、最灵活的内置数据结构之一。它们允许存储键值对,从而实现高效的数据检索、操作和组织。本文深入探讨了字典,涵盖了它们的创建、操作和高级用法,以帮助中级Python开发...

超级完整|Python字典详解(python字典的方法或操作)

一、字典概述01字典的格式Python字典是一种可变容器模型,且可存储任意类型对象,如字符串、数字、元组等其他容器模型。字典的每个键值key=>value对用冒号:分割,每个对之间用逗号,...

Python3.9版本新特性:字典合并操作的详细解读

处于测试阶段的Python3.9版本中有一个新特性:我们在使用Python字典时,将能够编写出更可读、更紧凑的代码啦!Python版本你现在使用哪种版本的Python?3.7分?3.5分?还是2.7...

python 自学,字典3(一些例子)(python字典有哪些基本操作)

例子11;如何批量复制字典里的内容2;如何批量修改字典的内容3;如何批量修改字典里某些指定的内容...

Python3.9中的字典合并和更新,几乎影响了所有Python程序员

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

Python3大字典:《Python3自学速查手册.pdf》限时下载中

最近有人会想了,2022了,想学Python晚不晚,学习python有前途吗?IT行业行业薪资高,发展前景好,是很多求职群里严重的香饽饽,而要进入这个高薪行业,也不是那么轻而易举的,拿信工专业的大学生...

python学习——字典(python字典基本操作)

字典Python的字典数据类型是基于hash散列算法实现的,采用键值对(key:value)的形式,根据key的值计算value的地址,具有非常快的查取和插入速度。但它是无序的,包含的元素个数不限,值...

324页清华教授撰写【Python 3 菜鸟查询手册】火了,小白入门字典

如何入门学习python...

Python3.9中的字典合并和更新,了解一下

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

python3基础之字典(python中字典的基本操作)

字典和列表一样,也是python内置的一种数据结构。字典的结构如下图:列表用中括号[]把元素包起来,而字典是用大括号{}把元素包起来,只不过字典的每一个元素都包含键和值两部分。键和值是一一对应的...

取消回复欢迎 发表评论:

请填写验证码