[译] R8 优化: Lambda Groups

这是我参与更文挑战的第 30 天,活动详情查看: 更文挑战

原文出自 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 *.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
复制代码

我们可以看到 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;)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,同时构造函数中包含 Objectint 类型的两个参数用于给字段赋值。

现在让我们看看 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-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 生成的类的名称中似乎有某种哈希。在下一篇文章中,我们将深入研究这些类的独特命名。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享