开发者社区> 也说Android> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

手把手带你实战 AGP 7.x ASM 字节码插桩

简介: 本文介绍了如何使用 AGP 7.0 推荐的 Transform Action API 来实现 ASM 插桩。
+关注继续查看

《手把手带你实战 AGP 7.x ASM 字节码插桩》目录.png

一、前言

字节码插桩技术在 Android 领域应用广泛,甚至在不少中高级面试中,是必问的技术面之一。它的应用场景包括但不限于:

  • 性能优化:监控函数耗时,优化线程数量。
  • 无痕埋点:不侵入业务源码,实现全量埋点。
  • 隐私合规:监控敏感方法调用,防止 App 因安全风险等原因而被下架。
  • ……

字节码插桩的本质是对字节码文件(.class)的修改。从原理上讲,利用文本编辑器手动编辑也是能修改的(笑,但实际上一般是通过各种框架来做。实现字节码插桩的框架有很多,

在 AGP 7.0 以前,通过 AGP 的 Transform API 来实现字节码插桩;从 7.0 开始,Transform API 被声明为 Deprecated,并计划在 AGP 的 8.0 版本中移除。但这并不表示无法再使用字节码插桩了,相反,有一套新的 API —— TransformAction,供我们实现这一需求。

二、目的

一句话,本文将会带大家一步一步使用 AGP 7.0 开始推荐使用的 TransformAction API,来实现 ASM 插桩。

可以学到什么

  • 如何编写一个 Gradle Plugin
  • 如何使用 TransformAction API 进行字节码插桩
  • 了解其中的一些坑,避免重蹈覆辙

需要了解什么

  • Kotlin 语法
  • Gradle 的一些知识:包括不限于文件组成结构、Composite Build

三、实战

有时我们想知道一个函数究竟有没有被执行,常见的手段有断点、手动加 Log。今天我们以 ASM 插桩的方式,在进入函数时打印一条 Log 和时间点。

假设已经有一个 Android 项目(可以新建一个),并且确保 AGP 版本大于 7.0.0。下面所用到的 Android 项目(主工程)名称为 AsmTourism。

1. 添加自定义插件工程目录

Gradle 7 引入了 Composite Build,可以让一个 Gradle 项目参与到另一个 Gradle 项目的构建当中。这里采用这种方式来实现自定义插件,而不是使用保留目录 buildSrc(会有坑)或者发布插件的方式。

1.1 新建 build-logic 目录

在项目根目录下新建 build-logic 文件夹(名字其实可以随意),并在该目录内创建 settings.gradle.kts 文件。

再把主工程的 gradle 文件夹、gradle.properties 文件拷贝到该目录。

此时的目录结构如下:
image.png

1.2 新建 convention 目录

在 build-logic 目录下,新建 convention 目录,作为插件源码的模块目录。

同时,还需要创建 Gradle 项目必备的文件 convention/build.gradle.kts 和文件夹 convention/src/main/kotlin/

此时的目录结构如下:
image.png

1.3 连接主工程和 build-logic 工程

在主工程的 settings.gradle 文件中,使用 includeBuild 将主工程 AsmTourism 和插件工程 build-logic 连接起来。

diff --git a/settings.gradle b/settings.gradle
index a777782..2b49529 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,5 @@
 pluginManagement {
+    includeBuild("build-logic")
     repositories {
         gradlePluginPortal()
         google()

1.4 小结

熟悉 Gradle 的朋友会发现,这里其实相当于是在主工程目录中,创建了一个 Gradle 工程,它有它自己的 settings.gradle.kts 文件和一个名为 convention 的模块。

Tips:如果一个目录下存在 settings.gradle.kts 文件,Gradle 会把它当作一个 Gradle 工程,而不是模块。

2. 编写 Gradle 插件

2.1 配置 build-logic/settings.gradle.kts

// 配置项目的依赖源
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}
// 将 convention 模块加入编译
include(":convention")

2.2 配置 convention/build.gradle.kts

plugins {
      // 使用 kotlin dsl 作为 gradle 构建脚本语言
    `kotlin-dsl`
}

// 配置字节码的兼容性
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

dependencies {
    val agpVersion = "7.2.2"
    val kotlinVersion = "1.7.10"
    val asmVersion = "9.3"
      // AGP 依赖
    implementation("com.android.tools.build:gradle:$agpVersion") {
        exclude(group = "org.ow2.asm")
    }
    // Kotlin 依赖 —— 插件使用 Kotlin 实现
    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") {
        exclude(group = "org.ow2.asm")
    }
    // ASM 依赖库
    implementation("org.ow2.asm:asm:$asmVersion")
    implementation("org.ow2.asm:asm-commons:$asmVersion")
    implementation("org.ow2.asm:asm-util:$asmVersion")
}

gradlePlugin {
    plugins {
          // 注册插件,这样可以在其他地方 apply
        register("LogPlugin") {
              // 注册插件的 id,需要应用该插件的模块可以通过 apply 这个 id
            id = "me.hjhl.gradle.plugin.log"
            implementationClass = "LogPlugin"
        }
    }
}

2.3 创建插件 LogPlugin

在 build-logic/convention/src/main/kotlin/ 目录下新建 LogPlugin.kt 文件,内容如下:

import org.gradle.api.Plugin
import org.gradle.api.Project

class LogPlugin : Plugin<Project> {
    companion object {
        private const val TAG = "LogPlugin"
    }

    override fun apply(target: Project) {
        log("======== start apply ========")
        log("apply target: ${target.displayName}")
        log("========  end apply  ========")
    }

    private fun log(msg: String) {
        println("[$TAG]: $msg")
    }
}

此时的目录结构如下:
image.png

2.4 app 模块中应用插件

回到 app/build.gradle 文件,在 plugins 语句块中应用该插件,如下:

diff --git a/app/build.gradle b/app/build.gradle
index 7ace7ff..aa2d937 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,7 @@
 plugins {
     id 'com.android.application'
     id 'org.jetbrains.kotlin.android'
+    id 'me.hjhl.gradle.plugin.log'
 }

 android {

sync 一下工程,如果可以在 AS 的 Build 窗口中如下输出:
image.png
则表示插件应用成功了。

2.5 小节

这一步需要重点注意插件在项目中的注册和使用。其次,需要熟悉下 Gradle 插件的编写方式 —— 从继承 Plugin<Project> 开始。

3. 实现 ASM 插桩

3.1 编写 Transform 类,实现 ClassVisitor

不需要繁琐的手段,AGP 提供了一个抽象接口 AsmClassVisitorFactory 简化了 Transform 的编写流程,我们只需要这样使用:

定义一个抽象类,实现该接口。

实现 createClassVisitorisInstrumentable 两个方法。

如下:

package me.hjhl.gradle.plugin.log

import com.android.build.api.instrumentation.AsmClassVisitorFactory
import com.android.build.api.instrumentation.ClassContext
import com.android.build.api.instrumentation.ClassData
import com.android.build.api.instrumentation.InstrumentationParameters
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes

abstract class LogTransform : AsmClassVisitorFactory<InstrumentationParameters.None> {
    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
          // 返回一个 ClassVisitor 对象,其内部实现了我们修改 class 文件的逻辑
        return object : ClassVisitor(Opcodes.ASM5, nextClassVisitor) {
            val className = classContext.currentClassData.className
              // 这里,由于只需要修改方法,故而只重载了 visitMethod 找个方法
            override fun visitMethod(
                access: Int,
                name: String?,
                descriptor: String?,
                signature: String?,
                exceptions: Array<out String>?
            ): MethodVisitor {
                val oldMethodVisitor =
                    super.visitMethod(access, name, descriptor, signature, exceptions)
                // 返回一个 MethodVisitor 对象,其内部实现了我们修改方法的逻辑
                return LogMethodVisitor(className, oldMethodVisitor, access, name, descriptor)
            }
        }
    }

    override fun isInstrumentable(classData: ClassData): Boolean {
        return true
    }
}

3.2 实现 MethodVisitor

package me.hjhl.gradle.plugin.log

import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.commons.AdviceAdapter

class LogMethodVisitor(
    private val className: String,
    nextMethodVisitor: MethodVisitor,
    access: Int,
    name: String?,
    descriptor: String?,
) : AdviceAdapter(Opcodes.ASM5, nextMethodVisitor, access, name, descriptor) {
    override fun onMethodEnter() {
          // 往栈上加载两个变量,用于后面的函数调用
        mv.visitLdcInsn("LogMethodVisitor")
        mv.visitLdcInsn("enter: $className.$name")
        // 调用 android.util.Log 函数
        mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            "android/util/Log",
            "i",
            "(Ljava/lang/String;Ljava/lang/String;)I",
            false
        )
        super.onMethodEnter()
    }

    override fun onMethodExit(opcode: Int) {
        super.onMethodExit(opcode)
    }
}

注意到,我们并没有直接继承并实现抽象类 MethodVisitor,而是继承 AdviceAdapter —— 它是继承自 MethodVisitor 的,这样的好处是简化了代码,只需要添加我们需要的逻辑即可 —— 这里我们打印了所调用方法的类及名字。

3.3 注册 Transform

原来 Transform API 是通过 AppExtension 注册的,现在 AGP 中是通过 AndroidComponentsExtension 注册 Transform。用法如下:

diff --git a/build-logic/convention/src/main/kotlin/LogPlugin.kt b/build-logic/convention/src/main/kotlin/LogPlugin.kt
index 0d71d92..dc62f26 100644
--- a/build-logic/convention/src/main/kotlin/LogPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/LogPlugin.kt
@@ -1,3 +1,7 @@
+import com.android.build.api.instrumentation.FramesComputationMode
+import com.android.build.api.instrumentation.InstrumentationScope
+import com.android.build.api.variant.AndroidComponentsExtension
+import me.hjhl.gradle.plugin.log.LogTransform
 import org.gradle.api.Plugin
 import org.gradle.api.Project

@@ -9,6 +13,15 @@ class LogPlugin : Plugin<Project> {
     override fun apply(target: Project) {
         log("======== start apply ========")
         log("apply target: ${target.displayName}")
+        val androidComponentsExtension =
+            target.extensions.getByType(AndroidComponentsExtension::class.java)
+        androidComponentsExtension.onVariants { variant ->
+            log("variant: ${variant.name}")
+            variant.instrumentation.apply {
+                transformClassesWith(LogTransform::class.java, InstrumentationScope.PROJECT) {}
+                setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)
+            }
+        }
         log("========  end apply  ========")
     }

3.4 小结

注意新 Transform API 的注册方式 AndroidComponentsExtension 和关键类 AsmClassVisitorFactory。至于它们的用法,可以查看本文提供的参考资料或搜索 ASM 相关资料了解。

4. 结果

安装并运行 App,看到如下 Log 表示插桩成功。
image.png

Tips:如果对插桩结果有疑问,或者想从字节码角度分析,可以使用 Bytecode Viewer 查看字节码。例如:https://github.com/Konloch/bytecode-viewer

5. Q & A

Q:为什么说使用 buildSrc 写插件会有坑?

A:插件工程(本文中的 build-logic/convention)中,需要依赖 AGP 以访问注册 Transform 的 API,但如果宿主模块中使用了 plugins 语句块的方式引入 AGP 插件,并且使用 buildSrc 来编写插件,则会导致 sync/编译报错。

Q:为什么在 build-logic/convention/build.gradle.kts 中,要 exclude org.ow2.asm

A:在 AGP 和 Kotlin 中,也使用到了 ASM。如果不屏蔽掉,使用时选错,会导致编译出现奇怪的报错。

Tips:使用时尤为注意,ASM 相关的类是否来自手动依赖的 org.ow2.asm 包中。

四、总结

如果使用过旧版 Transform API,会发现 TransformAction 的方式节省了非常多的模版代码,比如处理增量编译问题。这使得插件开发者能更专注于核心逻辑实现,提高效率。

源码仓库

Github:https://github.com/HJHL/AsmTourism

五、参考资料

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
使用xshell连接云服务器
通过添加云服务器的网络安全组入方向和出方向的TCP连接的22端口号,然后使用xshell连接云服务器的公网IP并输入账号和密码即可成功连接云服务器。
61 0
话不多说,赶紧带你了解JavaWeb以及Tomcat的配置和使用
JavaWeb是指所有通过Java语言编写可以通过浏览器访问的程序的总称,叫做JavaWeb。JavaWeb是基于请求和响应来开发的,今天我们一起了解JavaWeb以及Tomcat的配置和使用
30 0
最佳实践 | RDS & POLARDB归档到X-Pack Spark计算
部分RDS和POLARDB For MySQL的用户曾遇到如下场景:当一张表的数据达到几千万时,你查询一次所花的时间会变多。 这时候采取水平分表的策略,水平拆分是将同一个表的数据进行分块保存到不同的数据库中,这些数据库中的表结构完全相同。 本文将介绍如何把这些水平分表的表归档到X-Pack Spark数仓,做统一的大数据计算。
6541 0
公司内网搭建代理DNS使用内网域名代替ip地址
企业场景 一般在企业内部(科帮网),开发、测试以及预生产都会有一套供开发以及测试人员使用的网络环境。运维人员会为每套环境的相关项目配置单独的Tomcat,然后开放一个端口,以 IP+Port 的形式访问。
5931 0
UWP: 掌握编译型绑定 x:Bind
原文:UWP: 掌握编译型绑定 x:Bind 在 UWP 开发中,我们在进行数据绑定时,除了可以使用传统的绑定 Binding,也可以使用全新的 x:Bind,由于后者是在程序编译时进行初始化操作(不同于 Binding,它是在运行时创建、初始化),所以我们可以称 x:Bind 为编译型绑定,正像本文标题一样。
1518 0
java使用telnet连接交换机并管理交换机
像crt或者ssh、甚至是cmd命令中使用window的telnet命令连接交换机。都可以起到控制交换机的作用。<br> telnet说白了就是一个tcp的长连接。你向交换机输入一组命令,其实就是你使用socket连接上交换机,把你的命令out出去。<br> 如果你想看你执行的命令,返回了什么,你就是用io流直接读取socket中的长连接流中的内容即可。telnet其实就是这么简单。<
4123 0
java使用动态代理来实现AOP(日志记录)
以下内容为原创,转载时请注明链接地址:http://www.cnblogs.com/tiantianbyconan/p/3336627.html AOP(面向方面)的思想,就是把项目共同的那部分功能分离开来,比如日志记录,避免在业务逻辑里面夹杂着跟业务逻辑无关的代码。
756 0
2
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载