一、前言
本文是前作「Lambda 设计参考」的实战部分,具体将介绍如何使用 ASM 对 Java 8 Lambda 表达式和方法引用进行 Hook 操作。
在此之前会介绍一些基础概念和字节码相关的知识方便大家对这块内容的理解,最后会给出一个完整的代码供大家参考。
二、脱糖
2.1 概念介绍
Java 脱糖(Desugar):简单地说,就是在编译阶段将语法层面一些底层字节码不支持的特性转换为底层支持的结构。例如:可以在 Android 中使用 Java 8 的 Lambda 特性,就是使用了脱糖。
使用脱糖的最主要原因是 Android 设备并没有提供 Java 8 的运行时环境。下面用一个例子来展示对 Lambda 脱糖需要做的工作。
class Java8 {
interface Logger {
void log(String s);
}
public static void main(String... args) {
sayHi(s -> System.out.println(s));
}
private static void sayHi(Logger logger) {
logger.log("Hello!");
}
}
首先是将 Lambda 方法体中的内容从 main 方法中移到 Java8 类的内部方法中,改变后的结果如下:
public class Java8 {
interface Logger {
void log(String s);
}
public static void main(String... args) {
//使用 lambda$main$0 替换原有的逻辑
sayHi(s -> lambda$main$0(s));
}
private static void sayHi(Logger logger) {
logger.log("Hello!");
}
//方法体中的内容移到这里
static void lambda$main$0(String str){
System.out.println(str);
}
}
接着生成一个类,这个类实现了 Logger 接口,实现的方法中调用 lambda$main$0 方法,并且使用实现类替换代码 sayHi(s -> lambda$main$0(s)) ,改变后的代码如下:
public class Java8 {
interface Logger {
void log(String s);
}
public static void main(String... args) {
//这里使用 Logger 的实现类 Java8$1
sayHi(s -> new Java8$1());
}
private static void sayHi(Logger logger) {
logger.log("Hello!");
}
//方法体中的内容移到这里
static void lambda$main$0(String str){
System.out.println(str);
}
}
public class Java8$1 implements Java8.Logger {
public Java8$1(){
}
@Override
public void log(String s) {
//这里调用 Java8 方法的静态方法
Java8.lambda$main$0(s);
}
}
最后,因为 Lambda 并没有捕获外部作用的任何变量,所以这是一个无状态 Lambda。实现类会生成一个单例,在使用的地方用这个单例来替换 new Java8$1(),最终的代码如下:
class Java8 {
interface Logger {
void log(String s);
}
public static void main(String... args) {
//此处使用单例替换原有代码
sayHi(Java8$1.INSTANCE);
}
static void lambda$main$0(String s) {
System.out.println(s);
}
private static void sayHi(Logger logger) {
logger.log("Hello!");
}
}
public class Java8$1 implements Java8.Logger {
static final Java8$1 INSTANCE = new Java8$1();
@Override
public void log(String s) {
Java8.lambda$main$0(s);
}
}
这个例子简单地展示了脱糖的过程,其中 lambda$main$0 方法会在编译的时候生成。需要注意的是:方法引用并不会生成额外的方法。
(关于方法引用和 lambda$main$0 的生成规则以及上面提到的无状态 lambdas 等知识可以通过「Lambda 设计参考」获取,读者如果对这部分内容不了解可以先看这篇文章)
2.2 Android 中的脱糖
上一节介绍了什么是脱糖以及用一个简单的例子来演示 Lambda 表达式的脱糖逻辑,那么我们为什么要关注 Android 中的脱糖呢?
首先 Android 系统本身并不支持 Java 8,前面说了 Android 设备并没有提供 Java 8 的运行时环境。因此,App 项目使用 Java 8 编译产生的字节码是无法在 Android 设备上解析的,Android 使用 Gradle 在编译时会将 .class 文件中的一些 Java 8 语法特性脱糖成 Java 7 中支持的语法特性。
我们看下图 2-1 描述的 Android 处理 Java 文件的流程,注意图中的 “Third-party plugins” 是 Android 为我们提供的可以在编译期有机会处理 .class 文件。关于插件开发,可以参考我司出版的《Android 全埋点解决方案》
一书。
图 2-1 Android 处理 Java 文件的流程(来源:https://developer.android.com/studio/write/java8-support)
根据图 2-1 所示,自定义的 Android 插件是在 D8/R8 之前先操作 .class 文件。D8 是 Android 提供的脱糖工具,这就导致自定义插件获取的 .class 是原始未脱糖的 .class(注:多个自定义插件执行顺序跟引入顺序有关,我们自定义的插件获取到的 .class 可能是其他插件处理过的 )。现在我们来分析下面这段代码:
<