原文出自 jakewharton 关于 D8 和 R8 系列文章第 17 篇。
由于 Kotlin 标准库的功能特性,在 Kotlin 中 Lambda 的使用感觉比 Java 更普及。有些 Lambda 仅仅是通过使用内联函数在编译时消除的语法结构。其余部分具体化为整个类,以便在运行时使用。
Android Java 8 support 文章中介绍了 Lambdas 的工作机制,但这里有一个快速的更新:
javac 将 Lambda 主体提升到包私有方法中,并在调用站点为目标 Lambda 类型编写invoke-dynamic 指令。JVM 在运行时旋转所需类型的类,并调用方法体中的包私有方法。Android 不提供这种运行时支持,因此 D8 对实现所需类型并调用包私有方法的类执行编译时转换。
kotlinc 直接跳过 invoke-dynamic 字节码(即使是针对 java8+),直接生成完整的类。这里有两个 Kotlin 类和一些 Lambda 用法,我们可以进行实验。
class Employee( val id: String, val joined: LocalDate, val managerId: String?)
class EmployeeRepository(val allEmployees: () -> Sequence<Employee>) { fun joinedAfter(date: LocalDate) = allEmployees() .filter { it.joined >= date } .toList()
fun reports(manager: Employee) = allEmployees() .filter { it.managerId == manager.id } .toList()}
复制代码
EmployeeRepository 类接受一个 Lambda,该 Lambda 生成一个雇员序列,并有两个 public 函数,用于列出在特定日期之后加入的 Employee 和向特定雇员报告的 Employee。这两个函数都使用 Lambda 的 filter 过滤到所需的项,然后再转换为列表。
在编译这个类之后,Kotlin 对 lambdas 的方法立即可见。
$ kotlinc EmployeeRepository.kt$ ls *.classEmployee.classEmployeeRepository.classEmployeeRepository$joinedAfter$1.classEmployeeRepository$reports$1.class
复制代码
每个 lambda 都有一个唯一的名称,通过连接封闭类名、封闭函数名和单调值形成。
1. Kotlin Lambdas 和 D8
因为我们没有实际使用这些 API,所以需要显式保留它们,否则 R8 将生成一个空的 dex 文件。
-keep class Employee { *; }-keep class EmployeeRepository { *; }-dontobfuscate
复制代码
保留两个类之后,让我们运行 R8,看看有什么变化。
$ java -jar $R8_HOME/build/libs/r8.jar \ --lib $ANDROID_HOME/platforms/android-29/android.jar \ --release \ --output . \ --pg-conf rules.txt \ *.class kotlin-stdlib-*.jar
复制代码
我们可以看到 joinedAfter 和 reports 函数的主体发生了什么变化。
[000dd4] EmployeeRepository.joinedAfter:(Ljava/time/LocalDate;)Ljava/util/List; 0000: iget-object v0, v3, LEmployeeRepository;.allEmployees:Lkotlin/jvm/functions/Function0; 0002: invoke-interface {v0}, Lkotlin/jvm/functions/Function0;.invoke:()Ljava/lang/Object; 0005: move-result-object v0-0006: new-instance v1, LEmployeeRepository$joinedAfter$1;-0008: invoke-direct {v1, v3}, LEmployeeRepository$joinedAfter$1;.<init>:(Ljava/time/LocalDate;)V+0006: new-instance v1, L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;+0008: const/4 v2, #int 0+0009: invoke-direct {v1, v2, v4}, L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;.<init>:(ILjava/lang/Object;)V 000d: invoke-static {v0, v1}, Lkotlin/sequences/SequencesKt;.filter:(Lkotlin/sequences/Sequence;Lkotlin/jvm/functions/Function1;)Lkotlin/sequences/Sequence; 0010: move-result-object v0 0011: invoke-static {v0}, Lkotlin/sequences/SequencesKt;.toList:(Lkotlin/sequences/Sequence;)Ljava/util/List; 0014: move-result-object v0 0015: return-object v0
[000e34] EmployeeRepository.reports:(LEmployee;)Ljava/util/List; 0000: iget-object v0, v3, LEmployeeRepository;.allEmployees:Lkotlin/jvm/functions/Function0; 0002: invoke-interface {v0}, Lkotlin/jvm/functions/Function0;.invoke:()Ljava/lang/Object; 0005: move-result-object v0-0006: new-instance v1, LEmployeeRepository$reports$1;-0008: invoke-direct {v1, v3}, LEmployeeRepository$reports$1;.<init>:(LEmployee;)V+0006: new-instance v1, L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;+0008: const/4 v2, #int 1+0009: invoke-direct {v1, v2, v4}, L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;.<init>:(ILjava/lang/Object;)V 000d: invoke-static {v0, v1}, Lkotlin/sequences/SequencesKt;.filter:(Lkotlin/sequences/Sequence;Lkotlin/jvm/functions/Function1;)Lkotlin/sequences/Sequence; 0010: move-result-object v0 0011: invoke-static {v0}, Lkotlin/sequences/SequencesKt;.toList:(Lkotlin/sequences/Sequence;)Ljava/util/List; 0014: move-result-object v0 0015: return-object v0
复制代码
我们来分解新的字节码:
在 0006 位置处创建了一个名为 -$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA 的类,值得注意的是,这两个函数现在都在创建同一个类的实例;
在 0008 位置给 joinedAfter 保存了值 0,以及给 reports 函数保存值 1;
0009 调用类构造函数并传递整数和日期或管理器(但作为对象)。
这两个函数现在都为它们的 Lambda 实例化同一个类。让我们看看那个类。
Class #15 - Class descriptor : 'L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;' Access flags : 0x0011 (PUBLIC FINAL) Interfaces - #0 : 'Lkotlin/jvm/functions/Function1;' Instance fields - #0 : (in L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;) name : '$capture$0' type : 'Ljava/lang/Object;' access : 0x1011 (PUBLIC FINAL SYNTHETIC) #1 : (in L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;) name : '$id$' type : 'I' access : 0x1011 (PUBLIC FINAL SYNTHETIC) Direct methods - #0 : (in L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;) name : '<init>' type : '(ILjava/lang/Object;)V' access : 0x10001 (PUBLIC CONSTRUCTOR) code -[000db0] -$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA.<init>:(ILjava/lang/Object;)V0000: iput v1, v0, L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;.$id$:I0002: iput-object v2, v0, L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;.$capture$0:Ljava/lang/Object;0004: return-void
复制代码
通过字节码可以看到,这个类实现了 Function1 接口,拥有两个 Field,分别是一个 Object 和一个 int 类型的 id,同时构造函数中包含 Object 和 int 类型的两个参数用于给字段赋值。
现在让我们看看 invoke 函数的实现。
[000d14] -$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA.invoke:(Ljava/lang/Object;)Ljava/lang/Object;0000: iget v0, v4, L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;.$id$:I0002: iget-object v1, v4, L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;.$capture$0:Ljava/lang/Object;0004: if-eqz v0, 002c0006: const/4 v2, #int 10007: if-ne v0, v2, 002a
000a: check-cast v1, LEmployee; ⋮0029: return-object v5
002a: const/4 v5, #int 0002b: throw v5
002c: check-cast v0, Ljava/time/LocalDate; ⋮0044: return-object v5
复制代码
我裁剪了很多,但我们来分解一下:
0000 行为 id 字段加载值;
0002 行位 object 字段加载值;
0004 检查 id 是否等于 0,如果是,则跳转到 002c 行;
0006-0007 检查 id 是否是其它的值,如果是则跳转到 002a;
000a-0029 在 reports 的 lambda 实现中,强转 object 为 Employee 对象, 记住,这里的执行条件是 id != 1 失败;
002a-002a 引起 NullPointerException 异常;
002c-0044 在 joinedAfter 的 Lambda 实现中,将 Object 转换为 LocalDate。仅仅通过查看 Dalvik 字节码很难准确理解这种转换的含义。我们可以在源代码中做等价的转换来更清楚地说明它。
class EmployeeRepository(val allEmployees: () -> Sequence<Employee>) { fun joinedAfter(date: LocalDate) = allEmployees()- .filter { it.joined >= date }+ .fitler(MyLambdaGroup(date, 0)) .toList()
fun reports(manager: Employee) = allEmployees()- .filter { it.managerId == manager.id }+ .filter(MyLambdaGroup(manager, 1)) .toList() }++private class MyLambdaGroup(+ private val capture0: Any?,+ private val id: Int+) : (Employee) -> Boolean {+ override fun invoke(employee: Employee): Boolean+ return when (id) {+ 0 -> employee.joinedAfter >= (capture0 as LocalDate)+ 1 -> employee.managerId == (capture0 as Employee).id+ else -> throw NullPointerException()+ }+ }+}
复制代码
两个 Lambda 本来可以产生两个类,但现在已经被一个具有整数的类所取代。通过合并 Lambda 的主体,可以减少 APK 中的类的数量。
这只适用于两个 Lambda 具有相同的格式。它们不需要与我们在示例中看到的完全相同。一个 Lambda 捕获 LocalDate,而另一个捕获 Employee。因为两者只捕获一个值,所以它们具有相同的结构,可以合并到这个 lambda group 类中。
2. Java Lambdas 和 R8
让我们用 Java 重写示例,看看会发生什么。
final class EmployeeRepository { private final Function0<Sequence<Employee>>allEmployees;
EmployeeRepository(Function0<Sequence<Employee>> allEmployees) { this.allEmployees = allEmployees; }
List<Employee> joinedAfter(LocalDate date) { return SequencesKt.toList( SequencesKt.filter( allEmployees.invoke(), e -> e.getJoined().compareTo(date) >= 0)); }
List<Employee> reports(Employee manager) { return SequencesKt.toList( SequencesKt.filter( allEmployees.invoke(), e -> Objects.equals(e.getManagerId(), manager.getId()))); }}
复制代码
我们使用 Kotlin 的 Function0 代替 Supplier, 使用 Sequence 替代 Stream,以及使用序列扩展作为静态帮助器,以使两个示例尽可能彼此接近。我们可以使用 javac 进行编译并重用相同的 R8 调用。
$ rm EmployeeRepository*.class$ javac -cp . EmployeeRepository.class$ java -jar $R8_HOME/build/libs/r8.jar \ --lib $ANDROID_HOME/platforms/android-29/android.jar \ --release \ --output . \ --pg-conf rules.txt \ *.class kotlin-stdlib-*.jar
复制代码
joinedAfter 和 reports 函数体应该看起来与用 Kotlin 编写时相同,对吗?
[000d2c] EmployeeRepository.joinedAfter:(Ljava/time/LocalDate;)Ljava/util/List; ⋮0008: new-instance v1, L-$$Lambda$EmployeeRepository$RwNrgP_DBeZWqltgaXgoLCrPfqI;000a: invoke-direct {v1, v4}, L-$$Lambda$EmployeeRepository$RwNrgP_DBeZWqltgaXgoLCrPfqI;.<init>:(Ljava/time/LocalDate;)V ⋮
[000d80] EmployeeRepository.reports:(LEmployee;)Ljava/util/List; ⋮0008: new-instance v1, L-$$Lambda$EmployeeRepository$JjZ4a6TbrR3768PIUyNflFlLVF8;000a: invoke-direct {v1, v4}, L-$$Lambda$EmployeeRepository$JjZ4a6TbrR3768PIUyNflFlLVF8;.<init>:(LEmployee;)V ⋮
复制代码
他们没有!每个实现都调用自己的 Lambda 类,而不是使用 Lambda group。
据我所知,对于为什么这只适用于 kotlin lambdas 而不适用于 java lambdas,没有任何技术限制。工作还没做完。问题153773246跟踪了对将 Java lambda 合并到 lambda group 的支持。
通过将相同结构的 lambda合并在一起,R8 以增加 lambda 的方法体为代价,减少了 APK 大小影响和运行时类加载负担。
虽然优化确实在整个应用程序上运行,但默认情况下,合并只会在包中发生。这确保了 lambda 主体中使用的任何包私有方法或类型都是可访问的。将 -allowccessmodification 指令添加到 shrinker 规则中,以便 R8 能够在需要时通过提高引用方法和类型的可见性来全局合并lambdas。
您可能已经注意到,为 Java lambda 和 lambda group 生成的类的名称中似乎有某种哈希。在下一篇文章中,我们将深入研究这些类的独特命名。
评论