脸书从0到1000万行Kotlin代码的经验分享


近年来,Kotlin 已成为 Android 开发的流行语言。因此,我们将在 Meta 上的 Android 开发转移到 Kotlin 上才有意义,因为我们努力使我们的开发工作流程更加高效。 
Meta 的 Android 存储库非常庞大,涵盖了我们的应用程序和技术系列,包括 Facebook、Instagram、Messenger、Portal 和 Quest。从我们目前用于 Android 开发的 Java 转向 Kotlin 并非易事。

除了受欢迎之外,Kotlin 还拥有一些主要优势:

  • 可空性:空指针异常是 Meta 的一个常见问题,与其他任何地方一样。我们非常擅长在发布我们的应用程序之前修复它们,但处理这些问题仍然很耗时。我们使用内部工具更早地检测空安全问题,并且我们严格注释我们的代码作为我们工作的一部分,以便更早地检测 Java 中的此类问题。但即便如此,Kotlin 内置的可空性处理更健壮且更易于使用。
  • 函数式编程:Kotlin 对内联函数和 lambda 表达式的支持使我们能够使用函数式编程风格而不会影响执行速度。尽管 Java 8 添加了对 lambdas 的支持并且可用于 Android,但它的代价是更多的匿名对象,这会对低端 Android 设备的性能产生负面影响。Meta 的自制Redex最大限度地减少了这些问题,但它们仍然存在,使 Kotlin 成为更好的选择。
  • 更短的代码:Kotlin 的现代设计使其代码更短。Kotlin 允许删除显式类型(Java 11 也是如此),并且与基于上述函数式风格的标准库一起,它将许多重复循环缩短为更简单的语句。这个更短的代码也更明确,可以更容易理解。
  • 领域特定语言 (DSL) / 类型安全构建器Kotlin 的各种函数结合在一起,让我们定义一个 DSL。基本上,一种将 Android XML 等定义移动到直接在 Kotlin 代码中实现的方法。但是应该谨慎使用这个工具,因为在 Kotlin 中实现 DSL 可能有用,也可能变成过度工程。

然而,采用 Kotlin 也有一些我们不能忽视的缺点:

  • 采用另一种语言可能意味着我们将不得不长时间处理两种语言的混合代码库。Kotlin 非常擅长与 Java 交互,但有时也会出现一些怪癖。
  • Kotlin 是一门流行的语言,但与 Java 相比,流行度差距明显。Java 是世界上第二或第三流行的语言(取决于如何衡量这一点)。这意味着可用的工具更少。更糟糕的是,所有 Kotlin 工具都需要考虑 Kotlin 和 Java 的互操作性,这使得它们的实现变得复杂。

最后,我们最大的担忧是构建时间。我们从一开始就知道 Kotlin 的构建时间会比 Java 的要长。该语言及其生态系统更加复杂,Java 在优化其编译器方面领先了 20 年。由于我们拥有多个大型应用程序,较长的构建时间可能会对我们的开发人员体验产生负面影响。听到诸如OkHttp 迁移到 Kotlin 的经历等轶事描绘了一幅不太理想的画面。

。。。

我们从 Kotlin 迁移中学到了什么
随着我们工具的改进,我们已经能够将相当大的一部分代码转换成Kotlin。我们的代码库中已经有超过1000万行的Kotlin代码,Meta公司的大多数Android开发人员现在都在编写Kotlin代码。  

这种规模使我们得出了一些结论:

1、减少了代码长度
我们预期Kotlin的代码在这次迁移中会更短。有些文件确实缩短了一半(甚至更多),特别是当Java代码必须对许多字段进行空值检查时,或者简单的重复循环可以用接受lambda的标准Kotlin方法来代替,如 “first,” “single,” “any,”。

然而,我们的很多代码都是简单地传递数值的。例如,一个Litho类,它定义了UI和它的风格,无论它是在Java还是Kotlin中,都保持着同样的长度。

平均来说,我们已经看到这种迁移使代码行数减少了11%。我们在网上看到的数字要高得多,但我们怀疑这些数字是来自于具体的例子。

我们仍然对这个数字感到高兴,因为删除的行数通常是模板代码,与较短的Kotlin对应代码相比,其隐含性较低。

2、保持执行速度
由于Kotlin编译为相同的JVM字节码,我们并不期望从这次迁移中看到任何执行速度的性能退步。

为了验证这一点,我们进行了多次A/B测试,使用Kotlin的功能,如lambdas、nullability等,对Java实现和Kotlin实现进行比较。我们发现,Kotlin与Java的性能相当,正如我们预期的那样。

3、构建规模不是一个问题
Kotlin的标准库相当小,由于我们所有的版本都使用了Proguard和Redex,甚至只有部分库能进入发布的APK。因此,除了几KB的额外代码重要的情况外,大小并没有被证明是一个问题。在这些情况下,我们发现通过避开Kotlin的标准库,使用已经可用的Java方法,问题可以得到解决。例如,使用kotlin.text中的CharSequence.split与使用Java的String.split相比,会增加一些类和常量。

4、解决构建时间长的问题
我们预计Kotlin的构建时间会更长,因为它是一种相对较新的语言,与Java相比。我们猜对了,我们的开发人员注意到,随着我们在代码库中使用更多的Kotlin,构建时间也在增加。

在Kotlin编译器不断改进的同时,我们也在研究如何改善我们这边的构建时间。其中之一是在我们的Buck构建系统中支持纯源码的ABI,它可以为构建图中的依赖关系生成ABI jars,而不需要实际编译它们。这已经支持了Java,我们正在开发Kotlin版本,我们相信这将使构建图变得更平坦,并极大地提高增量构建速度。

我们调查的另一个领域是注释处理,这是一个已知的构建速度的痛点。Kotlin支持使用KAPT的注释处理器,该处理器目前处于维护模式。KAPT的工作方式是为现有的Java注释处理器代码生成一个Java代码存根来运行。这很好,因为它让你现有的代码不需要修改就能运行,但由于生成Java存根,它的速度很慢。

解决方案是使用KSP,这是一种新的、推荐的处理注释的方式。我们在Buck中增加了对它的支持,并且正在努力使用我们开发的适配器将我们现有的处理器移植到KSP中。这确实可以将运行注释处理器的成本降到最低,但前提是没有基于KAPT的处理器存在。缺点是,这需要大量的工作来更新所有的注释处理器。我们发现Room开发者的互操作库是重用现有代码的另一个选择,但每个处理器仍然需要必要的迁移工作。

详细点击标题