发布时间:2021年8月4日 - 8分钟阅读
作者:Damian Patel和 Mahi K
两年前,安卓开源项目(AOSP)应用团队开始了将AOSP应用从Java重构为Kotlin的过程。采取这一举措有两个原因:确保AOSP应用程序遵循安卓的最佳实践,并首先提供用Kotlin开发应用程序的良好范例。此外,Kotlin最大的吸引力之一是其简洁的语法。在许多情况下,用Kotlin编写的大部分代码块与相应的功能相同的Java代码块相比要小一些。此外,这种富有表现力的编程语言还有其他各种有益的功能,例如。
- 空值安全。这个概念已经根植于Kotlin中,有助于避免毁灭性的空指针异常。
- 并发性。正如Android在Google I/O 2019上所描述的那样,结构化的并发性允许共同程序来简化后台任务管理。
- 与Java的兼容性。特别是在这个项目的背景下,Kotlin与Java编程语言的兼容性使我们能够一次进行一个文件的转换过程。
去年夏天,AOSP团队发表了一篇文章,详细介绍了AOSP DeskClock应用程序的转换过程。今年夏天,我们将AOSP日历应用程序从Java转换为Kotlin。在转换之前,该应用有超过18000行的代码。转换后,我们能够将代码库减少约300行代码。遵循与AOSP DeskClock转换类似的技术,我们利用Kotlin与Java编程语言的互操作性,逐一转换Java文件,并在转换过程中使用单独的构建目标将Java文件替换为Kotlin的对应文件。因为我们有两个人在做这个工作,所以我们还在Android.bp文件中为我们每个人创建了一个exclude_srcs属性,这样我们两个人都可以转换和推送变化,同时减少合并冲突。此外,这使我们能够逐步测试并确定哪些文件会导致错误。
为了转换任何给定的文件,我们开始使用Android Studio的Kotlin插件中的自动Java到Kotlin转换工具。虽然该插件成功地转换了大部分代码,但有几个问题是开发者可能遇到的,必须手动解决。我们必须进行的手动修改在下面的章节中描述。
在将每个文件转换为Kotlin后,我们手动测试了日历应用程序的用户界面,运行单元测试,并运行了兼容性测试套件(CTS)测试的一个子集,以验证功能并确保没有回归现象。
自动转换后采取的步骤
如前所述,在运行自动转换工具后,有一些反复出现的问题必须手动解决。AOSP DeskClock的文章详细介绍了其中一些错误以及必要的修复方法。下面是我们在转换AOSP日历时又遇到的几个问题。
开放的父类
我们遇到的问题之一是Kotlin中父类和子类的互动。在Kotlin中,要将一个类标记为可继承,你必须在类的声明中添加open关键字。对于父类中被子类重写的任何方法也是如此。在Java的继承中,open关键字是不需要的。由于Kotlin和Java的互操作性,这个问题直到大部分文件被完全转换为Kotlin才出现。 例如,下面的代码片断显示了一个继承了SimpleWeeksAdapter的声明类。
class MonthByWeekAdapter(context: Context?, params: HashMap<String?, Int?>) : SimpleWeeksAdapter(context as Context, params) {// body}
由于转换过程是一次一个文件进行的,这就导致了一个错误,因为即使是完全转换的SimpleWeeksAdapter.kt文件也不会在其类声明中出现open关键字。这需要手动添加,以便SimpleWeeksAdapter可以被继承。这个特殊的类声明看起来像这样。
open class SimpleWeeksAdapter(context: Context, params: HashMap<String?, Int?>?) {// body}
覆盖修改器
同样地,子类中的方法如果覆盖了父类中的方法,必须用覆盖修饰符来标记。在Java中,这是用@Override注解来完成的。然而,尽管看到了相应的Java注解,自动转换器还是自动给Kotlin的方法声明添加了覆盖修饰符。解决的办法是手动添加覆盖修饰符到所有适当的方法声明中。
覆盖属性
在Kotlin中,我们遇到了另一个关于重写属性的不寻常问题。当一个子类声明了一个与父类中定义的非私有变量同名的变量时,我们需要添加一个重写修饰符。然而,即使子类的变量与父类的类型不同,似乎仍然需要修改器。在某些情况下,添加覆盖修饰符并不能解决问题,特别是当子类的类型完全不同时。事实上,如果存在类型不匹配,在子类的变量上添加覆盖修饰符,并在父类的变量上添加开放,会导致另一个错误,其内容为。 属性名称*的类型与被覆盖的var-property的类型不匹配
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代码,甚至被报告为一个错误。
导入语句
在我们转换的每个文件中,自动转换器工具倾向于将Java文件中的全部导入语句列表截断到相应的Kotlin文件中的第一行。最初,这导致了一些令人沮丧的错误,编译器抱怨整个代码中的 "未知引用"。在意识到这个错误后,我们开始手动将Java文件中的导入语句复制到Kotlin文件中,并单独转换该段。
暴露字段
默认情况下,Kotlin会自动生成类中实例变量的getters和setters。然而,有些时候我们希望一个变量是一个简单的Java字段。这可以使用@JvmField注解来实现。
@JvmField注解 "指示Kotlin编译器不要为这个属性生成getters/setters,并将其作为一个字段公开"。这个注解在CalendarData类中特别有用,它包含两个静态的最终变量。通过对只读val变量使用@JvmField注解,我们确保了这些变量可以被其他类作为字段访问,从而实现了Java和Kotlin类之间的兼容性。
对象中的静态方法
在Kotlin对象中定义的函数必须用@JvmStatic标记,以便在Java代码中通过方法名而不是通过实例化来调用它们。换句话说,这个注解执行了类似Java的行为,即通过类名来调用方法。根据Kotlin文档,"编译器会在对象的包围类中生成一个静态方法,在对象本身中生成一个实例方法。" 我们在Utils文件中遇到了这个问题。一旦转换,Java类就变成了Kotlin对象。随后,所有在对象中定义的方法都必须用@JvmStatic标记,这样它们就可以在其他文件中用Utils.method()语法来调用。值得一提的是,在类名和方法名之间使用.INSTANCE(Utils.INSTANCE.method())也是一种选择;但是,它违背了普通的Java语法,需要改变所有Java静态方法的调用。
性能评估和分析
所有的基准测试都是在一台有96个内核和176GB内存的机器上进行的。本项目分析的主要指标是放弃的代码行数、目标APK大小、构建时间和初始启动屏幕显示的时间。在分析上述每个因素的同时,我们还对每个参数收集的数据进行了列表说明。
减少的代码行数
从Java完全转换到Kotlin之后,代码行数从18,004减少到17,729。这比原来的Java代码减少了大约1.5%。虽然减少的代码量并不大,但对于像本文提到的大型应用来说,这种转换可能会使代码行数减少得更多。
目标APK大小
Kotlin应用程序的APK大小为2.7 MB,而Java应用程序的APK大小为2.6 MB。可以说这一大小差异可以忽略不计,由于包含了一些小的额外的Kotlin库,这一大小的边际增加实际上是可以预期的。这种大小的增加可以通过使用Proguard或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 Calendar是一个相对较小的应用程序,仅由43个类组成。尽管清洁构建时间较慢,但Kotlin仍有其优势,这一点应予考虑。例如,如前所述,使用Kotlin通常可以保证较少的代码量,因为它的语法比Java更简明。这可以使维护Kotlin代码库更加容易。此外,作为一种隐含的更安全和更有生产力的语言,可以公平地论证较慢的清洁构建时间是可以忽略不计的。
初始显示时间
使用这种方法测试应用程序完全显示初始启动屏幕的时间,我们发现Kotlin应用程序的平均时间在10次试验后约为197.7ms,而Java应用程序的平均时间约为194.9ms。这些各自的试验都是在Pixel 3a XL设备上进行的。从这个测试中可以得出结论,与Kotlin应用程序相比,Java应用程序可能有一个微小的优势;然而,由于平均时间非常相似,时间差异可能可以忽略不计。因此,可以说,AOSP日历的Kotlin转换并没有对应用程序的初始启动时间产生负面影响。
总结
将AOSP日历应用转换为Kotlin大约花了1.5个月(6周)的时间,由2名实习生来完成这个项目。一旦我们熟悉了代码库并更善于解决反复出现的编译时、运行时和语法问题,效率肯定会提高。总的来说,这个特殊的项目成功地展示了Kotlin是如何影响现有的安卓应用的,并被证明是向其他AOSP应用转换的一个伟大的下一步。