原文出自 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 *.class
Employee.class
EmployeeRepository.class
EmployeeRepository$joinedAfter$1.class
EmployeeRepository$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;)V
0000: iput v1, v0, L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;.$id$:I
0002: 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$:I
0002: iget-object v1, v4, L-$$LambdaGroup$ks$D2r6uJKXMyXfodlTO7Kw1WcCloA;.$capture$0:Ljava/lang/Object;
0004: if-eqz v0, 002c
0006: const/4 v2, #int 1
0007: if-ne v0, v2, 002a
000a: check-cast v1, LEmployee;
⋮
0029: return-object v5
002a: const/4 v5, #int 0
002b: 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
生成的类的名称中似乎有某种哈希。在下一篇文章中,我们将深入研究这些类的独特命名。
评论