前文
没根底不要紧, 我基本也是从 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 根底入门