使用 Kotlin 重写 AOSP 日历应用

2021年09月15日 阅读数:8
这篇文章主要向大家介绍使用 Kotlin 重写 AOSP 日历应用,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

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

  • 空安全: 这一律念能够说是根植于 Kotlin 之中,从而帮助避免破坏性的空指针异常;
  • 并发: 正如 Google I/O 2019 中关于 Android 的描述,结构化并发 (structured concurrency) 可以容许使用协程简化后台的任务管理;
  • 兼容 Java: 尤为是在此次的重构项目中,Kotlin 与 Java 语言的兼容性可以让咱们一个文件一个文件地进行 Kotlin 转换。

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

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

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

自动转换以后的步骤

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

用 open 关键词标记父类api

咱们遇到的问题之一是 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 类能够被继承。这个特殊的类声明以下所示:app

open class SimpleWeeksAdapter(context: Context, params: HashMap<String?, Int?>?) {//方法体}

override 修饰符jvm

一样地,子类中覆盖父类的方法也必须使用 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 了。

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 之间的兼容性。

对象中的静态方法

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

性能评估分析

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

减小的代码行数

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

目标 APK 大小

使用 Kotlin 编写的应用 APK 大小是 2.7 MB,而使用 Java 编写的应用 APK 大小是 2.6 MB。能够说这个差别基本能够忽略不计了,因为包含了一些额外的 Kotlin 库,因此 APK 体积上的增长,其实是能够预期的。这种大小的增长能够经过使用 ProguardR8 来进行优化。

编译时间

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 是一种更为安全有效的编程语言,咱们能够认为完整构建时间较慢的问题能够忽略不计。

首屏显示的时间

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

结论

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

欢迎您 点击这里 向咱们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对咱们很是重要,感谢您的支持!