AMS字节码插桩

一直觉得字节码插桩是不可高攀的知识,最近工作不太忙,于是下决心去学习一下字节码插桩;打开我收藏了很久的文章开始学习,下面几个是我之前收藏的文章(收藏==学会)

下面开始正题,就简单的实现一下在Test.java类中的test()方法中插入一些代码;由于是初体验,就不讲很难的,怎么简单怎么来。小白看完上面文章还是有点懵逼,刚开始我也是很懵逼,实践是最好的方式去掌握新的技能知识的方式,于是我就找着博客踩坑自己体验了一次AMS字节码插桩。下面我就一步一步的从0开始新建一个项目来展示字节码插桩,test方法执行前后插入一些代码。

项目github下载地址

1.新建工程,新建一个插件module

微信截图_20220129155221.png

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()
    //引入ams
    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.使用AMS bytecode Viewer 生成相应的字节码

新建一个Test.java

public class Test {

    public static final String TAG = "Test.class ";

    void test() {
    }

} 

右键代码区

微信截图_20220129162619.png

然后

image.png

复制下这里的代码,后面要用;

然后在Test.java中,编写想插入的代码

public class Test {

    public static final String TAG = "Test.class ";

    void test() {
        Log.d(TAG, "aaa start");
        
        Log.d(TAG, "aaa end");
    }

} 

在重复AMS bytecode Viewer步骤,拿到字节码,复制保存下,使用diff对比工具找出两次字节码的区别,如图

diff对比工具

微信截图_20220129163342.png

找到不同之处,复制下来,

微信截图_20220129163542.png

从代码中可以看到多了两次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.生成插件

微信截图_20220129164819.png

点击 uploadArchives 等待编译,编译完成会生成插件

微信截图_20220129165743.png

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中引入插件

微信截图_20220129170741.png

这里的id命名规则就是第四步.properties之前的部分

微信截图_20220129170841.png

个人感觉这里命名规则有点恶心,命名不对编译就过不了,想要试试的同学一定要主要每个步骤的命名不是随便命名的。

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文件是否插入成功

微信截图_20220129171346.png

插入成功,至此简单的AMS字节码插桩就完成了。

最后

按照国际惯例,给大家分享一套十分好用的Android进阶资料:《全网最全Android开发笔记》。

整个笔记一共8大模块、729个知识点,3382页,66万字,可以说覆盖了当下Android开发最前沿的技术点,和阿里、腾讯、字节等等大厂面试看重的技术。

图片

图片

因为所包含的内容足够多,所以,这份笔记不仅仅可以用来当学习资料,还可以当工具书用。

如果你需要了解某个知识点,不管是Shift+F 搜索,还是按目录进行检索,都能用最快的速度找到你要的内容。

相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照整个知识体系编排的。

(一)架构师必备Java基础

1、深入理解Java泛型

2、注解深入浅出

3、并发编程

4、数据传输与序列化

5、Java虚拟机原理

6、高效IO

……

图片

(二)设计思想解读开源框架

1、热修复设计

2、插件化框架设计

3、组件化框架设计

4、图片加载框架

5、网络访问框架设计

6、RXJava响应式编程框架设计

……

图片

(三)360°全方位性能优化

1、设计思想与代码质量优化

2、程序性能优化

  • 启动速度与执行效率优化
  • 布局检测与优化
  • 内存优化
  • 耗电优化
  • 网络传输与数据储存优化
  • APK大小优化

3、开发效率优化

  • 分布式版本控制系统Git
  • 自动化构建系统Gradle

……

图片

(四)Android框架体系架构

1、高级UI晋升

2、Android内核组件

3、大型项目必备IPC

4、数据持久与序列化

5、Framework内核解析

……

图片

(五)NDK模块开发

1、NDK开发之C/C++入门

2、JNI模块开发

3、Linux编程

4、底层图片处理

5、音视频开发

6、机器学习

……

图片

(六)Flutter学习进阶

1、Flutter跨平台开发概述

2、Windows中Flutter开发环境搭建

3、编写你的第一个Flutter APP

4、Flutter Dart语言系统入门

……

图片

(七)微信小程序开发

1、小程序概述及入门

2、小程序UI开发

3、API操作

4、购物商场项目实战

……

图片

(八)kotlin从入门到精通

1、准备开始

2、基础

3、类和对象

4、函数和lambda表达式

5、其他

……

图片

好啦,这份资料就给大家介绍到这了,有需要详细文档的小伙伴,可以微信扫下方二维码免费领取哈~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值