作者:享学课堂Lance老师

转载请声明出处!

一、什么是插桩

QQ空间曾经发布的《热修复解决方案》中利用 Javaassist库实现向类的构造函数中插入一段代码解决 CLASS_ISPREVERIFIED问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Robus等都利用到了插桩技术。

插桩就是将一段代码插入或者替换原本的代码。字节码插桩顾名思义就是在我们编写的源码编译成字节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代码逻辑的操作。

我们需要查看方法执行耗时,如果每一个方法都需要自己手动去加入这些内容,当不需要时也需要一个个删去相应的代码。一个、两个方法还好,如果有10个、20个得多麻烦!所以可以利用注解来标记需要插桩的方法,结合编译后操作字节码来帮助我们自动插入,当不需要时关掉插桩即可。这种AOP思想让我们只需要关注插桩代码本身。

二、字节码操作框架

上面我们提到QQ空间使用了 Javaassist来进行字节码插桩,除了 Javaassist之外还有一个应用更为广泛的 ASM框架同样也是字节码操作框架,Instant Run包括 AspectJ就是借助 ASM来实现各自的功能。

我们非常熟悉的JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。

字节码操作框架的作用在于生成或者修改Class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的Class才需要打包进入APK中。它的工作时机在上图Android打包流程中的生成Class之后,打包dex之前。

三、ASM的使用

由于 ASM具有相对于 Javassist更好的性能以及更高的灵活行,我们这篇文章以使用ASM为主。在真正利用到Android中之前,我们可以先在 Java程序中完成对字节码的修改测试。

3.1、在AS中引入ASM

ASM可以直接从 jcenter()仓库中引入,所以我们可以进入:https://bintray.com/进行搜索

点击图中标注的工件进入,可以看到最新的正式版本为:7.1。

因此,我们可以在AS中加入:

同时,需要注意的是:我们使用 testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,对我们Android中的依赖关系没有任何影响。

AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。

3.2、准备待插桩Class

在 test/java下面创建一个Java类:

public class InjectTest {public static void main(String[] args) {}
}

由于我们操作的是字节码插桩,所以可以进入 test/java下面使用 javac对这个类进行编译生成对应的class文件。

javac InjectTest.java

3.3、执行插桩

因为 main方法中没有任何输出代码,我们输入命令:javaInjectTest执行这个Class不会有任何输出。那么我们接下来利用 ASM,向 main方法中插入一开始图中的记录函数执行时间的日志输出。

在单元测试中写入测试方法

/**
* 1、准备待分析的class
*/
FileInputStream fis = new  FileInputStream
("xxxxx/test/java/InjectTest.class");
/**
* 2、执行分析与插桩
*/
//class字节码的读取与分析引擎
ClassReader cr = new  ClassReader(fis);
// 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单
ClassWriter cw = new  ClassWriter(ClassWriter.COMPUTE_FRAMES);
//分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问
cr.accept(new  ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);
/**
* 3、获得结果并输出
*/
byte[] newClassBytes = cw.toByteArray();
File file = new  File("xxx/test/java2/");
file.mkdirs();
FileOutputStream fos = new  FileOutputStream
("xxx/test/java2/InjectTest.class");
fos.write(newClassBytes);
fos.close();

关于ASM框架本身的设计,我们这里先不讨论。上面的代码会获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到 test/java2目录下。其中关键点就在于第2步中,如何进行插桩。

把class数据交给 ClassReader,然后进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数 ClassAdapterVisitor

public  class  ClassAdapterVisitor  extends  ClassVisitor {public  ClassAdapterVisitor(ClassVisitor cv) {super(Opcodes.ASM7, cv);}@Overridepublic  MethodVisitor visitMethod(int access, String name, String desc, String signature,String[] exceptions) {System.out.println("方法:" + name + " 签名:" + desc);MethodVisitor mv = super.visitMethod(access, name, desc, signature,exceptions);return  new  MethodAdapterVisitor(api,mv, access, name, desc);}
}

分析结果通过 ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此 ClassReader会将调用 ClassAdapterVisitor中对应的 visitMethodvisitAnnotationvisitField这些 visitXX方法。

我们的目的是进行函数插桩,因此重写 visitMethod方法,在这个方法中我们返回一个 MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在 MethodVisitor中进行分析与处理。

package com.enjoy.asminject.example;
import com.enjoy.asminject.ASMTest;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;
/**
* AdviceAdapter: 子类
* 对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析
*/
public  class  MethodAdapterVisitor  extends  AdviceAdapter {private  Boolean inject;protected  MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {super(api, methodVisitor, access, name, descriptor);}/**
* 分析方法上面的注解
* 在这里干嘛???
* <p>
* 判断当前这个方法是不是使用了injecttime,如果使用了,我们就需要对这个方法插桩
* 没使用,就不管了。
*
* @param desc
* @param visible
* @return
*/@Overridepublic  AnnotationVisitor visitAnnotation(String desc, Boolean visible) {if (Type.getDescriptor(ASMTest.class).equals(desc)) {System.out.println(desc);inject = true;}return  super.visitAnnotation(desc, visible);}private  int start;@Overrideprotected  void onMethodEnter() {super.onMethodEnter();if (inject) {//执行完了怎么办?记录到本地变量中invokeStatic(Type.getType("Ljava/lang/System;"),new  Method("currentTimeMillis", "()J"));start = newLocal(Type.LONG_TYPE);//创建本地 LONG类型变量//记录 方法执行结果给创建的本地变量storeLocal(start);}}@Overrideprotected  void onMethodExit(int opcode) {super.onMethodExit(opcode);if (inject){invokeStatic(Type.getType("Ljava/lang/System;"),new  Method("currentTimeMillis", "()J"));int end = newLocal(Type.LONG_TYPE);storeLocal(end);getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" +"/PrintStream;"));//分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuildernewInstance(Type.getType("Ljava/lang/StringBuilder;"));dup();invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new  Method("<init>","()V"));visitLdcInsn("execute:");invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new  Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));//减法loadLocal(end);loadLocal(start);math(SUB,Type.LONG_TYPE);invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new  Method("append","(J)Ljava/lang/StringBuilder;"));invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new  Method("toString","()Ljava/lang/String;"));invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new  Method("println","(Ljava/lang/String;)V"));}}
}

MethodAdapterVisitor继承自 AdviceAdapter,其实就是 MethodVisitor 的子类, AdviceAdapter封装了指令插入方法,更为直观与简单。

上述代码中 onMethodEnter进入一个方法时候回调,因此在这个方法中插入指令就是在整个方法最开始加入一些代码。我们需要在这个方法中插入 longs=System.currentTimeMillis();。在 onMethodExit中即方法最后插入输出代码。

@Override
protected  void onMethodEnter() {super.onMethodEnter();if (inject) {//执行完了怎么办?记录到本地变量中invokeStatic(Type.getType("Ljava/lang/System;"),new  Method("currentTimeMillis", "()J"));start = newLocal(Type.LONG_TYPE);//创建本地 LONG类型变量//记录 方法执行结果给创建的本地变量storeLocal(start);}
}

这里面的代码怎么写?其实就是 longs=System.currentTimeMillis();这句代码的相对的指令。我们可以先写一份代码

void test(){//插入的代码long s = System.currentTimeMillis();/**
*  方法实现代码....
*///插入的代码long e = System.currentTimeMillis();System.out.println("execute:"+(e-s)+" ms.");
}

然后使用 javac编译成Class再使用 javap-c查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。

安装完成之后,可以在需要插桩的类源码中点击右键:

点击ASM Bytecode Viewer之后会弹出

所以第20行代码: longs=System.currentTimeMillis();会包含两个指令: INVOKESTATICLSTORE

再回到 onMethodEnter方法中

@Override
protected  void onMethodEnter() {super.onMethodEnter();if (inject) {//invokeStatic指令,调用静态方法invokeStatic(Type.getType("Ljava/lang/System;"),new  Method("currentTimeMillis", "()J"));//创建本地 LONG类型变量start = newLocal(Type.LONG_TYPE);//store指令 将方法执行结果从操作数栈存储到局部变量storeLocal(start);}
}

onMethodExit也同样根据指令去编写代码即可。最终执行完插桩之后,我们就可以获得修改后的class数据。

四、Android中的实现

在Android中实现,我们需要考虑的第一个问题是如何获得所有的Class文件来判断是否需要插桩。Transform就是干这件事情的。

 start = newLocal(Type.LONG_TYPE);//store指令 将方法执行结果从操作数栈存储到局部变量storeLocal(start);
}

}


而 `onMethodExit`也同样根据指令去编写代码即可。最终执行完插桩之后,我们就可以获得修改后的class数据。## **四、Android中的实现**在Android中实现,我们需要考虑的第一个问题是**如何获得所有的Class文件来判断是否需要插桩**。Transform就是干这件事情的。

Android程序员的硬通货——ASM字节码插桩相关推荐

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

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

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

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

  3. 字节码插桩框架ASM(一)

    本文大纲: ams是做什么的 asm使用 1.ASM是做什么的? 简单来说,asm是用来进行字节码插桩的.什么是字节码插桩? 字节码插桩就是修改节码文件(.class). 如同 gson框架是用来做操 ...

  4. Android 字节码插桩全流程解析

    在Android进阶宝典 – Handler应用于线上卡顿监控中,我简单介绍了一下关于ASM实现字节码插桩来实现方法耗时的监控,但是当时只是找了一个特定的class文件,针对某个特定的方法进行插桩,但 ...

  5. Android AOP之字节码插桩

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

  6. 关于android字节码插桩

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

  7. 【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )

    文章目录 一." 字节码插桩 " 技术简介 二.AspectJ 插桩工具 三.ASM 插桩工具 一." 字节码插桩 " 技术简介 性能优化 , 插件化 , 热修 ...

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

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

  9. Android字节码插桩

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

最新文章

  1. 理想的计算机职业作文100,我的理想作文100字(通用30篇)
  2. 私人定制---打造属于自己的linux小系统
  3. python全栈开发 * 31知识点汇总 * 180716
  4. linux专用的opencv下载链接(持续更新)
  5. 使用 Canvas 生成公众号头图
  6. 我的osu游戏程序设计(oo)
  7. 七个帮助你处理Web页面层布局的jQuery插件
  8. Hi Windows Live Writer
  9. centos 增加分区容量
  10. ARM汇编中ldr伪指令和ldr指令(转载)
  11. qml demo分析(maskedmousearea-异形窗口)
  12. 阅读笔记-HTTP返回状态码
  13. 直流充电桩和交流充电桩有什么区别?
  14. html a标签设置背景,css利用A标签的背景可能作出很有意思的效果
  15. java可视化计时器,java – 使用计时器动画JPanel(幻灯片)
  16. Python爬取豆瓣短评
  17. C语言制作一个学生信息管理系统【附代码】
  18. 可达性分析算法与强弱引用
  19. 比较图像相似的三种方法
  20. 春哥博客 - Python+selenium自动化 - 环境搭建

热门文章

  1. STM32 电机教程 2 - 直流有刷电机转动控制
  2. Dispatch Source Timer
  3. Cesium地图暗色滤镜
  4. 移动链表中的最大值到尾部
  5. 60+ 个前端常用插件库合集
  6. 软件架构实践(第四版)
  7. android 服务器sessionid,Android用WebView获取sessionid保持登录状态
  8. 恢复出厂设置android手机号码,安卓手机怎么正确恢复出厂设置的简单方法
  9. 定义文档兼容性,让IE按指定的版本解析我们的页面
  10. GDB调试指南(入门,看这篇够了)