写点什么

[译] R8 优化: Lambda Groups

用户头像
Antway
关注
发布于: 58 分钟前

原文出自 jakewharton 关于 D8R8 系列文章第 17 篇。



由于 Kotlin 标准库的功能特性,在 KotlinLambda 的使用感觉比 Java 更普及。有些 Lambda 仅仅是通过使用内联函数在编译时消除的语法结构。其余部分具体化为整个类,以便在运行时使用。


Android Java 8 support 文章中介绍了 Lambdas 的工作机制,但这里有一个快速的更新:


  • javacLambda 主体提升到包私有方法中,并在调用站点为目标 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。这两个函数都使用 Lambdafilter 过滤到所需的项,然后再转换为列表。


在编译这个类之后,Kotlinlambdas 的方法立即可见。


$ 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
复制代码


我们可以看到 joinedAfterreports 函数的主体发生了什么变化。


 [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,同时构造函数中包含 Objectint 类型的两个参数用于给字段赋值。


现在让我们看看 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-0029reportslambda 实现中,强转 objectEmployee 对象, 记住,这里的执行条件是 id != 1 失败;

  • 002a-002a 引起 NullPointerException 异常;

  • 002c-0044joinedAfterLambda 实现中,将 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()))); }}
复制代码


我们使用 KotlinFunction0 代替 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
复制代码


joinedAfterreports 函数体应该看起来与用 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 lambdalambda group 生成的类的名称中似乎有某种哈希。在下一篇文章中,我们将深入研究这些类的独特命名。

用户头像

Antway

关注

持续精进,尽管很慢 2019.05.27 加入

专注开源库

评论

发布
暂无评论
[译] R8 优化: Lambda Groups