使用 Kotlin 重写 AOSP 日历应用

使用 Kotlin 重写 AOSP 日历应用
www.zeeklog.com  - 使用 Kotlin 重写 AOSP 日历应用

两年前,Android 开源项目 (AOSP) 应用团队开始使用  替代 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

在转换任意给定的文件时,我们一开始先使用  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 的文件大小、构建时间和首屏从启动到显示的时间。在对上述每个因素进行分析的同时,我们还收集了每个参数的数据并以表格的方式进行了展示。

减少的代码行数

www.zeeklog.com  - 使用 Kotlin 重写 AOSP 日历应用

从 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 大小

www.zeeklog.com  - 使用 Kotlin 重写 AOSP 日历应用

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

Proguard

https://developer.android.google.cn/studio/build/shrink-code

R8

https://r8.googlesource.com/r8

编译时间

www.zeeklog.com  - 使用 Kotlin 重写 AOSP 日历应用

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

首屏显示的时间

www.zeeklog.com  - 使用 Kotlin 重写 AOSP 日历应用

我们使用了这种方法来测试应用从启动到完全显示首屏所需要的时间,经过 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 应用进行转换的路途中迈出了坚实的一步。

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

www.zeeklog.com  - 使用 Kotlin 重写 AOSP 日历应用

免费中文系列课程下载

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

☟ 即刻了解课程详情 ☟

推荐阅读

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

www.zeeklog.com  - 使用 Kotlin 重写 AOSP 日历应用

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


www.zeeklog.com  - 使用 Kotlin 重写 AOSP 日历应用
www.zeeklog.com  - 使用 Kotlin 重写 AOSP 日历应用
www.zeeklog.com  - 使用 Kotlin 重写 AOSP 日历应用

Read more

深入理解 Proxy 和 Object.defineProperty

在JavaScript中,对象是一种核心的数据结构,而对对象的操作也是开发中经常遇到的任务。在这个过程中,我们经常会使用到两个重要的特性:Proxy和Object.defineProperty。这两者都允许我们在对象上进行拦截和自定义操作,但它们在实现方式、应用场景和灵活性等方面存在一些显著的区别。本文将深入比较Proxy和Object.defineProperty,包括它们的基本概念、使用示例以及适用场景,以帮助读者更好地理解和运用这两个特性。 1. Object.defineProperty 1.1 基本概念 Object.defineProperty 是 ECMAScript 5 引入的一个方法,用于直接在对象上定义新属性或修改已有属性。它的基本语法如下: javascript 代码解读复制代码Object.defineProperty(obj, prop, descriptor); 其中,obj是目标对象,prop是要定义或修改的属性名,descriptor是一个描述符对象,用于定义属性的特性。 1.2 使用示例 javascript 代码解读复制代码//

By Ne0inhk

Proxy 和 Object.defineProperty 的区别

Proxy 和 Object.defineProperty 是 JavaScript 中两个不同的特性,它们的作用也不完全相同。 Object.defineProperty 允许你在一个对象上定义一个新属性或者修改一个已有属性。通过这个方法你可以精确地定义属性的特征,比如它是否可写、可枚举、可配置等。该方法的使用场景通常是需要在一个对象上创建一个属性,然后控制这个属性的行为。 Proxy 也可以用来代理一个对象,但是相比于 Object.defineProperty,它提供了更加强大的功能。使用 Proxy 可以截获并重定义对象的基本操作,比如访问属性、赋值、函数调用等等。在这些操作被执行之前,可以通过拦截器函数对这些操作进行拦截和修改。因此,通过 Proxy,你可以完全重写一个对象的默认行为。该方法的使用场景通常是需要对一个对象的行为进行定制化,或者需要在对象上添加额外的功能。 对比 以下是 Proxy 和 Object.defineProperty 的一些区别对比: 方面ProxyObject.defineProperty语法使用 new Proxy(target,

By Ne0inhk