在Android进阶宝典 – Handler应用于线上卡顿监控中,我简单介绍了一下关于ASM实现字节码插桩来实现方法耗时的监控,但是当时只是找了一个特定的class文件,针对某个特定的方法进行插桩,但是真正的开发中不可能这么做的,因为整个工程中会有成百上千的方法,而且存储的位置也各有不同,这个时候,我们就需要借助gradle插件来实现ASM字节码插桩。

1 准备工作

但凡涉及到gradle开发,我一般都是会在buildSrc文件夹下进行,还有没有伙伴不太了解buildSrc的,其实buildSrc是Android中默认的插件工程,在gradle编译的时候,会编译这个项目并配置到classpath下。这样的话在buildSrc中创建的插件,每个项目都可以引入。

在buildSrc中可以创建groovy目录(如果对groovy或者kotlin了解),也可以创建java目录,对于插件开发个人更便向使用groovy,因为更贴近gradle。

1.1 创建插件

创建插件,需要实现Plugin接口,在引入这个插件后,项目编译的时候,就会执行apply方法。

class ASMPlugin implements Plugin<Project>{@Overridevoid apply(Project project) {def ext = project.extensions.getByType(AppExtension)if (ext != null){ext.registerTransform(new ASMTransform())}}
}

在apply方法中,可以执行自定义的Task,也可以执行自定义的Transform(其实也可以看做是一种特殊的Task),这里我们自定义了插桩相关的Transform。

1.2 创建Transform

什么是Transform呢?就是在class文件打包生成dex文件的过程中,对class字节码做处理,最终生成新的dex文件,那么有什么方式能够对字节码操作呢?ASM是一种方式,使用Javassist也可以织入字节码。

class ASMTransform extends Transform {@OverrideString getName() {return "ASMTransform"}@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}@OverrideSet<QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT}@Overrideboolean isIncremental() {return false}@Overridevoid transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {inputs.each { input ->input.directoryInputs.each { dic ->/**这里会拿到两个路径,分别是java代码编译后的javac/debug/classes,以及kotlin代码编译后的 tmp/kotlin-classes/debug */println("dic path == >${dic.file.path}")/**所有的class文件的根路径,我们已经拿到了,接下来就是分析这些文件夹下的class文件*/findAllClass(dic.file)/**这里一定不能忘记写*/def dest = outputProvider.getContentLocation(dic.name, dic.contentTypes, dic.scopes, Format.DIRECTORY)FileUtils.copyDirectory(dic.file, dest)}input.jarInputs.each { jar ->/**这里也一定不能忘记写*/def dest = outputProvider.getContentLocation(jar.name,jar.contentTypes,jar.scopes,Format.JAR)FileUtils.copyFile(jar.file,dest)}}}/*** 查找class文件* @param file 可能是文件也可能是文件夹*/private void findAllClass(File file) {if (file.isDirectory()) {file.listFiles().each {findAllClass(it)}} else {modifyClass(file)}}/*** 进行字节码插桩* @param file 需要插桩的字节码文件*/private void modifyClass(File file) {println("最终的class文件 ==> ${file.absolutePath}")/**如果不是.class文件,抛弃*/if (!file.absolutePath.endsWith(".class")) {return}/**BuildConfig.class文件以及R文件都抛弃*/if (file.absolutePath.contains("BuildConfig.class") || file.absolutePath.contains("R")) {return}doASM(file)}/*** 进行ASM字节码插桩* @param file 需要插桩的class文件*/private void doASM(File file) {def fis = new FileInputStream(file)def cr = new ClassReader(fis)def cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)cr.accept(new ASMClassVisitor(Opcodes.ASM9, cw), ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG)/**重新覆盖*/def bytes = cw.toByteArray()def fos = new java.io.FileOutputStream(file.absolutePath)fos.write(bytes)fos.flush()fos.close()}
}

如果想要使用Transform,那么需要引入transform-api,其实在transform 1.5之后gradle就支持Transform了。

implementation 'com.android.tools.build:transform-api:1.5.0'

当执行Transform任务的时候,最终会执行到transform方法,在这个方法中可以获取TransformInput的输入,主要包括两种:文件夹和Jar包;对于Jar包,我们不需要处理,只需要拷贝到目标文件夹下即可。

对于文件夹我们是需要处理的,因为这里包含了我们要处理的.class文件,对于Java编译后的class文件是存在javac/debug/classes根文件夹下,对于kotlin编译后的class文件是存在temp/classes根文件下。

所以在整个编译的过程中,只要是.class文件都会执行doASM这个方法,在这个方法中就是我们在上节提到的对于字节码的插桩。

1.3 ASM字节码插桩

class ASMClassVisitor extends ClassVisitor {ASMClassVisitor(int api) {super(api)}@OverrideMethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {println("visitMethod==>$name")/**所有的方法都会在ASMMethodVisitor中插入字节码*/def method = super.visitMethod(access, name, descriptor, signature, exceptions)return new ASMMethodVisitor(api, method, access, name, descriptor)}ASMClassVisitor(int api, ClassVisitor classVisitor) {super(api, classVisitor)}@OverrideFieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {return super.visitField(access, name, descriptor, signature, value)}@OverrideAnnotationVisitor visitAnnotation(String descriptor, boolean visible) {return super.visitAnnotation(descriptor, visible)}
}
class ASMMethodVisitor extends AdviceAdapter {private def methodName/*** Constructs a new {@link AdviceAdapter}.** @param api the ASM API version implemented by this visitor. Must be one of {@link*     Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}.* @param methodVisitor the method visitor to which this adapter delegates calls.* @param access the method's access flags (see {@link Opcodes}).* @param name the method's name.* @param descriptor the method's descriptor (see {@link Type Type}).*/protected ASMMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {super(api, methodVisitor, access, name, descriptor)this.methodName = name}@Overrideprotected void onMethodEnter() {super.onMethodEnter()visitFieldInsn(GETSTATIC,"com/lay/learn/base_net/LoggUtils","INSTANCE","Lcom/lay/learn/base_net/LoggUtils;")visitMethodInsn(INVOKEVIRTUAL, "com/lay/learn/base_net/LoggUtils", "start", "()V", false)}@Overrideprotected void onMethodExit(int opcode) {super.onMethodExit(opcode)visitFieldInsn(GETSTATIC,"com/lay/learn/base_net/LoggUtils","INSTANCE","Lcom/lay/learn/base_net/LoggUtils;")visitLdcInsn(methodName)visitMethodInsn(INVOKEVIRTUAL, "com/lay/learn/base_net/LoggUtils", "end", "(Ljava/lang/String;)V",false)}
}

这里就不再细说了,贴上源码大家可以借鉴一下哈。

最终在编译的过程中,对所有的方法插入了我们自己的耗时计算逻辑,当运行之后

class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)}
}

虽然我们没有显示地在MainActivity的onCreate中插入耗时检测代码,但是在控制台中我们可以看到,onCreate方法耗时180ms

2022-12-28 19:50:19.243 13665-13665/com.lay.learn.asm E/LoggUtils: <init> 耗时==>0
2022-12-28 19:50:19.458 13665-13665/com.lay.learn.asm E/LoggUtils: onCreate 耗时==>180

1.4 插件配置

当我们完成一个插件之后,需要在META-INF文件夹下创建一个gradle-plugins文件夹,并在properties文件中声明插件全类名。

implementation-class=com.lay.asm.ASMPlugin

要注意插件id就是properties文件的名字。

这样只要某个工程中需要字节码插桩,只需要引入asm_plugin这个插件即可在编译的时候扫描整个工程。

plugins {id 'com.android.application'id 'org.jetbrains.kotlin.android'id 'asm_plugin'
}

附上buildSrc中的gradle配置文件

plugins{id 'groovy'
}repositories {google()mavenCentral()
}dependencies {implementation gradleApi()implementation localGroovy()implementation  'org.apache.commons:commons-io:1.3.2'implementation "com.android.tools.build:gradle:7.0.3"implementation 'com.android.tools.build:transform-api:1.5.0'implementation 'org.ow2.asm:asm:9.1'implementation 'org.ow2.asm:asm-util:9.1'implementation 'org.ow2.asm:asm-commons:9.1'
}java {sourceCompatibility = JavaVersion.VERSION_1_8targetCompatibility = JavaVersion.VERSION_1_8
}

最后需要说一点就是,在Transform任务执行时,一定要将文件夹或者jar包传递到下一级的Transform中,否则会导致apk打包时缺少文件导致apk无法运行

作者:Vector7
链接:https://juejin.cn/post/7182178552207376421

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。

相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

二、源码解析合集


三、开源框架合集


欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

Android 字节码插桩全流程解析相关推荐

  1. Android字节码插桩

    什么是字节码插桩 字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加. 简单来讲,我们要实现无埋点对客户端的全量统计.这里的统计概括的范围比较广泛, ...

  2. 关于android字节码插桩

    转自:https://www.jianshu.com/p/c202853059b4 基于字节码插桩可以实现面向切面的编程, 实际是在字节码中插入要执行的相关程序. 通过非侵入的方式实现切面编程. (1 ...

  3. 【字节码插桩】Android 打包流程 | Android 中的字节码操作方式 | AOP 面向切面编程 | APT 编译时技术

    文章目录 一.Android 中的 Java 源码打包流程 1.Java 源码打包流程 2.字符串常量池 二.Android 中的字节码操作方式 一.Android 中的 Java 源码打包流程 Ja ...

  4. Android AOP之字节码插桩

    背景   本篇文章基于<网易乐得无埋点数据收集SDK>总结而成,关于网易乐得无埋点数据采集SDK的功能介绍以及技术总结后续会有文章进行阐述,本篇单讲SDK中用到的Android端AOP的实 ...

  5. Android程序员的硬通货——ASM字节码插桩

    作者:享学课堂Lance老师 转载请声明出处! 一.什么是插桩 QQ空间曾经发布的<热修复解决方案>中利用 Javaassist库实现向类的构造函数中插入一段代码解决 CLASS_ISPR ...

  6. 美团热修复Robust源码庖丁解牛(第一篇字节码插桩)

    如果你想对java编译后的class文件做一些手脚的话,市面上有供你选择的asm.javassist.aspectJ(aop面向切面编程)等等,一般修改class文件的用途有你想统计一些东西,例如ap ...

  7. 调研字节码插桩技术,用于系统监控设计和实现

    作者:小傅哥 博客:https://bugstack.cn ❝ 沉淀.分享.成长,让自己和他人都能有所收获!???? ❞ 目录 一.来自深夜的电话! 二.准备工作 三.使用 AOP 做个切面监控 1. ...

  8. aop 获取方法入参出参_ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称及入参和出参结果并记录方法耗时...

    作者:小傅哥 博客:bugstack.cn ❝ 沉淀.分享.成长,让自己和他人都能有所收获! ❞ 一.前言 在我们实际的业务开发到上线的过程中,中间都会经过测试.那么怎么来保证测试质量呢?比如:提交了 ...

  9. 看完这一篇,你也可以自如地掌握字节码插桩

    /   今日科技快讯   / 近日,一些国家的黑客频繁对俄罗斯发动网络攻击,以阻止它们正常运行.未来几天,俄罗斯可能与全球互联网断开.针对网络威胁,俄罗斯政府准备启动自己的"大局域网&quo ...

最新文章

  1. 深度学习Pytorch框架Tensor张量
  2. 华北电力大学计算机导论试题,保定华北电力大学计算机与科学大一课程
  3. 系统变量与环境变量的关系
  4. Sql Server实用操作-存储过程精解
  5. cpu序列号能告诉别人嘛_微信这个开关不删除,别人手机能随意登录你的微信,学会告诉家人...
  6. Linux学习笔记4-CentOS7中redis3.2.9安装教程
  7. 委托、事件的个人理解
  8. python正十三边形_一起学python-opencv十三(直方图反向投影和模板匹配)
  9. python链表实现栈_使用python实现数组、链表、队列、栈
  10. 90-30-020-源码-任务调度-Kylin任务调度
  11. Vmware Workstation常用操作和常见问题
  12. Python 3 实现插入排序
  13. matlab与信道编码,基于MATLAB的信道编码.doc
  14. tf.nn.tanh 双曲正切曲线
  15. 画洗碗机器人的思维导图_赞!三年级小学生画出这样的思维导图
  16. VSCode Remote SSH 过程试图写入的管道不存在
  17. China‘s Housing Market Economy Is Crumbling
  18. mPEG-SS 甲氧基PEG琥珀酰亚胺丁二酸酯
  19. 去除人声--安装和使用spleeter分离人声和背景声
  20. Python Unet网络结构pytorch简单实现+torchsummary可视化(可以直接运行)

热门文章

  1. 腾讯云,短信sdk接入,vue2中使用
  2. 字符串匹配算法(BF、KMP)
  3. Android 蓝牙打印小票与WiFi打印小票两种打印方式的实现(带有图片和二维码)
  4. Revit 求质心(重心点)
  5. 为家长解答青少儿编程常见问题
  6. 查看jks证书文件内容
  7. arduino下载库出错_arduino运行程序出现这样的错误是为什么?急求解
  8. SpringBoot+Vue实现酒店客房管理系统
  9. 虚拟机连接物理机的打印机_deepin下virtualbox虚拟windows 7系统安装物理打印机的方法...
  10. assign()函数