前文

没根底不要紧, 我基本也是从 0 开端, 慢慢来.
提一句, 刚刚开端学习 ASM 的皮裘, 不要被字节码吓到, 本文只要最终一个末节才有字节码常识点, 先简略入门吧.

大方向:
AOP:「Aspect Oriented Program」, 面向切面编程, 完结你想做的.
OOP:「Object Oriented Program」, 面向对象编程, 承继、封装、多态,职责分配,便于类的重用等.

思想解读:
自己查找 OOP vs AOP,会发现原本 AOP 便是为了完结这种功用诞生的. 理解之后,会恍然大悟. 其实编译期插桩有好几种办法, 这儿只介绍 ASM 的办法, 看下面的图「图是抄的」:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

AOP 运用场景:
全自动埋点「无痕埋点」;
办法耗时监控;
办法替换…

常识点:
自界说 Gradle 插件;
Transform;
ASM;
Android 打包流程「了解下 AOP 是在哪个流程中发挥效果的,也能够读完本文再去看」;
字节码「只要最终一个末节有一些, 这个彻底能够结合要完结的功用去学, 先看下去便是了」;

开发环境:
Studio 版别 2022.1.1 Canary 1;
Kotlin 版别 1.6.10;
gradle 版别 7.3;
Android gradle plugin「AGP」 版别 7.2.0;

留意开发环境, gradle 不同版别间差异很大, Studio 不同版别与 Gradle 也有各种兼容问题, 不然我也踩不了那么多坑,下面就骂骂咧咧的开端吧「来 跟我念 涨薪,涨薪,涨薪,cao 卷死了」…

开端自界说 Gradle

自界说 Gradle 插件, 有三种办法, 这儿只说用起来最灵活的.
留意的点:
假如plugin 编译成功, 给其他 module 运用后, 再更改Plugin 文件 或许其他相关的 ASM 代码, 直接 publish 会不收效. 需求删去本地 maven 库房的数据, 再履行 publish.

  1. 新建 Project, 新建 Moudle「aab」, 挑选 Library, 挑选如下选项:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

  1. 删去剩余文件夹, 只留如下文件夹内容:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

  1. 修正 aab 的 build.gradle 文件, 如下:
plugins {
    id 'groovy'
    id 'maven-publish'
}
dependencies {
    implementation gradleApi()
    implementation localGroovy()
}
def group = 'com.zly.aab' //组
def versionA = '1.0.0' //版别
def artifactIdA = 'myGradlePlugin' //仅有标示
task sourceJar(type: Jar) {
    archiveClassifier.set('sources')
    from sourceSets.main.java.srcDirs
}
publishing {
    publications {
        debug(MavenPublication) {
            groupId = group
            artifactId = artifactIdA
            version = versionA
            from components.java
            artifact sourceJar
        }
    }
    // 添加库房地址
    repositories {
        // 本地库房, 会创立 repos 文件, 坐落项目根目录下
        maven { url uri('../repos') }
    }
}
  1. 装备 project 的 build.gradle, 如下:
plugins {
    id 'com.android.application' version '7.2.0-alpha07' apply false
    id 'com.android.library' version '7.2.0-alpha07' apply false
    id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
    id 'org.jetbrains.kotlin.jvm' version '1.6.10' apply false
}
task clean(type: Delete) {
    delete rootProject.buildDir
}
  1. src/main 下新建 groovy 目录, 在 groovy 下创立目录A「eg:com/zly/aab」,创立名为 Bplugin「随便起」 的 groovy 文件, 完结后如图所示:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

//这是 groovy 文件内容
package com.zly.aab
import org.gradle.api.Plugin
import org.gradle.api.Project
class BPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        println("======================================success============================> BPlugin")
    }
}
  1. src/main 下新建 resources 目录, 继续创立 META-INF/gradle-plugins 目录, 然后创立 B.properties 文件, 文件内容如下:
//效果: 声明Gradle插件的详细完结类
implementation-class=com.zly.aab.BPlugin
//com.zly.aab.BPlugin 便是 *.groovy 文件

此时, 目录应如下图:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

  1. 将插件上传到本地库房 repos 目录中, 过程如下:

找到 Android Studio 的 Preferences… 选项, 改成下图装备:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

履行 publish 命令, 可借助于刚刚的设置, 翻开 Gradle 面板, 如下:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

此时 repos 目录如下, 代表 ok 了:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

  1. 运用插件:

装备 project 的 build.gradle, 如下:

buildscript {
    repositories {
        maven {
            url uri('./repos') //指定本地maven的途径,在项目根目录下
        }
    }
    dependencies {
        classpath 'com.zly.aab:myGradlePlugin:1.0.0'
//classpath指定的途径格式如下:
//classpath '[groupId]:[artifactId]:[version]'
//不清楚能够看 aab 的 build.gradle
    }
}
plugins {
    id 'com.android.application' version '7.2.0-alpha07' apply false
    id 'com.android.library' version '7.2.0-alpha07' apply false
    id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
    id 'org.jetbrains.kotlin.jvm' version '1.6.10' apply false
}
task clean(type: Delete) {
    delete rootProject.buildDir
}

新增装备 app 的 build.gradle 文件如下:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
//引证彻底自界说插件,properties文件名称的办法
    id 'com.zly.aab'
}

履行 build 使命, 能够看到 Build 输出, 细节如图:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

至此,你的自界说 Plugin 肯定是能够用了.

先学一点 ASM, 把 Plugin 装备好

先学一点 ASM, 不然 Transform 也继续不下去… 根据 AGP 7.2.0 版别中, Transform 已被标记为 @Deprecated, 并将在 AGP 8.0 版别中被移除, 所以这儿运用 AsmClassVisitorFactory.
其他再提一点, 在这个末节结束之前不会涉及到字节码常识, 仅仅为了装备好 Plugin.

//自己看 AsmClassVisitorFactory 源码吧, 这儿把补白都省掉了
interface AsmClassVisitorFactory<ParametersT : InstrumentationParameters> : Serializable {
    @get:Nested
    val parameters: Property<ParametersT>
    @get:Nested
    val instrumentationContext: InstrumentationContext
    //有必要完结 createClassVisitor 所以我们先创立 ClassVisitor
    fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor
    //这个办法是判断当前类是否要进行扫描,由于假如一切类都要经过ClassVisitor进行扫描仍是太耗时了.
    //我们能够经过这个办法过滤掉许多我们不需求扫描的类
    fun isInstrumentable(classData: ClassData): Boolean
}
  1. 创立 groovy 同级 java 目录:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

  1. 装备 aab 的 build.gradle 如下:
implementation 'org.ow2.asm:asm-commons:9.1'
implementation "com.android.tools.build:gradle-api:7.2"
implementation "com.android.tools.build:gradle:7.2.0"//cao 这是个坑,别用「$agpVersion」,老老实实写版别号,不知道为什么
implementation 'org.jetbrains:annotations:16.0.1'
  1. Java 目录中, 界说 ClassVisitor.kt, 代码:
package com.zly.aabb;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.AdviceAdapter;
class BClassVisitor extends ClassVisitor {
    BClassVisitor(ClassVisitor nextVisitor) {
        super(Opcodes.ASM5, nextVisitor);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        // AdviceAdapter 是 MethodVisitor 的子类,运用 AdviceAdapter 能够更方便的修正办法的字节码。
        // AdviceAdapter其间几个重要办法如下:
        // void visitCode():表明 ASM 开端扫描这个办法
        // void onMethodEnter():进入这个办法
        // void onMethodExit():即将从这个办法出去
        // void onVisitEnd():表明办法扫描结束
        MethodVisitor newVisitor = new AdviceAdapter(Opcodes.ASM5, visitor, access, name, descriptor) {
            @Override
            protected void onMethodEnter() {
            //这儿未做实际修正, 打个log, 在Build 的时分能够输出
                System.out.println("access:" + access + ", name:" + name + " , descriptor:" + descriptor + " ===> onMethodEnter");
                super.onMethodEnter();
            }
            @Override
            protected void onMethodExit(int opcode) {
                System.out.println("access:" + access + ", name:" + name + " , descriptor:" + descriptor + " ===> onMethodExit opcode:$opcode   <===");
                super.onMethodExit(opcode);
            }
        };
        return newVisitor;
    }
}
  1. 在 Java 目录中, 自界说 AsmClassVisitorFactory, 代码如下:
package com.zly.aabb;
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.jetbrains.annotations.NotNull;
import org.objectweb.asm.ClassVisitor;
public abstract class BBFactory implements AsmClassVisitorFactory<InstrumentationParameters.None> {
    @NotNull
    @Override
    public ClassVisitor createClassVisitor(@NotNull ClassContext classContext, @NotNull ClassVisitor classVisitor) {
        return new BClassVisitor(classVisitor);
    }
    @Override
    public boolean isInstrumentable(@NotNull ClassData classData) {
        //先简略完结, 直接就先不过滤了.
        return true;
    }
}
  1. 修正之前界说的 BPlugin.groovy 文件:
package com.zly.aab
import com.android.build.api.instrumentation.FramesComputationMode
import com.android.build.api.instrumentation.InstrumentationScope
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.Variant
import com.zly.aabb.BBFactory
import kotlin.jvm.functions.Function1
import org.gradle.api.Plugin
import org.gradle.api.Project
class BPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        println("================success=================> BPlugin")
        //AppExtension「旧版」 VS AndroidComponentsExtension「新版」
        AndroidComponentsExtension extension = (AndroidComponentsExtension) project.getExtensions().getByType(AndroidComponentsExtension.class);
        extension.onVariants(extension.selector().all(), new Function1<Variant, Variant>() {
            @Override
            Variant invoke(Variant variant) {
                variant.getInstrumentation().transformClassesWith(BBFactory.class, InstrumentationScope.PROJECT, none -> null);
                variant.getInstrumentation().setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS);
                return variant;
            }
        })
    }
}
  1. 履行 aab 中 publish task. 为使修正收效, 需求删去 repos 文件下的内容, 注释掉 app/Project 的 build.gradle 中 aab 插件的 classpath and plugin. 再履行 publish, 翻开注释后 run app, 会看到如下日志:
    AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

至此, 外部装备结束, 能够开端相关相关事务了, 这儿就简略写, 比如计算办法耗时.

开端结合事务代码, 修正详细的字节码, 先简略打印个 log

  1. 先装置一个字节码查看插件, 我用的 Kotlin, 所以装置 ASM Bytecode Viewer Support Kotlin, 能够看到编译后的字节, 以及对应的 ASM 代码. 真正写插桩代码的时分, 能够先根据 「show differences」功用查看自己所需的 ASM 代码.

    AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

  2. 先输出个 log 体验下插桩成功后的愉悦心情:

//初始状态下 MainActivity 代码
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        lifecycleScope.launch(Dispatchers.IO) {
            getResult()
        }
    }
    suspend fun getResult() {
    }
}

对着 MainActivity 右键履行「ASM ByteCode Viewer」,再修正 MainActivity 的 getResult 代码如下:

suspend fun getResult() {
    Log.e("zly_1111","print a test log")
}
  1. 然后再「ASM ByteCode Viewer」, 点击「show difference」,展示如下:

    AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

  2. 现在修正 BClassVisitor 的 onMethodExit 如下:

@Override
protected void onMethodExit(int opcode) {
    visitLdcInsn("zly_1111");
    visitLdcInsn( "test get result");
    visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
    visitInsn(POP);
    super.onMethodExit(opcode);
}
  1. 重新 publish「留意先注释 project and app 中 build.gradle 中此插件相关的装备, 然后删去 repos 中文件, publish 成功后, 再翻开注释, 否则修正不收效」, 然后装置 app, 此时在 logcat 中便可看到 Log.

至此,你的插桩肯定是收效了.

修正详细的字节码, 再计算一下办法耗时

  1. getResult 办法修正如下 vs getResult 为空办法:
suspend fun getResult() {
    val start = System.currentTimeMillis()
    Log.e("zly_1111","进入时刻: $start")
    val end = System.currentTimeMillis()
    Log.e("zly_1111","脱离时刻差: ${end - start}")
}

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

详细内容看图上, 其他需求留意画圆圈部分, LSTORE 与 LLOAD 中的后面数字需求对应, 原因的话自己去查询吧.

  1. 详细代码完结如下:
MethodVisitor newVisitor = new AdviceAdapter(Opcodes.ASM5, visitor, access, name, descriptor) {
   int slotIndex = 0;
   @Override
   protected void onMethodEnter() {
       slotIndex = newLocal(Type.LONG_TYPE);
       visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
       visitVarInsn(LSTORE, slotIndex);
       visitLdcInsn("zly_1111");
       visitLdcInsn("\u8fdb\u5165\u65f6\u95f4: ");
       visitVarInsn(LLOAD, slotIndex);
       visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
       visitMethodInsn(INVOKESTATIC, "kotlin/jvm/internal/Intrinsics", "stringPlus", "(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;", false);
       visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
       visitInsn(POP);
       super.onMethodEnter();
   }
   @Override
   protected void onMethodExit(int opcode) {
       visitLdcInsn("zly_1111");
       visitLdcInsn( "test get result");
       visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
       visitInsn(POP);
       int slotIndex2 = newLocal(Type.LONG_TYPE);
       visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
       visitVarInsn(LSTORE, slotIndex2);
       visitLdcInsn("zly_1111");
       visitLdcInsn(name + " ----- \u79bb\u5f00\u65f6\u95f4\u5dee: ");
       //name 即当前办法对应的name
       visitVarInsn(LLOAD, slotIndex2);
       visitVarInsn(LLOAD, slotIndex);
       visitInsn(LSUB);
       visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
       visitMethodInsn(INVOKESTATIC, "kotlin/jvm/internal/Intrinsics", "stringPlus", "(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;", false);
       visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
       visitInsn(POP);
       super.onMethodExit(opcode);
   }
};
  1. publish 后 install app, 可看到如下 log:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

Demo Github : Demo Github

至此,文章主题结束.

坑记录

以下文字, 不想看能够掠过, 末尾直接点赞 end.

坑一: Stduio 和 Gradle 版别兼容问题.
原本我的 Studio 版别 2021.2.1 Canary 7, 可是在我装备完 ClassVisitor/AsmClassVisitorFactory 之后, 80% 的情况下都会报错, 导致 Build 能成功, 可是无法装置到手机, 报错如下图:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

其时很无语, 查找不到, 直到我看到 Android Developers 开发平台, 如下图:

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

尽管写着兼容, 可是我仍是翻开了之前下载的另一个 Studio 版别「2022.1.1 Canary 1」, 能装置了:)

坑二: Groovy 文件中, 调用 Kotlin 泛型相关办法.

虽然 Groovy 和 Java 彻底兼容, Java 和 Kotlin 彻底兼容, but 我遇到 Groovy 调用 Kotlin 带有泛型的类, 真真搞破了脑袋也没有把 AsmClassVisitorFactory.kt 的承继类写入 Groovy 文件中, 最终仍是新建了 Groovy 文件夹同级其他 Java 文件夹, 用 Kotlin 的写法完结的.
可是这儿有个问题便是 Java 文件夹与 Groovy 文件夹途径不能相同, 否则 Groovy 调用 Java 目录下文件时, 会报找不到 class 的bug.

AOP 面向切面编程, 字节码插桩, 自定义 Gradle Plugin + Transform + ASM「小白操作版」, 基于 AGP 7.2.0,

End :
真的从 0 开端了, 一步一步写的, 真的只适合小白.
里边许多没细讲, 仅仅搭了一个架子, 消化完之后可看 详细讲解.

参阅链接

本文后续
自界说 Gradle 插件的 3 种办法
Android Gradle 插件 API 更新,建议多看看
现在准备好告别Transform了吗? | 拥抱AGP7.0
最通俗易懂的字节码插桩实战
AOP 利器 ASM 根底入门