问题一、什么是插桩?
用通俗的话来讲,插桩就是将一段代码通过某种策略插入到另一段代码,或替换另一段代码,即在Java字节码中去插入或者修改某些代码。
(这是一个统计方法耗时的案例)
由此可以发挥想象:
字节码插桩的好处:
1、我们开发人员在大面积改动的特定位置的代码的时候不用手动去一个个改动
2、添加或者移除大面积代码更灵活了,比方说我们第一版需要生成脏代码过审,后续版本不再需要就可以把插件给取消,或者全局埋点不再需要就可以取消插件而不是手动一个个文件修改。
3、修改也更灵活,如果大面积插入相同的内容需要修改,只需要修改插件的内容,而不用每一个文件去修改
4、在我们编写代码的时候不用直接接触这些插入的模版代码,可以保持我们编写时候代码和界面的整洁,也可以说它更加“优雅”。
问题二、插桩的使用场景是什么?
1、我们android开发同学们熟悉的ButterKnife,Dagger,Greendao这些常用的框架,就是在编译期间生成了代码,简化了我们开发同学的操作。
2、通过“字节码插桩”,我们可以全局替换目标方法的实现、增加目标方法的逻辑,这种处理方式更加通用彻底且具有兼容性,所以基于这样的能力,除了上述常用框架有使用插桩之外,我们业务开发人员可以利用插桩实现以下功能:
问题三、插桩的发生时机是什么?
从我们android开发同学非常熟悉的一张图开始讲起:
我们编写的源码(.java)通过javac编译成字节码(.class),然后通过dx/d8编译成dex文件。
题外话:什么是Dalvik 字节码?:
Android 本身是嵌入式平台类型的一种,为了优化性能,Android 虚拟机运行的是 Dex 文件。dex 我们可以理解为 Android 为移动设备(受限于早年的手机配置远低于 PC) 研发的 class 的压缩格式。Android SDK 工具包里面有 dx 工具可以将 class 文件打包成 dex。又由 Android 虚拟机的 PathClassLoader 装载到内存中。
(为非android同学简单解释)
言归正传:插桩的时机就是:在.class转为.dex之前,修改.class文件
上图是我们项目内在用的 ORM 映射数据库 Greendao。可见 build 目录下有很多 *.java 后缀的文件,build一般都是放置编译生成后的产物,很显然这些文件就是在我们 build 时候通过注解处理器产生的 Java 文件。
(类似的,ButterKnife、Greendao等框架也是在编译期间去帮我们生成代码)
问题四、实现Android插桩有哪些方案?
常见的插桩方案: AspectJ、Javassist、ASM
插桩方案的对比:
AspectJ
AspectJ 作为一个老牌的插桩框架优点是: 1 成熟稳定 2 使用简单 3 能提高代码的可维护性和复用性。但是对应的缺点是:由于其基于规则,所以其切入点相对固定,开发者对代码的掌控度较小。还有就是如果我们要实现对所有方法进行插桩,代码注入后的性能也是我们需要关注的一个重要的点,我们希望只插入我们想插入的代码,而AspectJ会额外生成一些包装代码,对性能以及包大小有一定影响。AspectJX
Javassist
Javassist 源代码级 API 比 ASM 中实际的字节码操作更容易使用。Javassist 在复杂的字节码级操作上提供了更高级别的抽象层。Javassist 源代码级 API 只需要很少的字节码知识,甚至不需要任何实际字节码知识,因此实现起来更容易、更快。Javassist使用反射机制,这使得它比运行时使用 Classworking 技术的ASM慢。Javassist
ASM
相比 AspectJ,ASM 更加直接高效。但是对于一些复杂情况,我们可能需要使用另外一种 Tree API 来完成对 Class 文件更直接的修改,因此这时候我们要掌握一些必不可少的 Java 字节码知识,ASM 的特点是功能强大操作灵活,但是上手的难度也会比 AspectJ 更难,(当然了现在我们也可以借助字节码翻译插件来辅助我们编写java字节码)但是它能获得更好的性能,更适合大面积的插桩场景。ASM
(上述几个问题解决了插桩是什么的问题,下面开始解决怎么做的问题)
问题五、实现Android插桩需要做哪些准备?
阶段1:groovy语言基础
阶段2:Gradle 基础
阶段3:ASM的使用
下图即字节码插桩的实现所需要做的准备内容:
阶段一:Groovy基础知识的学习
1.1 、集合
Groovy 中也有集合的概念,主要看一下常用的 List、Map,下面将对 List 和 Map 常用操作进行介绍。
那么如何在 Groovy 中定义 List 呢,Groovy 中的 List 的定义方式类似于 Java 中的数组,具体操作参考下面代码:
task list{ //定义List def idlist = [1,2,3,4,5,6]; def nameList = ['安1','安2','安3','安6','安8','安9','安10']; println "idlist的类型:"+idlist.class println "nameList的类型:"+nameList.class //访问集合里面的元素 println '第一个元素:'+list[0]//访问第一个元素 println '第二个元素:'+list[1]//访问第二个元素,以此类推 println '最后一个元素:'+list[-1]//访问最后一个元素 println '倒数第二个元素:'+list[-2]//访问倒数第二个元素,以此类推 println '某个范围内元素:'+list[2..4]//访问某个范围内元素,以此类推 //使用each遍历集合中的元素 nameList.each{ //类似于我们kotlin,使用it作为迭代的元素变量 println it } }
下面是输出结果:
PS User:\Documents\Gradle\study\Groovy\ListMap> gradle list > Configure project : list的类型:class java.util.ArrayList nameList的类型:class java.util.ArrayList 第一个元素:1 第二个元素:2 最后一个元素:6 倒数第二个元素:5 某个范围内元素:[3, 4, 5] 安1 安2 安3 安6 安8 安9 安10 BUILD SUCCESSFUL in 2s
那么如何在 Groovy 中定义 Map 呢,Groovy 中的 Map 当然也是键值对,具体定义及操作参考下面代码:
task map{ //定义Map def map = ['name':'安9技术部', 'id':9]; println "map的类型:"+map.getClass().name; //访问Map里面的元素 println map.name; println map['name']; //遍历Map中的元素 map.each{ println "Key:${it.key},value:${it.value}" } }
输出结果:
PS User:\Documents\Gradle\study\Groovy\ListMap> gradle map
> Configure project :
map的类型:java.util.LinkedHashMap
安9技术部
安9技术部
Key:name,value:安9技术部
Key:id,value:9BUILD SUCCESSFUL in 2s
1.2 、方法
Groovy 中的方法和 Java 中的方法类似,只是写法上更加灵活,Groovy 中 return 不是必须的,在不写 return 的时候,Groovy 会将最后一句代码作为该方法的返回值。代码块指的是一段被花括号包围的代码,Groovy 中可将代码块作为一个参数进行传递,可以参考前面关于集合的遍历部分,参考代码如下:
task method{ //方法调用 methodA(1, 2) methodA 1, 2 //获取方法返回的结果 def a = methodA 10, 20 println '获取方法返回的结果:'+a //代码块作为参数传递 def list = [1,2,3,4,5]; list.each( //闭包参数 { // println it } ) //Groovy规定,如果方法的最后一个参数是闭包,可以直接放到方法外面 list.each(){ // println it } //简写方式 list.each{ println it } } //方法的定义 def methodA(int a, int b){ println a + b //Groovy中return语句不是必须的,默认将最后一句代码的结果作为返回值 a + b }
下面是上述代码参考如下:
PS User:\Documents\Gradle\study\Groovy\Method> gradle method > Configure project : 3 3 30 获取方法返回的结果:30 1 2 3 4 5 BUILD SUCCESSFUL in 2s
1.3 、javaBean
Groovy 中的 JavaBean 相较 Java 中的比较灵活,可以直接使用 javaBean.属性的方式获取和修改 JavaBean 的属性值,无需使用相应的 Getter、Setter 方法,直接看代码
task javaBean{ //Groovy中定义JavaBean Employee employee = new Employee() employee.name = "Groovy" employee.age = 10 employee.setName("Gradle") println "名字是:"+employee.name //不能调用Getter方法获取值 // println "名字是:"+employee.getName println "年龄是:${employee.age}" println "id是:"+employee.id } class Employee{ private String name private int age private int id //定义的Getter方法所对应的属性可以直接调用 public String getId(){ 100 } //属性的Getter、Setter方法 public String setName(String name){ this.name = name } public void getName(){ name } }
下面是上述代码的执行结果:
PS User:\Documents\Gradle\study\Groovy\JavaBean> gradle javaBean > Configure project : 名字是:Gradle 年龄是:10 id是:100 BUILD SUCCESSFUL in 2s
1.4、闭包:
闭包是大多数脚本语言具有的一个特性,闭包就是一个使用花括号包围的代码块,下面来学习 Groovy 中的闭包,主要有两部分:闭包及闭包参数传递和闭包委托。
闭包及其参数传递:
task closure{ //自定义闭包的执行 mEach{ println it } //向闭包传递参数 mEachWithParams{m,n -> //m,n ->将闭包的参数和主体区分离开来 println "${m} is ${n}" } } //1.定义一个方法,参数closure用于接收闭包 //2.闭包的执行就是花括号里面代码的执行 //3.闭包接收的参数就是闭包参数closure参数中的i,如果是一个参数默认就是it变量 def mEach(closure){ for(int i in 1..5){ closure(i) } } //向闭包传递参数 def mEachWithParams(closure){ def map = ["name":"Groovy","id":100] map.each{ closure(it.key, it.value) } }
上面代码中定义了闭包以及如何进行闭包的参数的传递,当闭包只有一个参数时,默认就是 it,反之闭包有多个参数时,就需要将参数定义出来,具体可参考上述代码,下面是执行结果:
PS User:\Documents\Gradle\study\Groovy\Closure> gradle delegate > Configure project : 1 2 3 4 5 name is Groovy id is 100 BUILD SUCCESSFUL in 2s
闭包委托:
Groovy 闭包的强大之处在于它支持闭包方法的委托,Groovy 的闭包有三个属性:thisObject、owner、delegate,当在一个闭包中调用定义的方法时,由这三个属性来确定该方法由哪个对象来执行,默认 owner 和 delegate 是相等的,其中 delete 是可以被修改的,Gradle 中闭包的很多功能都是通过修改 delegate 来实现的。下面通过定义一个闭包以及方法,通过打印来说明这三个属性的一些区别:
//闭包的委托 task delegate{ new Delegate().test{ //Groovy闭包的三个属性:thisObject、owner、delegate println "thisObject:${thisObject.getClass()}" println "owner:${owner.getClass()}" println "delegate:${delegate.getClass()}" //闭包默认it println "闭包默认it:"+it.getClass() //定义的方法,优先使用thisObject来处理 method() //闭包中的方法 it.method() } } def method(){ println "mththod in root:${this.getClass()}" } class Delegate{ def method(){ println "mththod in Delegate:${this.getClass()}" } //闭包 def test(Closure<Delegate> closure){ closure(this); } }
下面是上述代码的执行结果,参考如下:
PS User:\Documents\Gradle\study\Groovy\Closure> gradle delegate > Configure project : thisObject:class build_3ajca04o1rprxygcsq0ajvt7i owner:class build_3ajca04o1rprxygcsq0ajvt7i$_run_closure2 delegate:class build_3ajca04o1rprxygcsq0ajvt7i$_run_closure2 闭包默认it:class Delegate mththod in root:class build_3ajca04o1rprxygcsq0ajvt7i mththod in Delegate:class Delegate BUILD SUCCESSFUL in 2s
当在闭包中调用方法 method() 时,发现是 thisObject 调用了 method() 方法,而不是 owner 或 delegate,说明闭包中优先使用 thisObject 来处理方法的执行,同时可以看到 owner 和 delegate 是一致的,但是 owner 比 delegate 的优先级要高,所以闭包中方法的处理顺序是:thisObject > owner > delegate。
Gradle 中一般会指定 delegate 为当前的 it,这样我们将可以通过 delegate 指定的对象来操作 it 了,下面指定闭包的 delegate 并设置委托优先,让委托的具体对象来执行其方法,下面是测试代码:
task student{ configStudent{ println "当前it:${it}" name = "Groovy" age = 10 getInfo() } } class Student{ String name int age def getInfo(){ println "name is ${name}, age is ${age}" } } def configStudent(Closure<Student> closure){ Student student = new Student() //设置委托对象为当前创建的Student实例 closure.delegate = student //设置委托模式优先,如果不设置闭包内方法的处理者是thisObject closure.setResolveStrategy(Closure.DELEGATE_FIRST) //设置it变量 closure(student) }
下面是上述代码的执行结果,参考如下:
PS E:\Gradle\study\Groovy\Closure> gradle student > Configure project : 当前it:Student@18f6d755 name is Groovy, age is 10 BUILD SUCCESSFUL in 2s
总结: 学习 Groovy 的目的还是为了加深对 Gradle 构建工具的理解,上面通过四个方面和一个案例对 Groovy 有了初步的认识,后续如果有需要在看 Groovy 的高级用法。
阶段2:Gradle 基础知识:
2.1、Gradle生命周期:
2.1.1、初始化阶段:
Gradle 构建过程可以分为三个不同的阶段,每个阶段具有特定的功能和任务。
在这个阶段,Gradle确定要参与构建的项目,并为每个项目创建一个Project实例。这是构建的开始阶段,Gradle根据项目根目录下的settings.gradle
文件来确定哪些项目参与构建。
settings.gradle
文件指定了构建所需的项目结构。在这个阶段,我们可以配置项目的包含关系和层次结构,以便Gradle知道有哪些子项目需要构建。
2.1.2、配置阶段:
在配置阶段,Gradle对每个项目对象进行配置。这意味着执行构建脚本,其中包含了所有参与构建的项目的配置信息。在这个阶段,您可以定义任务(Tasks)、依赖关系、构建规则和其他构建配置。
每个子项目的build.gradle
文件在这个阶段被解析,Gradle会创建所有项目所属的任务(Tasks)以及它们之间的依赖关系,并生成一个任务有向图,这个图形表示了任务执行的顺序和依赖关系。
2.1.3、执行阶段:
在执行阶段,Gradle确定要执行的任务子集。这个子集由传递给Gradle命令以及当前目录中指定的任务名称参数来确定。Gradle根据配置阶段创建和配置的任务列表,依次执行每个选定的任务。
在执行阶段,Gradle会执行从根项目到子项目的构建过程,逐个执行任务并满足任务之间的依赖关系。这是构建过程的实际执行阶段。
执行具体的task,如clean。注:gradle同步的时候不会触发执行阶段生命周期
通过这三个阶段,Gradle 可以完成从项目初始化到任务执行的全过程,提供了灵活而强大的构建工具。清晰地理解这些阶段的功能有助于更好地使用和配置 Gradle 构建系统。
2.2、Gradle Task:
在Gradle中,Task(任务)是构建过程的基本单位。每个Task代表一个构建阶段或操作,它可以执行编译、复制文件、运行测试等各种构建任务。任务是Gradle构建脚本中最重要的组成部分之一。
以下从这几个方面介绍Task:
2.2.1、创建Task/定位Task
//创建task的几种方式 tasks.register("taskName"){ } task taskName{ } task "taskName"{ } tasks.create("taskName"){ } //获取task的几种方式 tasks.findByName("") tasks.findByPath("") tasks.named("").get()
2.2.2、自定义Task
1、继承DefaultTask。 2、声明task执行方法,方法名随意,必须加上@TaskAction注解。 3、创建自定义task的时候 指定task类名。4、Gradle 自带的有 Delete、Copy、Zip 等等。 可以使用自定义类继承 DeaflutTask:
/**
* 自定义task
*/
class MyTask extends org.gradle.api.DefaultTask {
//方法名随意 只要加上TaskAction注解
@org.gradle.api.tasks.TaskAction
void run() {
println("自定义task 执行")
}
}
task myTask(type: MyTask)
2.2.3、Task 顺序
设置依赖的task,被依赖的task会在该task执行之前被执行
举个例子: task A(dependsOn:[B]){ .. } 表示任务 A 依赖于任务 B,那么 B 执行在 A 之前。
task A{ println "test A" doLast{ println "test A-doLast" } } //这里表示任务B依赖任务A,在A执行后才会执行B task B(dependsOn:[A]){ println "test B" doLast{ println "test B-doLast" } } 输出: > Configure project :app test A test B > Task :app:A test A-doLast > Task :app:B test B-doLast
动态依赖:
//构建任务依赖时动态的依赖其它任务 tasks.whenTaskAdded { theTask-> if(theTask.name == "packageDebug"){ theTask.dependsOn "get" } } task get{ doFirst{ println "get doFirst" } }
2.2.4、关于Task的输入输出
Gradle 支持一种叫做 up-to-date 检查的功能,也就是常说的增量构建。Gradle 的 Task 会把每次运行的结果缓存下来,当下次运行时,会检查输出结果有没有变更,如果没有变更则跳过运行,这样可以提高 Gradle 的构建速度。 图中表示一个 java 编译的 task,它的输入有2种,一是 JDK 版本号,一是源文件,它的输出结果为 class 文件,只要 JDK 版本号与源文件有任何变动,最终编译出的 class 文件肯定是不同的。当我们执行过一次编译任务后,再次运行该 task ,如果发现它的输入没有任何改变,那么它编译后的结果肯定也是不会变化的,可以直接从缓存里获取输出,这样 Gradle 会标识该 Task 为 UP-TO-DATE,进而跳过该 Task 的执行。
2.2.5、Task案例
一个简单的写文件案例:
class WriteTask extends DefaultTask {
@TaskAction
void write() {
println("begin write...")
//输入文件
def inFile = inputs.files.singleFile
println(inFile)
//输出文件
def outFile = outputs.files.singleFile
println(outFile)
outFile.createNewFile()
//write操作
outFile.text = inFile.text
}
}
task writeTask(type: WriteTask) {
//输入文件,例如build.gradle
inputs.file(file("build.gradle"))
//输出文件
outputs.file(file("text.txt"))
}
以计算Activity onCreate()耗时任务为例配置Gradle插件
下面我们以统计项目所有Activity的onCreate()方法的耗时为例,说明如何使用我们所自定义的Gradle:
1.新建工程,新建一个插件module
注:在 resource文件夹里面在创建一个META-INF文件夹然后在新建一个gradle-plugins文件夹,然后新建一个 properties 文件 如 com.anjiu.yiyuan.amsplugin.properties;
然后在文件中指定下TestMethodPlugin插件plugin的绝对路径
2.配置plugin的build.gradle
apply plugin: 'kotlin'
//要额外引入这两个plugin:
apply plugin: 'groovy'
apply plugin: 'maven'
repositories {
mavenCentral()
}
dependencies {
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
//引入asm框架内容
implementation 'org.ow2.asm:asm:7.2'
implementation 'org.ow2.asm:asm-commons:7.2'
implementation 'org.ow2.asm:asm-analysis:7.2'
implementation 'org.ow2.asm:asm-util:7.2'
implementation 'org.ow2.asm:asm-tree:7.2'
implementation 'com.android.tools.build:gradle:4.1.2', {
exclude group:'org.ow2.asm'
}
}
//group和version在后面引用自定义插件的时候会用到
group='com.anjiu.test.asm_plugin'
version='1.0.0'
//上传到本地maven仓库
uploadArchives {
repositories {
mavenDeployer {
//本地的Maven地址:当前工程下
repository(url: uri('./asm_plugin'))
}
}
}
3.编写一个gradle插件类
Gradle 引入了 Transform API,语序第三方插件在 .class 被转换为 dex 文件之前对 .class 文件进行处理。每一个 transform 都是一个 Gradle Task,有用输入和输出,上一个 transform 的输出就是下一个 transform 的输入:
(通俗易懂地说,就是gradle提供了一个让我们遍历所有文件的api,这个api叫 Transform,我们插入代码的时候通过和这个api对所有文件进行asm插桩)
插件主要作用就是遍历整个项目的文件,包含jar包
那我们以此为例,来看下我们插桩插件中Tranform的逻辑:
package com.yiyuan.test.pluginams
import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.AppExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import java.io.FileOutputStream
class TestMethodPlugin : Transform(), Plugin<Project> {
override fun apply(target: Project) {
val appExtension = target.extensions.getByType(AppExtension::class.java)
appExtension.registerTransform(this)
}
override fun getName(): String {
return "TestMethodPlugin"
}
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
return TransformManager.CONTENT_CLASS
}
override fun isIncremental(): Boolean {
return false
}
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
return TransformManager.SCOPE_FULL_PROJECT
}
override fun transform(transformInvocation: TransformInvocation?) {
val inputs = transformInvocation?.inputs
val out = transformInvocation?.outputProvider
inputs?.forEach { transformInput ->
//项目目录 遍历项目目录
transformInput.directoryInputs.forEach { directoryInput ->
if (directoryInput.file.isDirectory) {
FileUtils.getAllFiles(directoryInput.file).forEach {
val file = it
val name = file.name
//找到所有以Activity结尾的class文件(要注意这里不是.kotlin或者是.java文件而是.class文件)
if (name.endsWith("Activity.class") {
//找到需要的。class文件,进行一系列读写操作
val classPath = file.absolutePath
val cr = ClassReader(file.readBytes())
val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
val visitor = TestClassVisitor(cw)
cr.accept(visitor, ClassReader.SKIP_FRAMES)
val byte = cw.toByteArray();
val fos = FileOutputStream(classPath)
fos.write(byte)
fos.close()
}
}
}
val dest = out?.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY
)
FileUtils.copyDirectoryToDirectory(directoryInput.file, dest)
}
//jar包
transformInput.jarInputs.forEach {
val dest = out?.getContentLocation(
it.name,
it.contentTypes,
it.scopes,
Format.JAR
)
FileUtils.copyFile(it.file, dest)
}
}
}
}
4.生成插件
在gradle工具类里执行我们的上述编写的插件
5.引入插件
buildscript { ext.kotlin_version = "1.5.0" repositories { google() mavenCentral() //在这里引入我们的插件 maven { url uri('./pluginasm/asm_plugin') } } dependencies { classpath "com.android.tools.build:gradle:4.1.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" //在这里引入我们的插件 classpath "com.anjiu.test.asm_plugin:pluginasm:1.0.0" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } }
注意一点就是com.anjiu.yiyuan.asmplugin:pluginams:1.0.0 命名规则,就是查看生成插件中的 maven-metadata.xml文件中的 groupId+artifactId+version
<?xml version="1.0" encoding="UTF-8"?> <metadata> <groupId>com.anjiu.yiyuan.asmplugin</groupId> <artifactId>pluginams</artifactId> <versioning> <release>1.0.0</release> <versions> <version>1.0.0</version> </versions> <lastUpdated>20240510085728</lastUpdated> </versioning> </metadata>
自动生成的文件名如下:
com.anjiu.yiyuan.amsplugin pluginams 1.0.0 1.0.0 20240510085728
(关于gradle的内容较多,这节我们现讲插桩中会涉及的内容)
阶段3 关于ASM的知识:
3.1、类访问者 ClassReader
顾名思义:这是ASM为我们提供的访问的要修改的的类的对象
通常我们在使用 ASM 的访问者模式有一个模板代码,如下所示:
InputStream is = new FileInputStream(classFile); // 1 ClassReader classReader = new ClassReader(is); // 2 ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); // 3 ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter); // 4 classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
首先,在注释1处,我们 将目标文件转换为流的形式,并将它融入类读取器 ClassReader 之中。然后,在注释2处,我们 构建了一个类写入器 ClassWriter。接着,在注释3处,新建了一个自定义的类访问器,这个自定义的 ClassVisitor
的作用是为了在每一个方法的开始和结尾处插入相应的记时代码,以便统计出每一个方法的耗时。最后,在注释4处,类读取器 ClassReader 实例这个被访问者调用了自身的 accept 方法接收了一个 classVisitor 实例。 现在,让我们再回到上述注释4处的代码,在这里,我们调用了 classReader 的 accept 方法接收了一个访问者 classVisitor
3.2、类读取(解析)者 ClassVisitor
当然它还有一个子类AdviceAdapter,对其进行了封装、拓展,使开发者更方便去操作,我们在实现的时候继承于该类实现我们自定义的adviceAdapter,重写onMethodEnter()和onMethodExit()方法,即可实现在指定的方法进入/退出时进行代码的插入或者修改,(当然,需要通过字节码去进行修改,这里推荐使用插件 ASM ByteCode Viewer 查看字节码,以便编写对应java/kotlin代码的字节码。
我们简单了解一下 方法签名:
特殊字符 | 数据类型 | 特殊说明 |
---|---|---|
V | void | 一般用于表示方法的返回值 |
Z | boolean | |
B | byte | |
C | char | |
S | short | |
I | int | |
J | long | |
F | float | |
D | double | |
[ | 数组 | 以[开头,配合其他的特殊字符,表示对应数据类型的数组,几个[表示几维数组 |
L | 全类名; | 引用类型 以 L 开头 ; 结尾,中间是引用类型的全类名 |
例如这个指令:
INVOKESTATIC java/lang/System.currentTimeMillis ()J
我们在进行插桩的时候,需要对照表,System是一个对象,因此需要在最前边加上L, ()J表示一个方法的签名标识。 修改完成的代码就是这样的:
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
当然我们不必花太多精力在学习字节码和方法签名上,有插件可以帮我做这一步,让我们来看下:
首先,安装ASM ByteCode Viewer插件:
下面我们来看看如何去通过ASM ByteCode Viewer 查看字节码,如何把我们要插入的java/kotlin代码转换成字节码:
刚开始的时候,我们可以在 Application 的 onCreate 方法 先写下要插桩之后的代码,然后通过插件转换成对应的字节码,具体操作如下所示:
@Override
public void onCreate() {
long startTime = System.currentTimeMillis();
super.onCreate();
long endTime = System.currentTimeMillis() - startTime;
StringBuilder sb = new StringBuilder();
sb.append("com/sample/asm/SampleApplication.onCreate time: ");
sb.append(endTime);
Log.d("MethodCostTime", sb.toString());
}
重写AdviceAdapter(即把我们生成的字节码放到onMethodEnter()和onMethodExit()中):
class CustomizeMethodVisitor(
api: Int,
methodVisitor: MethodVisitor,
className: String?,
access: Int,
name: String?,
descriptor: String?
) : AdviceAdapter(api, methodVisitor, access, name, descriptor) {
private var mClassName: String? = className
private val TAG = "${this.javaClass.simpleName}: "
/**
* 在方法调用之前插入
*/
override fun onMethodEnter() {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); // 1 timeLocalIndex = newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, timeLocalIndex);
}
/**
* 在方法调用之后插入,注意在 super.onMethodExit(opcode) 之前
*/
override fun onMethodExit(opcode: Int) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LLOAD, timeLocalIndex);
// 此处的值在栈顶
mv.visitInsn(LSUB);
// 因为后面要用到这个值所以先将其保存到本地变量表中
mv.visitVarInsn(LSTORE, timeLocalIndex);
int stringBuilderIndex = newLocal(Type.getType("java/lang/StringBuilder"));
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
// 需要将栈顶的 stringbuilder 指针保存起来否则后面找不到了
mv.visitVarInsn(Opcodes.ASTORE, stringBuilderIndex);
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitLdcInsn(className + "." + methodName + " time:");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitInsn(Opcodes.POP);
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitVarInsn(Opcodes.LLOAD, timeLocalIndex);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitInsn(Opcodes.POP);
mv.visitLdcInsn("Geek");
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
// 注意: Log.d 方法是有返回值的,需要 pop 出去
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
// 2
mv.visitInsn(Opcodes.POP);
}
}
重写ClassVisitor:
class TestClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, classVisitor) {
private val TAG = "PluginAmsTag: "
private var className: String? = null
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
className = name
}
override fun visitMethod(
methodAccess: Int,
methodName: String?,
methodDescriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val methodVisitor = super.visitMethod(methodAccess, methodName, methodDescriptor, signature, exceptions)
println("$TAG method = $methodName")
println("$TAG className = $className")
//如果是Activity,且是onCreate()返回插入统计方法耗时的Visitor
if (className.endsWith("Activity") && methodName == "onCreate") {
return CustomizeMethodVisitor(api, methodVisitor, className, methodAccess, methodName, methodDescriptor)
}
//如果是Activity,且是onDestroy()返回插入测试Log的Visitor
if (className.endsWith("Activity") && methodName == "onDestroy") {
return CustomizeMethodVisitor2(api, methodVisitor, className, methodAccess, methodName, methodDescriptor)
}
return methodVisitor
}
}
验证结果:
(运行插件后 在目录app/build/intermediates/javac/包名 下可以看到插入的代码,或者通过反编译软件查看)
通过以上步骤就可以完成android的ASM插桩,要实现无痕埋点、统计方法耗时等其他特定需求的只需替换插入日志内容的代码即可。