Android全埋点解决方案之Javassist

栏目: Java · 发布时间: 7年前

内容简介:Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。Javassist 可以绕过编译,直接操作字节码,从而实现代码注入。所以使用 Javassist 的时机就是在构建工具 Gradle 将源 文件编译成 .class 文件之后,在将 .class 打包成 .dex 文件之前。

Android全埋点解决方案之Javassist

Javassist

Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。

Javassist 可以绕过编译,直接操作字节码,从而实现代码注入。所以使用 Javassist 的时机就是在构建工具 Gradle 将源 文件编译成 .class 文件之后,在将 .class 打包成 .dex 文件之前。

Javassist 基础

• 读写字节码

在 Javassist 中,.class 文件是用类 Javassist.CtClass 表示。一个 CtClass 对象可以处理一个 .class 文件。

Android全埋点解决方案之Javassist

在上面这个示例中,先获取一个 ClassPool 对象 。ClassPool 是 CtClass 对象的容器。它按需读取类文件来创建 CtClass 对象,并且保存 CtClass 对象以便以后会被使用到。

为了修改类的定义,首先需要使用 ClassPool.get() 方法从 ClassPool 中获得一个 CtClass 对象。使用 getDefault() 方法获 取的 ClassPool 对象使用的是默认系统的类搜索路径。

ClassPool 是一个存储 CtClass 的 Hash 表,类的名称作为 Hash 表的 key。ClassPool 的 get() 函数会从 Hash 表查找 key 对应的 CtClass 对象。如果没有找到,get() 函数会创建并返回一个新的 CtClass 对象,这个对象会保存在 Hash 表中。

从 ClassPool 中获取的 CtClass 对象是可以被修改的。在上面的例子中,com.sensorsdata.analytics.android.sdk.Sen- sorsDataAutoTrackHelper 的父类被设置为 java.lang.Object。调用 writeFile() 后,这项修改会被写入原始类文件中。

writeFile() 会将 CtClass 对象转换成类文件并写到本地磁盘。同时,也可以使用 toBytecode() 函数来获取修改过的字节码 :

byte[] b = aClass.toBytecode();

也可以使用 toClass() 函数直接将 CtClass 转换成 Class 对象:

Class clazz = aClass.toClass();

toClass() 请求当前线程的 ClassLoader 加载 CtClass 所代表的类文件,它返回此类文件的 java.lang.Class 对象。

• 冻结类

如果一个 CtClass 对象通过 writeFile()、toClass()、toBytecode() 等方法被转换成一个类文件,此 CtClass 对象就会被冻 结起来,不允许再被修改,这是因为一个类只能被 JVM 加载一次。

其实,一个冻结的 CtClass 对象也可以被解冻,比如: Android全埋点解决方案之Javassist

此处调用 defrost() 方法之后,这个 CtClass 对象就又可以被修改了。

• 类搜索路径

通过 ClassPool.getDefault() 获取的 ClassPool 使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服 务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下, ClassPool 必须添加额外的类搜索路径。 Android全埋点解决方案之Javassist

上面的代码示例,将 this 指向的类添加到 ClassPool 的类加载路径中。你可以使用任意 Class 对象来代替 this.getClass(),

从而将 Class 对象添加到类加载路径中。同时,也可以注册一个目录作为搜索路径。比如: Android全埋点解决方案之Javassist

上面的例子是将“/usr/local/Library/”目录添加到类搜索路径中。

• ClassPool

ClassPool 是 CtClass 对象的容器。因为编译器在编译引用 CtClass 代表的 Java 类的源代码时,可能会引用 CtClass 对象, 所以一旦一个 CtClass 被创建,它就会被保存在 CtClass 中。

• 避免内存溢出

如果 CtClass 对象的数量变得非常多,ClassPool 有可能会导致巨大的内存消耗。为了避免这个问题,我们可以从 ClassPool 中显式删除不必要的 CtClass 对象。如果对 CtClass 对象调用 detach() 方法,那么该 CtClass 对象将会被从 ClassPool 中删除。比如:

Android全埋点解决方案之Javassist

在调用 detach() 方法之后,就不能再调用这个 CtClass 对象的任何有关方法了。如果调用 ClassPool 的 get() 方法, ClassPool 会再次读取这个类文件,并创建一个新的 CtClass 对象。

• 在方法体中插入代码

CtMethod 和 CtConstructor 均提供了 insertBefore()、insertAfter() 及 addCatch() 等方法。它们可以把用 Java 编写的代 码片段插入到现有的方法体中。Javassist 包括一个用于处理源代码的小型编译器,它接收用 Java 编写的源代码,然后将 其编译成 Java 字节码,并内联到方法体中。

也可以按行号来插入代码段(如果行号表包含在类文件中)。向 CtMethod 和 CtConstructor 中的 insertAt() 方法提供源代 码和原始类定义中的源文件的行号,就可以将编译后的代码插入到指定行号位置。

insertBefore() 、insertAfter()、addCatch() 和 insertAt() 等方法都能接收一个表示语句或语句块的 String 对象。一个语 句是一个单一的控制结构,比如 if 和 while 或者以分号结尾的表达式。语句块是一组用大括号 {} 包围的语句。

语句和语句块可以引用字段和方法。但不允许访问在方法中声明的局部变量,尽管在块中声明一个新的局部变量是允许的。

传递给方法 insertBefore() 、insertAfter() 、addCatch() 和 insertAt() 的 String 对象是由 Javassist 的编译器编译的。由 于编译器支持语言扩展,所以以 $ 开头的几个标识符都有特殊的含义:

$0, $1, $2, ...

传递给目标方法的参数使用 $1,$2,... 来访问,而不是原始的参数名称。$1 表示第一个参数,$2 表示第二个参数,以此类推。 这些变量的类型与参数类型相同。$0 等价于 this 指针。如果方法是静态的,则 $0 不可用。

$args

变量 $args 表示所有参数的数组。该变量的类型是 Object 类型的数组。如果参数类型是原始类型(如 int、boolean 等), 则该参数值将被转换为包装器对象(如 java.lang.Integer)以存储在 $args 中。 因此,如果第一个参数的类型不是原始类型, 那么 $args[0] 等于 $1。注意 $args[0] 不等于 $0,因为 $0 表示 this。

$

变量 $$ 是所有参数列表的缩写,用逗号分隔。

$_

CtMethod 中的 insertAfter() 是在方法的末尾插入编译的代码。传递给 insertAfter() 的语句中,不但可以使用特殊符号如 $0,$1。也可以使用 $_ 来表示方法的结果值。

该变量的类型是方法的返回结果类型(返回类型)。如果返回结果类型为 void,那么 $_ 的类型为 Object,$_ 的值为 null。

虽然由 insertAfter() 插入的编译代码通常在方法返回之前执行,但是当方法抛出异常时,它也可以执行。要在抛出异常时 执行它,insertAfter() 的第二个参数 asFinally 必须为 true。

如果抛出异常,由 insertAfter() 插入的编译代码将作为 finally 子句执行。$_ 的值 0 或 null。在编译代码的执行终止后, 最初抛出的异常被重新抛出给调用者。注意,$_ 的值不会被抛给调用者,它将被丢弃。

• addCatch

addCatch() 插入方法体抛出异常时执行的代码,控制权会返回给调用者。在插入的源代码中,异常用 $e 表示。

Android全埋点解决方案之Javassist

转换成对应的 java 代码如下:

Android全埋点解决方案之Javassist

请注意,插入的代码片段必须以 throw 或 return 语句结束。

• 注解(Annotations)

CtClass、CtMethod、CtField 和 CtConstructor 均提供了 getAnnotations() 方法,用于读取注解。它返回一个注解类型 的对象数组。

我们目前只介绍当前全埋点方案会用到的关于 Javassist 的相关基础知识,关于 Javassist 更详细的用法,可以参考: https://github.com/jboss-javassist/javassist/wiki/Tutorial-1

原理概述

在自定义的 plugin 里,注册一个自定义的 Transform,然后可以分别对目录和 jar 包进行遍历。在遍历的过程中,利用 Javassist 的 API 来对满足特定条件的方法进行修改,插入相关埋点代码。原理与 ASM 类似,只是把操作 .class 文件的库 由 ASM 换成 Javassist。

实现步骤

完整的项目源码后续会 release 给大家。

缺点

• 暂时没有什么发现缺点

知识点

• 汇编相关知识

参考资料

[1] https://www.jianshu.com/p/43424242846b

[2]https://blog.csdn.net/Deemons/article/details/78473874

[3]https://blog.csdn.net/yulong0809/article/details/77752098

[4] https://juejin.im/post/58fea36bda2f60005dd1b7c5

[5] https://www.jianshu.com/p/417589a561da

[6] http://www.javassist.org

[7] https://github.com/jboss-javassist/javassist

[8]https://github.com/jbossjavassist/javassist/wiki/Tutorial-1

[9]https://github.com/jbossjavassist/javassist/wiki/Tutorial-2

[10]https://github.com/jbossjavassist/javassist/wiki/Tutorial-3

注:该内容来自神策数据用户行为洞察研究院出品的《Android 全埋点解决方案》白皮书,查看完整白皮书可点击 《Android 全埋点解决方案》

更多白皮书、报告、干货和案例,可以关注“神策数据”和“用户行为洞察研究院”公众号了解~ Android全埋点解决方案之Javassist


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Twenty Lectures on Algorithmic Game Theory

Twenty Lectures on Algorithmic Game Theory

Tim Roughgarden / Cambridge University Press / 2016-8-31 / USD 34.99

Computer science and economics have engaged in a lively interaction over the past fifteen years, resulting in the new field of algorithmic game theory. Many problems that are central to modern compute......一起来看看 《Twenty Lectures on Algorithmic Game Theory》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换