一直觉得字节码插桩是不可高攀的知识,最近工作不太忙,于是下决心去学习一下字节码插桩;打开我收藏了很久的文章开始学习,下面几个是我之前收藏的文章(收藏==学会)
文章中的AMS应该是ASM, 我搞错了,写文章的时候没注意,跟AMS(ActivityManagerService)搞混了一直打成AMS;在开头纠错下;
下面开始正题,就简单的实现一下在Test.java类中的test()方法中插入一些代码;由于是初体验,就不讲很难的,怎么简单怎么来。小白看完上面文章还是有点懵逼,刚开始我也是很懵逼,实践是最好的方式去掌握新的技能知识的方式,于是我就找着博客踩坑自己体验了一次ASM字节码插桩。下面我就一步一步的从0开始新建一个项目来展示字节码插桩,test方法执行前后插入一些代码。
1.新建工程,新建一个插件module
pluginams module只保留main文件夹,不需要资源目录,额外需要新建一个resource目录,后面会讲干嘛用的。
2.插件的build
apply plugin: 'kotlin'
//下面两个很重要
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.cb.test.amsplugin'
version='1.0.0'
//上传到本地maven残酷
uploadArchives {
repositories {
mavenDeployer {
//本地的Maven地址:当前工程下
repository(url: uri('./ams_plugin'))
}
}
}
3.编写一个gradle插件类
插件主要作用就是遍历整个项目的文件,包含jar包
```
package com.cb.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
//去掉不需要的.class文件
if (name.endsWith(".class") && name != "R.class"
&& !name.startsWith("R$") && name != "BuildConfig.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)
}
}
}
}
TestClassVisitor后面会将
4.给插件命名
之前不是新建了一个resource文件夹吗,在里面在创建一个META-INF文件夹然后在新建一个gradle-plugins文件夹,然后新建一个 properties 文件 如 com.cb.test.amsplugin.properties;
然后在文件中指定下TestMethodPlugin插件plugin的绝对路径
implementation-class=com.cb.test.pluginams.TestMethodPlugin
5.自定义一个MethodVisitor
package com.cb.test.pluginams
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.commons.AdviceAdapter
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() {
println("$TAG onMethodEnter")
super.onMethodEnter()
//this-方法接收的参数-方法内定义的局部变量
//下面代码不需要自己写 有工具可以生成
mv.visitLdcInsn("Test.class ")
mv.visitLdcInsn("aaa start")
mv.visitMethodInsn(
INVOKESTATIC, "android/util/Log", "d",
"(Ljava/lang/String;Ljava/lang/String;)I", false)
mv.visitInsn(POP)
}
/**
* 在方法调用之后插入,注意在 super.onMethodExit(opcode) 之前
*/
override fun onMethodExit(opcode: Int) {
mv.visitLdcInsn("Test.class ")
mv.visitLdcInsn("aaa end")
mv.visitMethodInsn(
INVOKESTATIC, "android/util/Log", "d",
"(Ljava/lang/String;Ljava/lang/String;)I", false)
mv.visitInsn(POP)
println("$TAG onMethodExit")
super.onMethodExit(opcode)
}
}
除了字节码部分其他的代码没什么好说的,都好理解,这部分代码也不需要自己写,可以在Android Stuido中搜索AMS bytecode Viewer插件。
6.使用ASM bytecode Viewer 生成相应的字节码
新建一个Test.java
public class Test {
public static final String TAG = "Test.class ";
void test() {
}
}
右键代码区
然后
复制下这里的代码,后面要用;
然后在Test.java中,编写想插入的代码
public class Test {
public static final String TAG = "Test.class ";
void test() {
Log.d(TAG, "aaa start");
Log.d(TAG, "aaa end");
}
}
在重复ASM bytecode Viewer步骤,拿到字节码,复制保存下,使用diff对比工具找出两次字节码的区别,如图
找到不同之处,复制下来,
从代码中可以看到多了两次Log 前四行对应 Log.d(TAG, "aaa start"); 后面四行对应 Log.d(TAG, "aaa end");
这就和第5步匹配上了。
7.编写ClassVisitor
package com.cb.test.pluginams
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
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")
//这里写死了Test.java 读者可以自定义
//注意这里是全路径 拷贝过来的是com.cb.test.amstest.Test.class 注意不能用.要换成/
if (className == "com/cb/test/amstest/Test" && methodName == "test") {
//返回我们自定义的MethodVisitor
return CustomizeMethodVisitor(api, methodVisitor, className, methodAccess, methodName, methodDescriptor)
}
return methodVisitor
}
}
至此插件代码就写完了
8.生成插件
点击 uploadArchives 等待编译,编译完成会生成插件
9.引入插件
在项目的build.gradle中引入生成的插件
buildscript {
ext.kotlin_version = "1.5.0"
repositories {
google()
mavenCentral()
//这里
maven {
url uri('./pluginams/ams_plugin')
}
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//这里
classpath "com.cb.test.amsplugin:pluginams:1.0.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
注意一点就是com.cb.test.amsplugin:pluginams:1.0.0 命名规则,就是查看生成插件中的 maven-metadata.xml文件中的 groupId+artifactId+version
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>com.cb.test.amsplugin</groupId>
<artifactId>pluginams</artifactId>
<versioning>
<release>1.0.0</release>
<versions>
<version>1.0.0</version>
</versions>
<lastUpdated>20220129085728</lastUpdated>
</versioning>
</metadata>
然后在app里的build.gradle中引入插件
这里的id命名规则就是第四步.properties之前的部分
个人感觉这里命名规则有点恶心,命名不对编译就过不了,想要试试的同学一定要主要每个步骤的命名不是随便命名的。
10.运行验证
Test test = new Test();
//调用时将 之前获取字节码的代码删除,test是空方法
test.test();
查看logcat打印
2022-01-29 17:11:30.560 14149-14149/com.cb.test.amstest D/Test.class: aaa start
2022-01-29 17:11:30.560 14149-14149/com.cb.test.amstest D/Test.class: aaa end
完美,然后查看下Test.java生成的Test.class文件是否插入成功
插入成功,至此简单的Asm字节码插桩就完成了。