使用 Kotlin 重写 AOSP 日历应用

2021 年 9 月 15 日 谷歌开发者

两年前,Android 开源项目 (AOSP) 应用团队开始使用 Kotlin 替代 Java 重构 AOSP 应用。之所以重构主要有两个原因: 一是确保 AOSP 应用能够遵循 Android 最佳实践,另外则是提供优先使用 Kotlin 进行应用开发的良好范例。Kotlin 之所以具有强大的吸引力,原因之一是其简洁的语法,很多情况下用 Kotlin 编写的代码块的代码数量相比于功能相同的 Java 代码块要更少一些。此外,Kotlin 这种具有丰富表现力的编程语言还具有其他各种优点,例如:

  • 空安全: 这一概念可以说是根植于 Kotlin 之中,从而帮助避免破坏性的空指针异常;

  • 并发: 正如 Google I/O 2019 中关于 Android 的描述,结构化并发 (structured concurrency) 能够允许使用协程简化后台的任务管理;
  • 兼容 Java: 尤其是在这次的重构项目中,Kotlin 与 Java 语言的兼容性能够让我们一个文件一个文件地进行 Kotlin 转换。

  • Android 开源项目 (AOSP) 应用
    https://android.googlesource.com/platform/packages/apps/

  • Kotlin
    https://kotlinlang.org/

  • Google I/O 2019
    https://developer.android.google.cn/kotlin/first


AOSP 团队在去年夏天发表了一篇文章,详细介绍了 AOSP 桌面时钟应用的转换过程 。而今年,我们将 AOSP 日历应用从 Java 转换成了 Kotlin。在这次转换之前,应用的代码行数超过 18,000 行,在转换后代码库减少了约 300 行。在这次的转换中,我们沿袭了同 AOSP 桌面时钟转换过程中类似的技术,充分利用了 Kotlin 与 Java 语言的互操作性,对代码文件一一进行了转换,并在过程中使用独立的构建目标将 Java 代码文件替换为对应的 Kotlin 代码文件。因为团队中有两个人在进行此项工作,所以我们在 Android.bp 文件中为每个人创建了一个 exclude_srcs 属性,这样两个人就可以在减少代码合并冲突的前提下,都能够同时进行重构并推送代码。此外,这样还能允许我们进行增量测试,快速定位错误出现在哪些文件。


  • AOSP 桌面时钟应用的转换过程
    https://medium.com/androiddevelopers/re-writing-the-aosp-deskclock-app-in-kotlin-76c836370cb


在转换任意给定的文件时,我们一开始先使用 Android Studio Kotlin 插件中提供的 从 Java 到 Kotlin 的自动转换工具 。虽然该插件成功帮助我们转换了大部份的代码,但是还是会遇到一些问题,需要开发者手动解决。需要手动更改的部分,我们将会在本文接下来的章节中列出。

在将每个文件转换为 Kotlin 之后,我们手动测试了日历应用的 UI 界面,运行了单元测试,并运行了 Compatibility Test Suite (CTS) 的子集来进行功能验证,以确保不需要再进行任何的回归测试。


  • Android Studio
    https://developer.android.google.cn/studio

  • 从 Java 到 Kotlin 的自动转换工具
    https://developer.android.google.cn/kotlin/add-kotlin#convert

  • Compatibility Test Suite (CTS)
    https://source.android.google.cn/compatibility/cts



自动转换之后的步骤


上面提到,在使用自动转换工具之后,有一些反复出现的问题需要手动定位解决。在 AOSP 桌面时钟文章中,详细介绍了其中遇到的一些问题以及解决方法。如下列出了一些在进行 AOSP 日历转换过程中遇到的问题。


用 open 关键词标记父类

我们遇到的问题之一是 Kotlin 父类和子类之间的相互调用。在 Kotlin 中,要将一个类标记为可继承,必须得在类的声明中添加 open 关键字,对于父类中被子类覆盖的方法也要这样做。但是在 Java 中的继承是不需要使用到 open 关键字的。由于 Kotlin 和 Java 能够相互调用,这个问题直到大部分代码文件转换到了 Kotlin 才出现。

例如,在下面的代码片段中,声明了一个继承于 SimpleWeeksAdapter 的类:
class MonthByWeekAdapter(context: Context?, params:    HashMap<String?, Int?>) : SimpleWeeksAdapter(context as Context, params) {//方法体}

由于代码文件的转换过程是一次一个文件进行的,即使是完全将 SimpleWeeksAdapter.kt 文件转换成 Kotlin,也不会在其类的声明中出现 open 关键词,这样就会导致一个错误。所以之后需要手动进行 open 关键词的添加,以便让 SimpleWeeksAdapter 类可以被继承。这个特殊的类声明如下所示:
open class SimpleWeeksAdapter(context: Context, params: HashMap<String?, Int?>?) {//方法体}


override 修饰符

同样地,子类中覆盖父类的方法也必须使用 override 修饰符来进行标记。在 Java 中,这是通过 @Override 注解来实现的。然而,虽然在 Java 中有相应的注解实现版本,但是自动转换过程中并没有为 Kotlin 方法声明中添加 override 修饰符。解决的办法是在所有适当的地方手动添加 override 修饰符。


覆写父类中的属性

在重构过程中,我们还遇到了一个属性覆写的异常问题,当一个子类声明了一个变量,而在父类中存在一个非私有的同名变量时,我们需要添加一个 override 修饰符。然而,即使子类的变量同父类变量的类型不同,也仍然要添加 override 修饰符。在某些情况下,添加 override 仍不能解决问题,尤其是当子类的类型完全不同的时候。事实上,如果类型不匹配,在子类的变量前添加 override 修饰符,并在父类的变量前添加 open 关键字,会导致一个错误:
type of *property name* doesn’t match the type of the overridden var-property

这个报错很让人疑惑,因为在 Java 中,以下代码可以正常编译:
public class Parent {    int num = 0;}
class Child extends Parent { String num = "num";}


而在 Kotlin 中相应的代码就会报上面提到的错误:
class Parent {    var num: Int = 0}
class Child : Parent() { var num: String = "num"}


这个问题很有意思,目前我们通过在子类中对变量重命名来规避了这个冲突。上面的 Java 代码会被 Android Studio 目前提供的代码转换器转换为有问题的 Kotlin 代码,这甚至被报告为是一个 bug 了。


  • 被报告为是一个 bug

    https://youtrack.jetbrains.com/issue/KTIJ-8621


import 语句

在我们转换的所有文件中,自动转换工具都倾向于将 Java 代码中的所有 import 语句截断为 Kotlin 文件中的第一行。最开始这导致了一些很让人抓狂的错误,编译器会在整个代码中报 "unknown references" 的错误。在意识到这个问题后,我们开始手动地将 Java 中的 import 语句粘贴到 Kotlin 代码文件中,并单独对其进行转换。


暴露成员变量

默认情况下,Kotlin 会自动地为类中的实例变量生成 getter 和 setter 方法。然而,有些时候我们希望一个变量仅仅只是一个简单的 Java 成员变量,这可以通过使用 @JvmField 注解来实现。


@JvmField 注解的作用是 "指示 Kotlin 编译器不要为这个属性生成 getter 和 setter 方法,并将其作为一个成员变量允许其被公开访问"。这个注解在 CalendarData 类中特别有用,它包含了两个 static final 变量。通过对使用 val 声明的只读变量使用 @JvmField 注解,我们确保了这些变量可以作为成员变量被其他类访问,从而实现了 Java 和 Kotlin 之间的兼容性。


  • @JvmField 注解
    https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-field/

  • CalendarData 类
    https://android.googlesource.com/platform/packages/apps/Calendar/+/42e4b43133c4f866e0729438fb38bebc6d03b0a4/src/com/android/calendar/CalendarData.kt

  • val
    https://kotlinlang.org/docs/basic-syntax.html#variables


对象中的静态方法

在 Kotlin 对象中定义的函数必须使用 @JvmStatic 进行标记,以允许在 Java 代码中通过方法名,而非实例化来对它们进行调用。也就是说,这个注解使其具有了类似 Java 的方法行为,即能够通过类名调用方法。根据 Kotlin 的文档 ,"编译器会为对象的外部类生成一个静态方法,而对于对象本身会生成一个实例方法。"我们在 Utils 文件 中遇到了这个问题,当完成转换后,Java 类就变成了 Kotlin 对象。随后,所有在对象中定义的方法都必须使用 @JvmStatic 标记,这样就允许在其他文件中使用 Utils.method() 这样的语法来进行调用。值得一提的是,在类名和方法名之间使用 .INSTANCE (即 Utils.INSTANCE.method() ) 也是一种选择,但是这不太符合常见的 Java 语法,需要改变所有对 Java 静态方法的调用。

  • Kotlin 的文档
    https://kotlinlang.org/docs/java-to-kotlin-interop.html#static-methods

  • Utils 文件
    https://android.googlesource.com/platform/packages/apps/Calendar/+/42e4b43133c4f866e0729438fb38bebc6d03b0a4/src/com/android/calendar/Utils.kt



性能评估分析


所有的基准测试都是在一台 96 核、176 GiB 内存的机器上进行的。本项目中分析用到的主要指标有所减少的代码行数、目标 APK 的文件大小、构建时间和首屏从启动到显示的时间。在对上述每个因素进行分析的同时,我们还收集了每个参数的数据并以表格的方式进行了展示


减少的代码行数

从 Java 完全转换到 Kotlin 后,代码行数从 18,004 减少到了 17,729。这比原来的 Java 代码量减少了大约 1.5%。虽然减少的代码量并不可观,但对于一些大型应用来说,这种转换对于减少代码行数的效果可能更为显著,可参阅 AOSP 桌面时钟 文中所举的例子。


  • AOSP 桌面时钟
    https://medium.com/androiddevelopers/re-writing-the-aosp-deskclock-app-in-kotlin-76c836370cb

目标 APK 大小

使用 Kotlin 编写的应用 APK 大小是 2.7 MB,而使用 Java 编写的应用 APK 大小是 2.6 MB。可以说这个差异基本可以忽略不计了,由于包含了一些额外的 Kotlin 库,所以 APK 体积上的增加,实际上是可以预期的。这种大小的增加可以通过使用 Proguard R8 来进行优化。

  • Proguard
    https://developer.android.google.cn/studio/build/shrink-code
  • R8
    https://r8.googlesource.com/r8

编译时间

Kotlin 和 Java 应用的构建时间是通过取 10 次从零进行完整构建的时间的平均值来计算的 (不包含异常值),Kotlin 应用的平均构建时间为 13 分 27 秒,而 Java 应用的平均构建时间为 12 分 6 秒。据一些资料 (如 " Java 和 Kotlin 的区别 " 以及 " Kotlin 和 Java 在编译时间上的对比 ") 显示,Kotlin 的编译时间事实上比 Java 要更耗时,特别是对于从零开始的构建。一些分析断言,Java 的编译速度会快 10-15%,又有一些分析称这一数据为 15-20%。拿我们的例子进行从零开始完整构建所花费的时间来说,Java 的编译速度比 Kotlin 快 11.2%,尽管这个微小的差异并不在上述范围内,但这有可能是因为 AOSP 日历是一个相对较小的应用,仅有 43 个类。尽管从零开始的完整构建比较慢,但是 Kotlin 仍然在其他方面占有优势,这些优势更应该被考虑到。例如,Kotlin 相对于 Java,更简洁的语法通常可以保证较少的代码量,这使得 Kotlin 代码库更易维护。此外,由于 Kotlin 是一种更为安全有效的编程语言,我们可以认为完整构建时间较慢的问题可以忽略不计。

  • Java 和 Kotlin 的区别

    https://www.educba.com/java-vs-kotlin/

  • Kotlin 和 Java 在编译时间上的对比
    https://medium.com/keepsafe-engineering/kotlin-vs-java-compilation-speed-e6c174b39b5d

首屏显示的时间

我们使用了这种 方法 来测试应用从启动到完全显示首屏所需要的时间,经过 10 次试验后我们发现,使用 Kotlin 应用的平均时间约为 197.7 毫秒,而 Java 的则为 194.9 毫秒。这些测试都是在 Pixel 3a XL 设备上进行的。从这个测试结果可以得出结论,与 Kotlin 应用相比,Java 应用可能具有微小的优势;然而,由于平均时间非常接近,这个差异几乎可以忽略不计。因此,可以说 AOSP 日历应用转换到 Kotlin,并没有对应用的初始启动时间产生负面影响。


  • 方法
    https://developer.android.google.cn/topic/performance/vitals/launch-time#time-initial


结论


将 AOSP 日历应用转换为 Kotlin 大约花了 1.5 个月 (6 周) 的时间,由 2 名实习生负责该项目的实施。一旦我们对代码库更加熟悉并更加善于解决反复出现的编译时、运行时和语法问题时,效率肯定会变得更高。总的来说,这个特殊的项目成功地展示了 Kotlin 如何影响现有的 Android 应用,并在对 AOSP 应用进行转换的路途中迈出了坚实的一步。


欢迎您通过下方二维码向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!






免费中文系列课程下载

系统地学习使用 Kotlin 进行 Android 开发


☟ 即刻了解课程详情 ☟



推荐阅读

如页面未加载,请刷新重试



 点击屏末  | 即刻了解更多 Android 开源项目 (AOSP) 应用相关信息




登录查看更多
0

相关内容

Android Open-Source Project
【2021新书】面向对象的Python编程,418页pdf
专知会员服务
70+阅读 · 2021年12月15日
专知会员服务
98+阅读 · 2020年12月19日
【2020新书】实战R语言4,323页pdf
专知会员服务
100+阅读 · 2020年7月1日
【2020新书】如何认真写好的代码和软件,318页pdf
专知会员服务
63+阅读 · 2020年3月26日
Android 13 首个开发者预览版到来
谷歌开发者
0+阅读 · 2022年2月12日
Android Studio 新特性详解
谷歌开发者
0+阅读 · 2022年1月19日
新版本系统适配: Android 12 中的兼容性变更
谷歌开发者
0+阅读 · 2022年1月13日
Kotlin,竟然这么香!| 极客时间
InfoQ
0+阅读 · 2022年1月5日
正式版 API 确定 | Android 12L Beta 1 发布
谷歌开发者
0+阅读 · 2021年12月9日
精彩回顾 | 2021 Android 开发者峰会
谷歌开发者
0+阅读 · 2021年12月6日
Room & Kotlin 符号的处理
谷歌开发者
0+阅读 · 2021年11月4日
使用 Kotlin Symbol Processing 1.0 缩短 Kotlin 构建时间
谷歌开发者
0+阅读 · 2021年10月8日
正式版即将到来 | Android 12 Beta 5 现已发布
谷歌开发者
0+阅读 · 2021年9月9日
国家自然科学基金
1+阅读 · 2014年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
Arxiv
0+阅读 · 2022年4月19日
VIP会员
相关资讯
Android 13 首个开发者预览版到来
谷歌开发者
0+阅读 · 2022年2月12日
Android Studio 新特性详解
谷歌开发者
0+阅读 · 2022年1月19日
新版本系统适配: Android 12 中的兼容性变更
谷歌开发者
0+阅读 · 2022年1月13日
Kotlin,竟然这么香!| 极客时间
InfoQ
0+阅读 · 2022年1月5日
正式版 API 确定 | Android 12L Beta 1 发布
谷歌开发者
0+阅读 · 2021年12月9日
精彩回顾 | 2021 Android 开发者峰会
谷歌开发者
0+阅读 · 2021年12月6日
Room & Kotlin 符号的处理
谷歌开发者
0+阅读 · 2021年11月4日
使用 Kotlin Symbol Processing 1.0 缩短 Kotlin 构建时间
谷歌开发者
0+阅读 · 2021年10月8日
正式版即将到来 | Android 12 Beta 5 现已发布
谷歌开发者
0+阅读 · 2021年9月9日
相关基金
国家自然科学基金
1+阅读 · 2014年12月31日
国家自然科学基金
0+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
Top
微信扫码咨询专知VIP会员