前言
什么是字节码插桩
字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加
从技术上来说,字节码插桩是自定义Gradle插件、ASM、Java字节码、切面编程的综合应用
字节码插桩可以做什么
举个例子,APP全量统计的时候,经常需要建立很多埋点。这是个很大重复性工作,那么可以通过字节码插桩,在apk打包之前,对class文件需要的地方进行埋点。这样就可以实现无埋点的全量统计。
下面我们来逐一介绍用到的知识,可能需要学习很多东西,学完这些将会有很大的收货!!
一、切面编程 AOP
AOP(Aspect Oriented Program的首字母缩写)是一种面向切面编程的思想。这种编程思想是相对于OOP(ObjectOriented Programming即面向对象编程)来说的。
先来说一下大家熟悉的面向对象编程:面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。
但是存在一个问题,如果每个类中都需要同样的功能,例如日志,统计等。这个是面向对象的编程天生的缺点,就是分散代码的同时,也增加了代码的重复性。按照OOP的思想,我们需要在各个模块里面都添加统计代码。但是如果按照AOP的思想,可以将统计的地方抽象成切面,只需要在切面里面添加统计代码就OK了。
字节码插桩是AOP编程一种很好的实现方式,在后台开发的Spring框架中已经在使用切面编程来添加操作日志记录
二、APK打包流程
在APK打包的时候,我们要对字节码进行修改,那么就需要了解整个打包流程,知道在哪个过程中可以获取到字节码。
官网的打包流程介绍的不是很详细,下图介绍了详细的打包流程。
apk打包使用的工具是gradle,Android 提供了Gradle插件 com.android.tools.build:gradle
,使得我们可以轻松执行这个打包流程
经过“Java Compiler步骤”,也就是代码编译,系统便生成了.class文件。这些class文件经过dex步骤再次转化成Android识别的.dex文件。
既然我们要做字节码插桩,就必须hook打包流程,在dex步骤之前对class字节码进行扫描与重新编织,然后将编织好的class文件交给dex过程。这样就实现了所谓的无埋点。
那么问题来了,怎么才能在打包流程中,添加我们想要执行的操作。也就是说如何才能拦截住打包流程呢? 我们下面分解
三、自定义Gradle插件
整个打包流程是由Android Gradle插件 com.android.tools.build:gradle
提供的,在1.5.0-beta1 及以后的版本,添加了Transform API ,允许第三方Gradle 插件,在打包为dex 之前,可以对class进行操作。(这些书都有记载,不是我在乱掰。详见官网)
那么由此引出两个知识点,介绍这两个的篇幅有点长,所以列出一下网上比较好的文章
3.1、自定义Gradle插件
自定义Gradle插件 官方文档:Developing Custom Gradle Plugins
在AndroidStudio中自定义Gradle插件
拥抱 Android Studio 之五:Gradle 插件开发
对以上两边文章,有两点补充:
1、关于自定义Gradle插件 ,网上中文文档,基本都还是在介绍使用使用groovy,现在已经支持使用kotlin来编写Gradle插件。两者只在目录方面有些差异。最后的demo就是使用kotlin来编写Gradle插件的
2、在build.gradle 文件中,通常会出现 apply plugin: 'com.android.application'
这里apply plugin 的是groovy 语言调用函数的方式,这句代码会调用com.android.application
插件的apply()
函数
3.2、如何使用Transform API
四、Java字节码
Java 字节码(英语:Java bytecode)是Java虚拟机执行的一种指令格式。通俗来讲字节码就是经过javac命令编译之后生成的Class文件。Class文件包含了Java虚拟机指令集和符号表以及若干其他的辅助信息。Class文件是一组以8位字节为基础单位的二进制流,所有数据项目严格按照顺序紧凑的排列在Class文件之中,中间没有任何分隔符,这使得整个Class文件中存储的内容几乎全是程序运行时的必要数据。
通俗的说就是,Java代码编译后生成的Class文件,这个文件是二进制。这个文件有个规定,第几位到第几位是什么数据,
4.1、关于字节码几个重要的内容::
1、 Class文件中使用全限定名来表示一个类的引用,全限定名很容易理解,即把类名所有“.”换成了“/”
例如:
android.widget.TextView
的全限定名 android/widget/TextView
2、描述符
Class文件中使用描述符,描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
- 基本数据类型(byte char double float int long short boolean)以及代表无返回值的void类型都用一个大写字符( Type Signature)来表示
- 对象类型则用字符“L”加对象的全限定名来表示,一般对象类型末尾都会加一个“;”来表示全限定名的结束。
类型签名
Type Signature | Java Type |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L | fully-qualified-class ;fully-qualified-class |
[ type | type[] |
( arg-types ) ret-type | method type |
看了字节码,相信你一定有疑问,是不是对class文件修改很复杂呀?其实根本没有这么复杂,而且已经有很多支持字节码编织的框架,学习字节码,是为了在使用框架修改字节码时更上手一点
五、 Java 字节码编织框架——ASM
什么是ASM?
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
看完下面这篇文章,基本可以直接上手使用ASM框架了,通过这个框架,我们可以修改class文件,增加函数,修改函数、各种逻辑等等编程操作。
AOP 的利器:ASM 3.0 介绍,这篇文章有点过时,但是基本的原理没变,所以可以学习其思想
虽然有了ASM这种框架,可以很方便的修改class文件,但是如果不熟悉框架的使用,写起来还是有点吃力
人类总是懒惰的,试图找出一些捷径,于是有了一款Idea插件——ASM Bytecode Outline
如果想深入学习ASM,可以查看这个系列的文章:
1.1 ASM-简介-目的
ASM Bytecode Outline
插件ASM Bytecode Outline,可以把java代码转为ASM框架 的代码,那么我们可以先修改好一个类的代码,把代码转为ASM框架的代码,然后把需要的代码复制到,这样就可以在自定义的gradle plugin 中批量自动去修改目标类了。
参考:
Android字节码插桩采坑笔记
通过自定义 Gradle 插件修改编译后的 class 文件