当前位置: 首页 > 科技观察

在Kotlin中重写AOSP日历应用

时间:2023-03-14 11:23:02 科技观察

两年前,Android开源项目(AOSP)应用团队开始使用Kotlin而非Java重构AOSP应用。重构的主要原因有两个:一是确保AOSP应用程序遵循Android最佳实践,二是提供一个先使用Kotlin进行应用程序开发的好例子。Kotlin具有强大吸引力的原因之一是其简洁的语法,在许多情况下,用Kotlin编写的代码块比功能等效的Java代码块的代码密集度要低。此外,Kotlin作为一种富有表现力的编程语言还有其他各种优势,例如:NullSafety:这个概念可以说是植根于Kotlin,从而有助于避免破坏性的NullPointerException;并发性:正如谷歌在I/O2019中对Android的描述,结构化并发可以允许使用协程来简化后台任务管理;Java的兼容性:尤其是在这个重构项目中,Kotlin和Java语言的兼容性可以让我们逐个文件地做Kotlin转换。AOSP团队去年夏天发表了一篇文章,详细介绍了AOSP桌面时钟应用程序的转换过程。今年,我们将AOSP日历应用程序从Java转换为Kotlin。在此转换之前,该应用程序有18,000多行代码,而在转换之后,代码库减少了大约300行。在本次转换中,我们沿用了AOSP桌面时钟转换过程中类似的技术,充分利用Kotlin与Java语言的互操作性,将代码文件一个一个转换,过程中使用独立构建的目标替换Java代码文件与他们的Kotlin同行。因为团队中有两个人在做这件事,所以我们在Android中为他们每个人创建了一个exclude_srcs属性。推送代码。此外,这使我们能够进行增量测试并快速查明错误发生在哪些文件中。在转换任何给定文件时,我们首先使用AndroidStudioKotlin插件中提供的自动Java到Kotlin转换工具。虽然插件成功帮我们转换了大部分代码,但还是有一些问题需要开发者手动解决。需要手动更改的部分将在本文的后续章节中列出。将每个文件转换为Kotlin后,我们手动测试日历应用程序的UI,运行单元测试,并运行兼容性测试套件(CTS)的子集进行功能验证,以确保没有额外的回归测试。自动转换后的步骤上面提到,使用自动转换工具后,有一些反复出现的问题,需要手动定位和解决。在AOSP桌面时钟一文中,对遇到的一些问题和解决方法进行了详细的描述。下面列出了AOSP日历转换过程中遇到的一些问题。用open关键字标记父类我们遇到的问题之一是Kotlin父类和子类之间的相互调用。在Kotlin中,要将类标记为可继承的,必须在类声明中添加open关键字,并且对父类中被子类重写的方法执行相同的操作。但是Java中的继承不需要使用open关键字。由于Kotlin和Java可以相互调用,所以直到大部分代码文件都转换为Kotlin才出现这个问题。例如,在下面的代码片段中,声明了一个继承自SimpleWeeksAdapter的类:classMonthByWeekAdapter(context:Context?,params:HashMap):SimpleWeeksAdapter(contextasContext,params){//methodbody}因为The代码文件转换过程是一个文件一个文件完成的,即使SimpleWeeksAdapter.kt文件完全转换为Kotlin,open关键字也不会出现在它的类声明中,这会导致错误。所以需要手动添加open关键字,这样才能继承SimpleWeeksAdapter类。这个特殊的类声明看起来像这样:openclassSimpleWeeksAdapter(context:Context,params:HashMap?){//methodbody}overridemodifier同样,子类中覆盖父类的方法也必须用override修饰符标记。在Java中,这是通过@Override注释实现的。但是,虽然Java中有对应的注解实现,但是自动转换过程并没有在Kotlin方法声明中加入override修饰符。解决方案是在所有适当的地方手动添加覆盖修饰符。在父类中重写属性在重构的过程中,我们也遇到了一个属性重写的异常问题。当子类声明了一个变量,而父类中有同名的非私有变量时,我们需要加上override修饰符。但是,即使子类的变量与父类的变量的类型不同,仍然必须加上override修饰符。在某些情况下,添加覆盖仍然不能解决问题,尤其是当子类是完全不同的类型时。实际上,如果类型不匹配,在子类的变量前加override修饰符,在父类的变量前加open关键字,会报错:typeof*propertyname*doesn'tmatchthetypeoftheoverriddenvar-property这个error是很烦人的迷惑,因为在Java中,下面的代码可以正常编译:publicclassParent{intnum=0;}classChildextendsParent{Stringnum="num";}而Kotlin中相应的代码会报上面提到的错误:classParent{varnum:Int=0}classChild:Parent(){varnum:String="num"}这个问题很有意思。我们目前通过重命名子类中的变量来避免这种冲突。上述Java代码被AndroidStudio目前提供的转码器转换为有问题的Kotlin代码,甚至被报告为bug。导入语句在我们转换的所有文件中,自动转换工具倾向于将Java代码中的所有导入语句截断到Kotlin文件的第一行。起初这导致了一些非常令人抓狂的错误,编译器在整个代码中抛出“未知引用”错误。意识到这个问题后,我们开始手动将import语句从Java粘贴到Kotlin代码文件中并单独进行转换。公开成员变量默认情况下,Kotlin会自动为类中的实例变量生成getter和setter方法。然而,有时我们希望一个变量只是一个简单的Java成员变量,这可以通过使用@JvmField注解来实现。@JvmField注解的作用是“指示Kotlin编译器不要为该属性生成getter和setter方法,并允许它作为成员变量公开访问”。此注释在包含两个静态最终变量的CalendarData类中特别有用。通过用@JvmField注解val声明的只读变量,保证这些变量可以作为成员变量被其他类访问,从而实现Java和Kotlin的兼容。对象中的静态方法在Kotlin对象中定义的函数必须用@JvmStatic标记,以允许它们通过方法名称而不是实例化从Java代码中调用。也就是说,这个注解使其具有类Java的方法行为,即可以通过类名来调用方法。根据Kotlin文档,“编译器为对象的外部类生成一个静态方法,为对象本身生成一个实例方法。”我们在Utils文件中遇到了这个问题,当转换完成后,Java类变成了Kotlin对象。随后,对象中定义的所有方法都必须用@JvmStatic标记,这允许使用Utils.method()等语法从其他文件调用。值得一提的是,在类名和方法名(即Utils.INSTANCE.method())之间使用.INSTANCE也是一种选择,但这并不适合常见的Java语法,并且需要将所有调用更改为Javastatic方法。性能评估分析所有基准测试均在具有96个内核和176GiB内存的机器上执行。该项目分析的主要指标是减少的代码行数、目标APK的文件大小、构建时间以及从启动到显示第一个屏幕的时间。除了对上述每个因素进行分析外,还收集了每个参数的数据并以表格形式呈现。代码行数减少在从Java全面切换到Kotlin后,代码行数从18,004行减少到17,729行。这比原来的Java代码大小减少了大约1.5%。虽然减少的代码量并不可观,但对于一些大型应用,这种改造可能更有效地减少代码行数,参见AOSP桌面时钟文章中的示例。使用Kotlin编写的应用程序的目标APK大小为2.7MB,使用Java编写的应用程序的目标APK大小为2.6MB。可以说这种差异基本可以忽略不计,而且由于包含了一些额外的Kotlin库,APK大小的增加其实也在意料之中。可以使用Proguard或R8优化这种大小的增加。Kotlin和Java应用程序的构建时间是通过从零开始计算10次完整构建时间的平均值(不包括异常值)计算得出的,Kotlin应用程序的平均构建时间为13分27秒,Java应用程序的平均构建时间为13分27秒应用程序。平均构建时间为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为197.7毫秒194.9毫秒。这些测试都是在Pixel3aXL设备上进行的。从这个测试结果可以得出结论,Java应用程序与Kotlin应用程序相比可能略有优势;但是,由于平均时间非常接近,这种差异几乎可以忽略不计。因此,可以说将AOSP日历应用程序转换为Kotlin并没有对应用程序的初始启动时间产生负面影响。结论将AOSP日历应用程序转换为Kotlin花费了大约1.5个月(6周),由2名实习生负责项目的实施。一旦我们对代码库更加熟悉,并且能够更好地解决反复出现的编译时、运行时和语法问题,效率肯定会更高。总体而言,这个特定项目成功地展示了Kotlin如何影响现有的Android应用程序,并且是向AOSP应用程序过渡的坚实一步。