前文
没基础不要紧, 我基本也是从 0 开始, 慢慢来.
提一句, 刚刚开始学习 ASM 的皮毛, 不要被字节码吓到, 本文只有最后一个小节才有字节码知识点, 先简单入门吧.
大方向:
AOP:「Aspect Oriented Program」, 面向切面编程, 实现你想做的.
OOP:「Object Oriented Program」, 面向对象编程, 继承、封装、多态,职责分配,便于类的重用等.
思想解读:
自己搜索 OOP vs AOP,会发现原来 AOP 就是为了实现这种功能诞生的. 理解之后,会豁然开朗.
其实编译期插桩有好几种方式, 这里只介绍 ASM 的方式, 看下面的图「图是抄的」:
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.
- 新建 Project, 新建 Moudle「aab」, 选择 Library, 选择如下选项:
- 删除多余文件夹, 只留如下文件夹内容:
- 修改 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') }
}
}
- 配置 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
}
- src/main 下新建 groovy 目录, 在 groovy 下创建目录A「eg:com/zly/aab」,创建名为 Bplugin「随便起」 的 groovy 文件, 完成后如图所示:
//这是 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")
}
}
- src/main 下新建 resources 目录, 继续创建 META-INF/gradle-plugins 目录, 然后创建 B.properties 文件, 文件内容如下:
//作用: 申明Gradle插件的具体实现类
implementation-class=com.zly.aab.BPlugin
//com.zly.aab.BPlugin 就是 *.groovy 文件
此时, 目录应如下图:
- 将插件上传到本地仓库 repos 目录中, 步骤如下:
找到 Android Studio 的 Preferences... 选项, 改成下图配置:
执行 publish 命令, 可借助于刚刚的设置, 打开 Gradle 面板, 如下:
此时 repos 目录如下, 代表 ok 了:
- 应用插件:
配置 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 输出, 细节如图:
至此,你的自定义 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
}
- 创建 groovy 同级 java 目录:
- 配置 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'
- 在 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;
}
}
- 在 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;
}
}
- 修改之前定义的 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;
}
})
}
}
- 执行 aab 中 publish task.
为使修改生效, 需要删除 repos 文件下的内容, 注释掉 app/Project 的 build.gradle 中 aab 插件的 classpath and plugin. 再执行 publish, 打开注释后 run app, 会看到如下日志:
至此, 外部配置完毕, 可以开始关联相关业务了, 这里就简单写, 比如计算方法耗时.
开始结合业务代码, 修改具体的字节码, 先简单打印个 log
-
先安装一个字节码查看插件, 我用的 Kotlin, 所以安装 ASM Bytecode Viewer Support Kotlin, 可以看到编译后的字节, 以及对应的 ASM 代码. 真正写插桩代码的时候, 可以先根据 「show differences」功能查看自己所需的 ASM 代码.
-
先输出个 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")
}
-
然后再「ASM ByteCode Viewer」, 点击「show difference」,展示如下:
-
现在修改 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);
}
- 重新 publish「注意先注释 project and app 中 build.gradle 中此插件相关的配置, 然后删除 repos 中文件, publish 成功后, 再打开注释, 否则修改不生效」, 然后安装 app, 此时在 logcat 中便可看到 Log.
至此,你的插桩肯定是生效了.
修改具体的字节码, 再计算一下方法耗时
- 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}")
}
具体内容看图上, 另外需要注意画圆圈部分, LSTORE 与 LLOAD 中的后面数字需要对应, 原因的话自己去查询吧.
- 具体代码实现如下:
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);
}
};
- publish 后 install app, 可看到如下 log:
Demo Github : Demo Github
至此,文章主题完结.
坑记录
以下文字, 不想看可以掠过, 末尾直接点赞 end.
坑一: Stduio 和 Gradle 版本兼容问题.
本来我的 Studio 版本 2021.2.1 Canary 7, 但是在我配置完 ClassVisitor/AsmClassVisitorFactory 之后, 80% 的情况下都会报错, 导致 Build 能成功, 但是无法安装到手机, 报错如下图:
当时很无语, 搜索不到, 直到我看到 Android Developers 开发平台, 如下图:
虽然写着兼容, 但是我还是打开了之前下载的另一个 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.
End :
真的从 0 开始了, 一步一步写的, 真的只适合小白.
里面很多没细讲, 只是搭了一个架子, 消化完之后可看 详细讲解.
参考链接
本文后续
自定义 Gradle 插件的 3 种方式
Android Gradle 插件 API 更新,建议多看看
现在准备好告别Transform了吗? | 拥抱AGP7.0
最通俗易懂的字节码插桩实战
AOP 利器 ASM 基础入门