阅读本篇可能需要的预备知识 《ASM4 使用手册(中文版)》,本文涉及代码已经上传Github,欢迎star一波~
《ASM 字节码插桩》大纲
背景和疑问
在 Android 中,你可能经常听某位中台大佬说 无痕埋点 , Hook ,apm监控,编译器动态修改代码等名词,小伙伴通常都知道 AspectJ 可以通过切面织入相关代码,但殊不知 就连小小的 Lambada 语法在自定义 Plugin 都无法实现。
更何况其他兼容问题,有没有一个相对完美的选择,实现全埋点呢?有没有最优质的技术选择向应用程序中插入调试或性能监视代码同时保证应用程序的运行速度。
好吧,不拐外抹角啦,今天就带大家详细聊聊一款轻量级AOP设计ASM。
1.0 关键技术
了解 ASM 之前 首先得了解 APP 的打包流程,这里推荐大家看一下邓凡平的 《深入理解Android虚拟机》,Android应用打包流程大概分为以下7个阶段:
- aapt 打包资源文件 阶段
- aidl 转 java 文件 阶段
- Java 编译(Compilers)生成.class文件 阶段
- dex(生成dex文件)阶段
- apkbuilder(生成未签名apk)阶段
- Jarsigner(签名)阶段
- zipalign(对齐) 阶段
具体每个阶段做了哪些工作,可以参考浅谈Android打包流程一文,我们只需要关注第四阶段 生成.dex之前的工作即可,因为在这个阶段我们能拦截到所有的.class文件,然后借助插件,就可以遍历.class文件的所有方法,再根据一定的条件找到需要的目标方法,最后修改并保存,就可以插入我们需要的代码了。
那么我们所说的插件到底是什么呢?在Google从 Android Gradle 1.5 开始就提供了 Transform Api,通过Transform Api,允许三方 Plugin形式,在应用程序打包生成 .dex 前编译过程中操作 .class 文件。
我们要做的工作就是 自定义 Transform, 迭代 .class 文件所有的方法,然后修改在特定的listener中插入埋点代码,最后对源文件替换,达到织入代码的目的,那么Transform到是什么呢?
1.1 Gradle Transform
回到什么一个问题,什么是Transform?Google官方文档是这么翻译的
A transform receives input as a collection TransformInput, which is composed of JarInputs and DirectoryInputs. Both provide information about the QualifiedContent.Scopes and QualifiedContent.ContentTypes associated with their particular content.The output is handled by TransformOutputProvider which allows creating new self-contained content, each associated with their own Scopes and Content Types. The content handled by TransformInput/Output is managed by the transform system, and their location is not configurable.It is best practice to write into as many outputs as Jar/Folder Inputs have been received by the transform. Combining all the inputs into a single output prevents downstream transform from processing limited scopes.
简单理解就是: 用来修改 .class 文件的一套标准 API,目的是把 .class 文件 转换成目标 字节码文件,其实你可以简单的把它和文件输入流和输出流类比起来,只不过IO体系里面操作的是流对象而Transform操作的是文件对象。
记住两个核心的API:TransformInput 和 TransformOutputProvider;TransformInput代表的是输入文件抽象接口,它有两个比较重要的方法
获取DirectoryInput集合和获取JarInput集合,其中: DirectoryInput是以源码方式参与项目编译所有目录结构及其目录下的源文件;而JarInput是以jar包方式参与项目编译的所有jar包和远程包。
TransformOutputProvider代表的是输出文件抽象接口,里面有一个核心接口方法getContentLocation主要是获取输出路径信息的
getContentLocation方法有几个比较重要的参数name,type,scopes和format,其中name代表该 Transform 对应的 Task 的名称。
QualifiedContent.ContentType代表的是Transform需要处理的数据类型;里面有两个默认枚举参数: CLASSES 和 PESOURCES。
CLASSES 代表 需要处理编译后的字节码,可能是jar 也可能是 目录。PESOURCES代表处理标准的 java 资源,scopes 也是一个比较有意思的枚举类,
用来指定 Transform 的作用域,其中有七个枚举对象:PROJECT,SUB_PROJECTS,PROJECT_LOCALDEPS,SUB_PROJECTS_LOCAL_OEPS,EXTERNAL_LIBRARIES,PROVIDED_ONLY和TESTED_CODE。
PROJECT只处理当前项目,SUB_PROJECTS,只处理子项目,PROJECT_LOCALDEPS只处理当前项目的本地依赖,例如: jar , arr。SUB_PROJECTS_LOCAL_OEPS只处理子项目的本地依赖。例如: jar , arr,EXTERNAL_LIBRARIES只处理外包的依赖库,PROVIDED_ONLY只处理本地或远程以 provided 形式引入的依赖库。而TESTED_CODE指的是测试代码。format 是用来格式化内容的。
至此TransformOutputProvider就介绍完了。纸上得来终觉浅,绝知此事要躬行。我们简单实现一下Transform吧
第一步: 新建一个 Project
里面自动生成一个主 moudle,即: app
第二步: 新建一个命名为 plugin 的 moudle
第三步: 清空 plugin/build.gradle 文件的内容,然后修改里面的内容
第四步: 删除 plugin.src/main 目录下所有的文件
第五步: 新建 groovy 目录
第六步: 创建 Transform 类
第七步: 新建 Plugin,创建 .properties
第八步: 执行 plugin 的 uploadArchives 任务构建 plugin
第九步: 修改项目根目录下的 buid.gradle 文件,添加对插件的依赖
第十步: 在 app/build.gradle 文件声明使用插件
第十一步: 构建应用程序
至此,一个简单的 Gradle Transform 的实例 就已经完成了
1.2 ASM 基础知识
如果说Transform只是前菜,那么今天的主菜应该是ASM,什么是ASM呢?官方是这么解释的。
ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).
ASM其实是一个功能比较齐全的 Java 字节码操作和分析框架,通过ASM,我们可以动态生成类或增强类的既有类的功能,ASM可以直接生成二进制的.class文件,也可以被类在加载入 java 虚拟机
之前动态改变现有类的行为,Java 的二进制被存储在严格格式定义的 .class 文件里,这些字节码拥有足够的元数据信息用来表示类的所有元素,包括类的名称,方法,属性以及Java 字节码指令。
ASM 从字节码文件读入这些信息后能改变 类的行为,分析类的信息,甚至根据具体要求生成新的类。ASM涉及了五个比较核心的类: ClassReader,ClassWriter,MethodVisitor,ClassVistor和AdiviveAdapter。
ClassReader 主要是 用来解析编译过的 .class 字节码文件,ClassWriter 主要是 用来重新构建编译后的类,比如修改类名,属性以及方法,甚至可以生产新的类名字节码。
MethodVisitor主要是方法访问类,有几个重要的方法:onMethodEnter,visitEnd和visitAnnotation。onMethodEnter主要是进入方法时插入字节码,visitEnd主要是退出方法前可以退出字节码,visitAnnotation可以在这里通过注解的方式操作字节码。
ClassVistor负责 "拜访" 类成员信息 其中包括 在类的注解,类的构造方法,类的字段,类的方法,静态代码块
ClassVistor 有几个比较重要的方法,重点需要了解的有: visit和visitMehtod;
visit有6个比较重要的参数: version,access,name,signature,signatureName和interfaces。version代表的是JDK的版本,版本对应表格如下:
access代表类的修饰符,具体修饰符含义如下:
name代表的是类的名称;signature是泛型信息;signatureName是当前类所继承的父类;interfaces是类所实现的接口列表,在java中,一个类是可以实现多个不同的接口,因此该参数是一个数组类型。下面我们看一下ClassVistor的样例代码:
class MkAnalyticsClassVisitor extends ClassVisitor implements Opcodes { private final static String SDK_API_CLASS = "com/github/microkibaco/asm_sdk/SensorsDataAutoTrackHelper" private String[] mInterfaces private ClassVisitor classVisitor private HashMap<String, MkAnalyticsMethodCell> mLambdaMethodCells = new HashMap<>() MkAnalyticsClassVisitor(final ClassVisitor classVisitor) { super(Opcodes.ASM6, classVisitor) this.classVisitor = classVisitor } private static void visitMethodWithLoadedParams(MethodVisitor methodVisitor, int opcode, String owner, String methodName, String methodDesc, int start, int count, List<Integer> paramOpcodes) { for (int i = start; i < start + count; i++) { methodVisitor.visitVarInsn(paramOpcodes[i - start], i) } methodVisitor.visitMethodInsn(opcode, owner, methodName, methodDesc, false) } /** * visit 可以拿到关于 .class 的所有信息,比如: 当前类所实现的接口列表 * @param version JDK的版本 * @param access 类的修饰符 * @param name 类的名称 * @param signature 当前类所继承的父类 * @param superName * @param interfaces 类所实现的接口列表,在java中,一个类是可以实现多个不同的接口,因此该参数是一个数组类型 */ @Override void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces) mInterfaces = interfaces } /** * 可以拿到关于 method所有的信息,比如: 方法名 方法的参数描述 * @param access 类的修饰符 * @param name 类的名称 * @param desc 方法签名 * @param signature 类签名 * @param exceptions 异常信息 * @return MethodVisitor */ @Override MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions) return methodVisitor } /** * 获取方法参数下标为 index 的对应 ASM index * @param types 方法参数类型数组 * @param index 方法中参数下标,从 0 开始 * @param isStaticMethod 该方法是否为静态方法 * @return 访问该方法的 index 位参数的 ASM index */ int getVisitPosition(Type[] types, int index, boolean isStaticMethod) { if (types == null || index < 0 || index >= types.length) { throw new Error("getVisitPosition error") } if (index == 0) { return isStaticMethod ? 0 : 1 } else { return getVisitPosition(types, index - 1, isStaticMethod) + types[index - 1].getSize() } } }
上面的 visitMethod中,可以对特定的方法镜像修改,修改方法的时候需要用到"拜访"方法所以信息 MethodVistitor
methodVisitor = new MkAnalyticsDefaultMethodVisitor(methodVisitor, access, name, desc) { boolean isSensorsDataTrackViewOnClickAnnotation = false /** * 退出方法前可以退出字节码 */ @Override void visitEnd() { super.visitEnd() if (mLambdaMethodCells.containsKey(nameDesc)) { mLambdaMethodCells.remove(nameDesc) } } /** * 进入方法时插入字节码 */ @Override protected void onMethodEnter() { super.onMethodEnter() } /** * 可以在这里通过注解的方式操作字节码 * @param s 访问的注解名 * @param b 是否方法 * @return methodVisitor */ @Override groovyjarjarasm.asm.AnnotationVisitor visitAnnotation(String s, boolean b) { if (s == 'Lcom/sensorsdata/analytics/android/sdk/SensorsDataTrackViewOnClick;') { isSensorsDataTrackViewOnClickAnnotation = true } return super.visitAnnotation(s, b) } }
visitMehtod 方法也有几个比较重要的参数: access,name,desc,signature和exceptions。access代表的是方法的修饰符,具体修饰符含义如下:
name表示方法名;desc表示方法签名,具体符号类型如下:
方法参数列表对应的方法签名如下:
signature表示泛型相关信息;exceptions表示将会抛出异常,如果方法不会抛出异常,该参数为空。最后我们看一下AdiviveAdapter,它实现 MethodVistor 接口,主要负责 "拜访" 方法的信息,用来进行具体方法
class MkAnalyticsDefaultMethodVisitor extends AdviceAdapter { MkAnalyticsDefaultMethodVisitor(MethodVisitor mv, int access, String name, String desc) { super(Opcodes.ASM6, mv, access, name, desc) } /** * 表示 ASM 开始扫描这个方法 */ @Override void visitCode() { super.visitCode() } @Override void visitMethodInsn(int opcode, String owner, String name, String desc) { super.visitMethodInsn(opcode, owner, name, desc) } @Override void visitAttribute(Attribute attribute) { super.visitAttribute(attribute) } /** * 表示方法输出完毕 */ @Override void visitEnd() { super.visitEnd() } @Override void visitFieldInsn(int opcode, String owner, String name, String desc) { super.visitFieldInsn(opcode, owner, name, desc) } @Override void visitIincInsn(int var, int increment) { super.visitIincInsn(var, increment) } @Override void visitIntInsn(int i, int i1) { super.visitIntInsn(i, i1) } /** * 该方法是 visitEnd 之前调用的方法,可以反复调用。用以确定类方法在执行时候的堆栈大小。 * @param maxStack * @param maxLocals */ @Override void visitMaxs(int maxStack, int maxLocals) { super.visitMaxs(maxStack, maxLocals) } @Override void visitVarInsn(int opcode, int var) { super.visitVarInsn(opcode, var) } @Override void visitJumpInsn(int opcode, Label label) { super.visitJumpInsn(opcode, label) } @Override void visitLookupSwitchInsn(Label label, int[] ints, Label[] labels) { super.visitLookupSwitchInsn(label, ints, labels) } @Override void visitMultiANewArrayInsn(String s, int i) { super.visitMultiANewArrayInsn(s, i) } @Override void visitTableSwitchInsn(int i, int i1, Label label, Label[] labels) { super.visitTableSwitchInsn(i, i1, label, labels) } @Override void visitTryCatchBlock(Label label, Label label1, Label label2, String s) { super.visitTryCatchBlock(label, label1, label2, s) } @Override void visitTypeInsn(int opcode, String s) { super.visitTypeInsn(opcode, s) } @Override void visitLocalVariable(String s, String s1, String s2, Label label, Label label1, int i) { super.visitLocalVariable(s, s1, s2, label, label1, i) } @Override void visitInsn(int opcode) { super.visitInsn(opcode) } @Override AnnotationVisitor visitAnnotation(String s, boolean b) { return super.visitAnnotation(s, b) } @Override protected void onMethodEnter() { super.onMethodEnter() } /** * 使用 onMethodExit 这样就不会影响到应用程序原有点击事件的响应速度 * @param opcode */ @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode) } }
但是我在使用过程遇到了这个坑,具体原因还要审核一下Plugin是否导包有误