了解 Gradle,Android Transform 与“字节码插桩“

了解 Gradle,Android Transform 与"字节码插桩"

  • 本文基于 Gradle 6.5,AGP(Android Gradle Plugin) 4.1.3
  • Demo 下载
前言

说起 gradle-pluginAndroid-Transform字节码插桩 你是否会觉得这听起来很牛逼或者很高深?

其实并没有你想的那么难。本文将带你了解,并实现一个简单的 “方法耗时监控” 插件。

在正文开始前,我们可以先通过下图大致了解下这三者的关系,正文会依据该层次逐步讲解:
在这里插入图片描述

了解 Gradle
Gradle 是什么

What is Gradle

Gradle 是一个灵活而强大的构建工具,当你第一次开始使用它时,你会很容易感到害怕。但是,理解以下的核心原则会让 Gradle 变得更加平易近人,你会在知道它之前就熟练地使用它。

  1. Gradle 是一个关注灵活性和性能的开源构建自动化工具。Gradle 构建脚本可使用 Groovy 或 Kotlin DSL 来编写。Gradle 的本质是一个运行在 JVM 中的程序。因此你必须安装 JDK 才能使用它。

  2. Gradle 的核心模型基于任务(Task)
    Gradle 模型会将任务构建成有向无环图(DAG),这意味着 Build 的本质是配置了一组 Task,并根据它们的依赖关系将它们连接起来并创建 DAG。一旦创建出任务图,Gradle 就会确定哪些任务需要按照哪个顺序运行,然后继续执行它们。

Gradle 构建过程的三个阶段
  1. 初始化阶段(Initialization)

    Gradle 支持单项目和多项目构建。在初始化阶段,Gradle 会决定哪些项目将参与构建,并为每个项目创建一个 Project 实例。(工程本身是一个 Project,工程中每个子 module 也是一个 Project)

  2. 配置阶段(Configuration)

    在此阶段将配置初始化时创建的 Project 对象。此时所有 Project 的构建脚本都会被执行。该过程结束后 Gradle 会确定有哪些 Task 需要被执行。

  3. 执行阶段(Execution)

    执行配置阶段选中的 Task。
    在这里插入图片描述

了解 Gradle Project

Project 其实是 Gradle 和构建脚本交互的主要中介。在 Initialization 阶段 Gradle 即会为每个构建脚本准备其各自的 Project 对象。构建脚本通过 Project 提供的 API 即可控制 Gradle 的构建行为。

执行流程

每个 module 的 build.gradle 文件在经过 Gradle 自带的 ScriptPlugin 中的编译器编译后会生成一个 .class 文件并加载到内存中。此时还处于 Initialization 阶段,只会执行 plugin 代码块和 buildscript 代码块,当这些执行完毕后会将剩下的需要执行的内容编译等到 Configuration 阶段执行。

编译流程
  • kts 脚本通过 KotlinScriptPlugin 经过 Interpreter 并通过 ResidualProgramCompiler 编译成如下类并执行

    • Initialization 阶段编译产物

      public final class Program extends ExecutableProgram.StagedProgram {
          public void execute(ExecutableProgram.Host host, KotlinScriptHost<?> kotlinScriptHost) {
              host.setupEmbeddedKotlinFor(kotlinScriptHost);
              try {
                  PluginRequestCollector pluginRequestCollector = new PluginRequestCollector(kotlinScriptHost.getScriptSource());
                  new Build_gradle(pluginRequestCollector.createSpec(1));
                  host.applyPluginsTo(kotlinScriptHost, pluginRequestCollector.getPluginRequests());
              } catch (Throwable th) {
                  host.handleScriptException(th, Build_gradle.class, kotlinScriptHost);
              }
              host.applyBasePluginsTo((Project) kotlinScriptHost.getTarget());
              // 准备 stage2
              host.evaluateSecondStageOf(this, kotlinScriptHost, "Project/TopLevel/stage2", HashCode.fromBytes(new byte[]{-41, 36, -105, -118, 78, 37, 39, -32, 61, 34, 79, 54, 56, -48, 45, -51}), host.accessorsClassPathFor(kotlinScriptHost));
          }
      
          public String getSecondStageScriptText() {
              return "除了 stage1 已经执行的剩下的需要 stage2 执行的脚本源码";
          }
      
          public CompiledScript loadSecondStageFor(ExecutableProgram.Host host, KotlinScriptHost kotlinScriptHost, String str, HashCode hashCode, ClassPath classPath) {
              return host.compileSecondStageOf(this, kotlinScriptHost, str, hashCode, ProgramKind.TopLevel, ProgramTarget.Project, classPath);
          }
      }
      public class Build_gradle extends CompiledKotlinPluginsBlock {
          public Build_gradle(PluginDependenciesSpec pluginDependenciesSpec) {
              super(pluginDependenciesSpec);
              // 执行 plugins 代码块
              PluginDependenciesSpec $this$plugins = ((CompiledKotlinPluginsBlock) this).getPluginDependencies();
              $this$plugins.id("com.android.application");
              $this$plugins.id("kotlin-android");
              Unit unit = Unit.INSTANCE;
          }
      
          public static final void main(String[] strArr) {
              RunnerKt.runCompiledScript(Build_gradle.class, strArr);
          }
      }
      
    • Configuration 阶段编译产物

      public final class Program extends ExecutableProgram {
          public void execute(ExecutableProgram.Host host, KotlinScriptHost<?> kotlinScriptHost) {
              try {
                  new Build_gradle(kotlinScriptHost, (Project) kotlinScriptHost.getTarget());
              } catch (Throwable th) {
                  host.handleScriptException(th, Build_gradle.class, kotlinScriptHost);
              }
          }
      }
      public class Build_gradle extends CompiledKotlinBuildScript {
          public final Project $$implicitReceiver0;
      
          public static final void main(String[] strArr) {
              RunnerKt.runCompiledScript(Build_gradle.class, strArr);
          }
      
          public Build_gradle(KotlinScriptHost kotlinScriptHost, Project project) {
              super(kotlinScriptHost);
              this.$$implicitReceiver0 = project;
              // 执行 android 代码块
              Accessors377twfxlhpj2n65rquy9ybeqsKt.android(this.$$implicitReceiver0, C00022.INSTANCE);
              // 执行 dependencies 代码块
              ProjectExtensionsKt.dependencies(this.$$implicitReceiver0, new Function1<DependencyHandlerScope, Unit>(this) {
                  /* class p000.Build_gradle.C00093 */
                  final /* synthetic */ Build_gradle this$0;
      
                  {
                      this.this$0 = r2;
                  }
      
                  public /* bridge */ /* synthetic */ Object invoke(Object obj) {
                      invoke((DependencyHandlerScope) obj);
                      return Unit.INSTANCE;
                  }
      
                  public final void invoke(@NotNull DependencyHandlerScope $this$dependencies) {
                      Intrinsics.checkParameterIsNotNull($this$dependencies, "$receiver");
                      ImplementationConfigurationAccessorsKt.implementation((DependencyHandler) $this$dependencies, this.this$0.fileTree(MapsKt.mapOf(new Pair[]{TuplesKt.to("dir", "libs"), TuplesKt.to("include", CollectionsKt.listOf("*.jar"))})));
                      ApiConfigurationAccessorsKt.api((DependencyHandler) $this$dependencies, "org.jetbrains.kotlin:kotlin-stdlib:1.4.10");
                      ApiConfigurationAccessorsKt.api((DependencyHandler) $this$dependencies, "androidx.core:core-ktx:1.3.2");
                      ApiConfigurationAccessorsKt.api((DependencyHandler) $this$dependencies, "androidx.appcompat:appcompat:1.2.0");
                      ApiConfigurationAccessorsKt.api((DependencyHandler) $this$dependencies, "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01");
                      ApiConfigurationAccessorsKt.api((DependencyHandler) $this$dependencies, "com.google.android.material:material:1.3.0-alpha04");
                      ApiConfigurationAccessorsKt.api((DependencyHandler) $this$dependencies, "androidx.legacy:legacy-support-v4:1.0.0");
                      ApiConfigurationAccessorsKt.api((DependencyHandler) $this$dependencies, "androidx.constraintlayout:constraintlayout:2.0.4");
                  }
              });
              Unit unit = Unit.INSTANCE;
          }
      }
      
  • groovy 脚本通过 ScriptPluginImpl 经过 ScriptCompilerImpl 编译生成 ProjectScript,并返回 ScriptRunner,ScriptPluginImpl 会调用 ScriptRunner 的 run 方法,从而调用 ProjectScript 来运行脚本

    • 由于 groovy 生成的代码量比较大,这里就不放上来了,感兴趣的同学可以通过如下方式查看
      在这里插入图片描述
了解 Gradle Plugin

简单的来说 Plugin 其实是对 Gradle 的扩展,其目的是为了简化对 Gradle 构建脚本的编写,并使的你构建更易维护。

了解 Android Transform

What is Transform

Transform 是 Google 在 AGP 中提供的 API,它允许开发者在执行 dex 打包任务前对 资源.class 这两种输入类型做处理。开发者可以通过 AGP 中的 com.android.build.gradle.BaseExtension (android { } 这个 dsl 块)来注册,但一般来说为了灵活性,会将 Transform 的逻辑封装到一个 Gradle Plugin 中注册使用。本小节将演示如何注册 Transform 并使用它。

注册 Transform

在这里插入图片描述

TransformInvocation API

在这里插入图片描述

演示 Transform

在这里插入图片描述

了解 “字节码插桩”
概念

我们经常听到的 “字节码插装” 通常指的是修改某个 class 文件的某个方法,并在其中插入新的字节码指令这一过程。通过上文我们可知,在安卓中我们可以通过 Transform 来获取到源码编译后生成的 class 文件,我们可以通过它在编译阶段给某些方法 “插装” 来对源方法进行增强,以达到我们在编写源码时难以达到的功能。

但其实 “字节码插装” 只是一种修改 class 的方式,实际上对 class 文件我们能做的不止是 “插装” 而已。

常用的字节码修改库
  • javassist 提供两套 API,分别是源码级别 API 和字节码级别 API

    • 源码级别 API 可以让你像写 Java 代码一样去修改字节码,但是难以对字节码做精细操作
    • 字节码级别 API 可以对 class 文件做更为精细的操作,但是需要对字节码有一定了解

    优点:提供的封装和抽象比较多,使用较为简单。源码级 API 可以不需要了解字节码也可以修改 class。

    缺点:运行速度相对稍慢,占用内存相对较大,目前靠个人开发者维护。

  • ASM 提供了基于访问者模式的 Core-API 和基于树形结构的 Tree-API

    • Core-API 在读取 class 的过程中即会回调各访问方法。速度非常快,占用内存小,但过于基础,对复杂操作需要进行封装。
    • Tree-API 基于 Core-API 的封装,会将访问到的元素储存为内部变量,并将其作为一个节点。速度相对 Core-API 会慢一点,但结构化后更加易于操作。

    优点:轻量,速度较快,内存占用小,由开源组织 OW2 维护,广泛的应用在 JDK,Gradle,Groovy-comiler,Kotlin-compiler 等项目中。能够保证稳定。

    缺点:API 较为基础,需要二次封装,必须要懂字节码才能使用。

如何查看字节码

了解 class 文件结构

相信大家在看了上文后已经对如何在编译时进行 “字节码插装” 有了基本的了解,本小节提供几种我常用的查看字节码方式,来帮助大家认识 class 文件。

  • 通过 javap 工具

在这里插入图片描述

  • 通过 IDEA 插件:ASM Bytecode Outline

在这里插入图片描述

  • 使用 010Editor

在这里插入图片描述

实战:方法耗时监控

在有了上面的基础后,我们来实战一下,使用 ASM 来做一个方法执行耗时的监控功能。

国际惯例,先上效果图:

在这里插入图片描述

如何设计
  1. 在方法开始时需要记录一个时间戳

  2. 在方法结束时也记录一个时间戳,这样就可以采用 endTime - startTime 来计算出调用耗时

  3. 结合 1,2 两点可得出需要如下类来辅助

在这里插入图片描述

  1. 通过某种方式将 TimeMonitor.start 插入方法开头,TimeMonitor.end 插入方法末尾
需要学习的字节码基础
  • 类&接口名:类和接口名称总是以一种称为二进制名称的完全限定形式表示。如:com/ysj/stu/gradle/TimeMonitor
  • 方法:在一个类中,通过方法名和方法描述即可确定一个唯一方法。如:start(Ljava/lang/String;)V
  • invokestatic 指令:调用一个类中的静态方法的指令,该指令需要一个常量池中的方法引用索引,方法引用中包含了方法的名称和描述。
  • ldc 指令:该指令会从常量池中取一个值。
使用 ASM-CORE API 实现
class TimeMonitorClassVisitor(cw: ClassWriter) : ClassVisitor(Opcodes.ASM7, cw) {

    private lateinit var className: String

    // 访问类时回调
    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(
        access: Int,
        name: String,
        descriptor: String,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor = object : MethodVisitor(
        api,
        cv.visitMethod(access, name, descriptor, signature, exceptions)
    ) {
        // 方法开始时回调
        override fun visitCode() {
            if (className == "com/ysj/stu/gradle/TimeMonitor") return super.visitCode()
            // 方法的参数
            mv.visitLdcInsn("${className}-${name}-${descriptor}")
            // 调用 TimeMonitor.start
            mv.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                "com/ysj/stu/gradle/TimeMonitor",
                "start",
                "(Ljava/lang/String;)V",
                false
            )
        }

        override fun visitInsn(opcode: Int) {
            if (className == "com/ysj/stu/gradle/TimeMonitor") return super.visitInsn(opcode)
            // 在每个 return 指令前插入 TimeMonitor.end
            if (opcode in Opcodes.IRETURN..Opcodes.RETURN) {
                // 方法的参数
                mv.visitLdcInsn("${className}-${name}-${descriptor}")
                // 调用 TimeMonitor.start
                mv.visitMethodInsn(
                    Opcodes.INVOKESTATIC,
                    "com/ysj/stu/gradle/TimeMonitor",
                    "end",
                    "(Ljava/lang/String;)V",
                    false
                )
            }
            super.visitInsn(opcode)
        }
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Y-S-J

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值