作者简介
禹昂,携程移动开发专家,Google 开发者专家(Android),上海 Kotlin User Group 组织者,图书《Kotlin 编程实践》译者。
2022 年底,我们在携程的 Github organization 下开源了 SQLlin,SQLlin 是一款基于 Kotlin DSL 及 KSP 技术的,支持众多平台的 Kotllin Multipllatform SQLite 数据库框架。感兴趣且不了解 SQLlin 的读者可以参考:《携程机票跨端 Kotlin DSL 数据库框架 SQLlin》一文。
SQLlin作为携程机票移动端团队最为完备的一款开源项目,在接近 1 年的时间内经历了不少升级与换血式的更新,也见证了这一年 Kotlin Multiplatform 技术的演进及社区生态的变化。本文将带领大家梳理这些更新,并探求这些更新背后所涉及到的 Kotlin Multiplatform 技术栈在这一年来的更迭与进化。
一、重写 native 驱动层
我们先来回顾一下最初的 SQLlin 架构图:
最初,SQLlin 在 Kotlin/Native 平台上基于开源项目 SQLiter(见参考链接 1),目的是避免重复造轮子。虽然 SQLliter 是来自 Touchlab的优秀开源项目,但最近一年维护更新缓慢。在本文撰写时,SQLiter 于 2023 年 11 月发布了 1.3.0 和 1.3.1 两个版本(1.3.1升级到了 Kotlin 1.9.21,用于修复 1.9.20 的 Kotlin/Native 库版本号相关的问题)。但在这之前的版本,即 1.2.1 发布于 2022年 8 月,基于 Kotlin 1.6.20,一年以上没有更新。对于 2023 年的项目来说,1.6.20 过于老旧。老旧的版本导致了如下一些问题。
1.1 Targets 更新维护不及时
Kotlin 在 1.8.20 版本废弃了一众 32 位 Kotlin/Native targets(目标平台),包括:iosArm32、watchosX86、wasm32、mingwX86、linuxArm32Hfp、linuxMips32、linuxMipsel32。这些目标平台几乎已经完全被淘汰,市面上已经极少有可以运行这些targets 的设备,继续支持已无意义。因此 Kotlin 决定将这些 targets 标记为“deprecated”,并在 1.9.20 版本将它们完全移除。
这些即将被移除的 targets 中,iosArm32、watchosX86、mingwX86 受到 SQLiter 及 SQLlin 的支持。由于 SQLiter 不更新版本,所以这些 targets 将继续存在于 SQLiter 当中,虽然 sqllin-driver 可以在上层移除对这些平台的支持,但长久来说由于编译器版本的更迭,仍然不是最佳做法。
如果说在 sqllin-driver 中移除对旧编译目标的支持可以暂时解决“废弃旧 targets 不及时”的问题,那么“对新 targets 的支持”则无计可施。
Kotlin 在 1.8.0 版本开始支持 watchosDeviceArm64 新目标平台,对应于全新的 64 位 Apple Watch 设备。虽然可以预见使用 Kotlin Multiplatform 技术开发 Apple Watch 应用的开发者不会很多,但 SQLlin 原本支持所有的 watchOS 相关 targets,不支持最新的 Arm64 架构并不合理。由于 SQLiter 不支持 watchosDeviceArm64,因此 SQLlin 也无法支持。
1.2 Bug 无法及时修复
在 SQL 中我们会遇到一个常见的用法——join,在 join 查询时遇到两个表拥有相同名字的列也是常见现象。在 SQLiter的原始实现中,后查询出来的同名列值会覆盖掉先查询出来的同名列值:
override val columnNames: Map<String, Int> by lazy {
val map = HashMap<String, Int>(this.columnCount)
for (i in 0 until columnCount) {
val key = columnName(i)
if (map.containsKey(key)) {
var index = 1
val basicKey = "$key&JOIN"
var finalKey = basicKey + index
while (map.containsKey(finalKey)) {
finalKey = basicKey + ++index
}
map[finalKey] = i
} else {
map[key] = i
}
}
map
}
之后,我于 2022 年 12 月提交了一个 PR 以修复此问题(参考链接 2),但 SQLliter 的维护者没有任何回复,同样是直到 2023 年 11 月才合并该 PR。
无法支持的新平台导致有刚需的用户无法继续使用 SQLlin,而无法修复的问题导致了特定场景必定出错的硬伤。一年没有任何维护让我对 SQLiter 感到疑虑,此时自行实现已经变成了必然选择。
1.3 Native 驱动层重写
重写 Native 驱动层并不困难,我们可以参考 SQLiter 的不少设计理念。
首先,SQLite 在不同的 Native 平台上都提供相同的 C API,所以我们绝大部分代码是平台(这里特指 Kotlin/Native 的诸多目标平台)无关的。根据官方 KMP 工程的架构约定,这部分平台无关的代码可以全部放在 nativeMain source set 下。但由于我们构建了一套面向对象风格的 API,加上需要处理例如线程同步等问题,因此还是会依赖一些系统平台 API。比如说如果要在 nativeMain 中使用线程锁,需要用 expect 关键字定义待实现的API,在各平台相关 source set 中使用 actual 关键字定义相关实现。比如说在 Apple 平台上我们使用 Apple Foundation 中的 Objective-C 类 NSRecursiveLock,而在 Linux 和Windows 平台上则使用 Posix C 中的 pthread_mutex_xxx 系列 C API。
我们将 SQLite 的 C 库头文件放在 include 路径下(与 nativeMain 平级),然后编写 .def 文件并放在 nativeInterop 路径下(同样与 nativeMain 平级),然后在 build.gradle.kts 文件中配置头文件的路径以及 SQLite C 库的 linkerOpts(编译链接参数),即可在所有 native 相关的 sourceSet 中调用 SQLite C 函数,build.gradle.kts 中的配置如下:
fun KotlinNativeTarget.setupNativeConfig() {
val main by compilations.getting
val sqlite3 by main.cinterops.creating {
includeDirs("$projectDir/src/include")
}
binaries.all {
linkerOpts += when {
HostManager.hostIsLinux -> listOf("-lsqlite3", "-L$rootDir/libs/linux", "-L/usr/lib/x86_64-linux-gnu", "-L/usr/lib", "-L/usr/lib64")
HostManager.hostIsMingw -> listOf("-Lc:\\msys64\\mingw64\\lib", "-L$rootDir\\libs\\windows", "-lsqlite3")
else -> listOf("-lsqlite3")
}
}
}
这是一个 native 目标平台可调用的扩展函数,使所有 native targets 都调用它即可。其中 linkerOpts 在 Linux 和 Windows 平台上都指向常见的 SQLite 安装路径(使用常见的包管理器),但为了确保 native 单元测试可以顺利在任何 Linux 或 Windows host 上运行,SQLlin 的源码目录中实际上附带了针对 Linux 及 Windows 的 SQLite .a 库,因此当链接过程无法在常见路径下找到 SQLite .a文件时,最终会链接到 SQLlin 源码路径下的版本。
但再次强调,以上场景仅限单元测试,如果你是使用 SQLlin 的应用开发者,且你的应用支持 Linux 和 Windows,需要确保用户的电脑安装了SQLite,或者在应用程序工程中附带 SQLite C 库,并自行添加 linkerOpts 链接到 SQLite .a 文件。至于 Apple 相关平台(iOS、macOS、watchOS、tvOS),系统框架中已经自带了SQLite,因此不必担心以上问题,sqllin-driver 中添加的编译链接参数可以正确链接到系统框架中自带的版本。最后我们来看一下 nativeMain 下的源码结构:
cinterop 包包含所有对 SQLite C 函数直接互操作的代码,通过单独的包将其与其它代码隔离;platform 包则存放所有待平台实现的相关代码,真正的实现则位于 appleMain、linuxMain、mingwMain 几个 source sets 中;其余代码是 sqllin-driver-native 的核心实现,都位于根目录包下。
二、JVM Target 支持
起初,根据预测,我认为使用 Kotlin Multiplatform 技术开发 JVM 桌面应用的人并不多。但由于 Compose Multiplatform 最初支持的平台便是 Android 与 JVM,因此吸引了大量 Kotlin Multiplatform 开发者将自己的多平台应用的支持范围扩展到 JVM。在部分用户提交了一些 issue(参考链接 3)后,我决定着手进行 JVM 平台的支持工作。而支持 JVM 平台也有助于调研将 SQLlin 支持的数据库扩展到 MySQL、H2、Oracle 等后端数据库的可能性,因为它们都基于 JDBC。
JVM 平台的实现基于 SQLite 官方的 JVM driver:sqlite-jdbc,库的使用者通过 JDBC 连接到 sqlite-jdbc,而 sqlite-jdbc 底层则通过 JNI 操作 SQLite C 库。由于 sqlite-jdbc本身就是 Java 库,因此 API 的抽象程度比 native 平台上直接调用 C API 高的多。所以 jvmMain 中的代码实现比 nativeMain 要简单很多。
但也有几个点值得一提:
首先,Windows平台上的文件路径分隔符是 ‘\’,而 Linux 和 macOS 上都是 ‘/’,因此在处理用户传入的路径参数时,即使是在 jvmMain 中也要判断当前运行的操作系统是不是 Windows。
其次,由于sqlite-jdbc 中没有对 sqlite3_config C 函数的调用,因此目前 lookasideSlotSize 和 lookasideSlotCount 两个参数在 JVM 平台上无法生效,后续我计划通过提交 PR 的方式参与sqlite-jdbc 的开发,使其支持 sqlite3_config,但目前还没有具体的时间表。
当然,支持 JVM 平台的开发过程还遇到过其他的细节问题,例如表示查询结果集的 java.sql.ResultSet 类型起始下标是 1 而不是 Android 平台 android.database.Cursor 和 Native 平台 C API 中的 0。不过这类问题都较为容易处理,在此不多做赘述。
在重写了 native 平台的 driver 和支持了 JVM 平台后,SQLlin 的架构图如下所示:
目前 SQLlin 支持的完整目标平台列表如下:
- Multiplatform Common
- Android (6.0+)
- JVM (Java 11+, since 1.2.0)
- iOS (x64, arm64, simulatorArm64)
- macOS (x64, arm64)
- watchOS (x64, arm32, arm64, simulatorArm64, deviceArm64)
- tvOS (x64, arm64, simulatorArm64)
- Linux (x64, arm64)
- Windows (mingwX64)
三、sqllin-dsl 并发安全
sqllin-driver 作为低阶 SQLite 框架,可以通过 SQLite 本身的线程安全机制来实现一定程度上的线程安全,我写过一篇文章《关于 SQLite 多线程行为的结论》讨论过相关知识。
简而言之,在多数情况下 SQLite 的默认线程模式都是:Multi-thread,在单连接多线程的情况下是可以保证线程安全的。因此我们只需尽量避免多连接多线程的情形即可,将同一个连接在多个线程间共享是个好方法。
现在我们来回顾一下 sqllin-dsl 的基本用法,以便理解本节接下来的内容:
private val db by lazy { Database(name = "person.db", path = path, version = 1) }
fun sample() {
val tom = Person(age = 4, name = "Tom")
val jerry = Person(age = 3, name = "Jerry")
val jack = Person(age = 8, name = "Jack")
val selectStatement: SelectStatement<Person> = db {
PersonTable { table ->
table INSERT listOf(tom, jerry, jack)
table UPDATE SET { age = 5; name = "Tom" } WHERE ((age LTE 5) AND (name NEQ "Tom"))
table DELETE WHERE ((age GTE 10) OR (name NEQ "Jerry"))
table SELECT WHERE (age LTE 5) GROUP_BY age HAVING (upper(name) EQ "TOM") ORDER_BY (age to DESC) LIMIT 2 OFFSET 1
}
}
selectStatement.getResult().forEach { person ->
println(person.name)
}
}
在 sqllin-dsl 中,一个 Database 对象中只会建立一个数据库链接。但上述示例中如果我们将对象 db(类型为 Database)在多个线程(或运行在不同线程上的协程)中共享,几乎必然会出现问题。
原因在于 Database 对象内部使用一个双向链表来进行一组 SQL 语句的构建,一个 Database 对象持有一个双向链表,每次子句的连接都会直接拼接到链表头部的 SQL语句上,而当 SQL 语句组执行完毕后链表会被清空。
如果在多个线程/协程中同事使用 db 对象,可以想象这可能会出现 SQL 语句拼接混乱的问题,例如线程 A 和 线程 B 都在构建自己的SQL 语句,由于没有同步机制,线程 B 中的子句可能被拼接到线程 A 中已经创建出的 SQL 语句后面,造成 SQL 语法错误。也有可能出现线程 A 还在构建 SQL 语句,但线程 B 已经进入SQL 语句执行阶段,线程 B 很可能会将还未构建完成的 SQL 语句传给 SQLite,造成运行错误。
SQLlin 最初之所以没有设计线程同步机制主要是基于 Kotlin 版本的考量。在 SQLlin 第一个版本发布的 Kotlin 1.7.20 时期,Kotlin/Native new Memory Management(新内存管理器,后文简称 new MM)还未进入正式版,不少开发者还在使用旧内存管理器。在 Kotlin/Native 的旧内存模型中,对象是不能直接跨线程访问的,必须要手动进行对象子图分离和再绑定操作,对象才能将自己的所有权转移到另一个线程,这种设计其实是强制开发者在编译期就保证对象在同一时刻只能被一个线程访问。
关于旧内存模型在本人以往的文章中讨论过很多次,并且在当下 Kotlin 1.9.20 时代已经被彻底淘汰,这里也不再过多讨论。基于以上的时代背景,在不能确定用户是否使用新内存管理器的情况下,做线程同步的设计非常困难,因此最好的方式就是不处理,并且建议用户不要在多线程间共享 Database 对象。但如今 2023 年末,在 Kotlin 1.9.2x 版本作为最新版本的背景下,new MM早已经被绝大部分开发者所使用,因此此时基于 new MM 的设计进行线程同步机制的开发非常合适。
在 sqllin-dsl 新版本的设计中,新增了挂起函数 API suspendScope,用于在并发环境下取代 operator 函数 invoke,并且管理 SQL 语句构建的双向链表被改成成员变量,只有在每次invoke 或 suspendScope 函数被调用时才创建,在 SQL 语句执行完毕后会被就会被抛弃。由于函数调用栈是线程私有的,因此这样的设计可以在不同的线程同时构建 SQL语句时隔离运行,既提高效率又保证了线程安全。
在 SQL 语句运行阶段,由于每次 SQL 语句构建完毕后执行的都是一组 SQL,为了避免不同线程同时执行 SQL语句时的顺序的不确定性,例如线程 A 需要执行 SQL 语句 a、b、c,线程 B 需要执行 SQL 语句 d、e、f,不加任何同步机制同时执行可能会导致 a、b、c、d、e、f的执行顺序不确定,从而导致不可预知的问题,因此 SQL 语句执行阶段必须加入协程锁 Mutex 来保证并发安全,suspendScope 的实现如下:
private val executiveMutex by lazy { Mutex() }
public suspend infix fun <T> suspendedScope(block: suspend DatabaseScope.() -> T): T {
val databaseScope = DatabaseScope(databaseConnection, enableSimpleSQLLog)
val result = databaseScope.block()
executiveMutex.withLock {
databaseScope.executeAllStatements()
}
return result
}
由于使用了协程锁 Mutex,因此自 1.2.2 版本起, sqllin-dsl 依赖 Kotlin 官方协程框架 kotlinx.coroutines。
四、Android 低版本向下兼容
Android 系统曾在 API 28(Android 9)版本对 framework 中的 SQLite Java APIs 进行了一次升级,这次升级提供了许多新 API 可以让开发者对 SQLite进行具体的参数配置,这些参数包括:日志模式、同步模式、连接超时时间、lookaside memory,这在之前的版本都是不可以的。由于 SQLlin 最低支持的Android 版本是 API 23(Android 6),因此在 Android 9 以下的设备上,以上提到的参数都无法生效。
但最初的认知并不准确,因为日志模式、同步模式两个参数都使用 PRAGMA 语句配置,因此只需要在 sqllin_driver 内自行构建 PRAGMA 语句并执行,即可在旧Android 系统上也能进行日志模式与同步模式的设置。因此,自 1.2.0 版本起,SQLlin 在旧 Android 设备上也支持设置日志模式与同步模式。但基于 SQLite C API才能配置的连接超时时间和 lookaside memory 仍然无法在旧设备上生效。
五、CI/CD 优化
在 SQLlin 开源之初没有进行 CI/CD 环境的搭建。CI/CD 对于验证 push、PR 的准确性,保证版本发布的 bug 率等方面具有重要意义。同时也是向 MavenCentral发布新版本的最佳途径。起初的发布都在本人的工作电脑上进行(Macbook Pro),由于 Mac 电脑的 Kotlin/Native 编译器不支持编译 Windows 平台的产物,导致1.0 版本的 SQLlin 不支持 MinGW 目标平台。
在 2023 年 1 月,SQLlin 第一个版本的 CI/CD pipeline 上线。此后经过持续的优化,如今已经进入较为完备的体系和状态。在搭建、优化的过程中,我认为以下几点内容颇为重要:
5.1 单元测试/仪器测试原则
单元测试对任何项目都具有重要意义,可以在一定程度上验证代码的修改不会导致原有预期行为的改变,因此单元测试是 CI/CD 流程中的关键步骤。我们可以先回看“二. JVMTarget 支持”一节中的 SQLlin 最终架构设计图,SQLlin 在任何一个平台上运行在底层都会涉及平台相关代码,因此单元测试必须覆盖所有平台相关代码。
例如,如果我们只在 macOS机器上执行单元测试,可以保证平台无关代码(sqllin-dsl、sqllin-processor、sqllin-driver(commonMain))以及 macOS 平台相关代码(sqllin-driver(nativeMain、appleMain))的正确性,但是无法验证其他平台相关的代码,例如 sqllin-driver 中的 androidMain、jvmMain、linuxMain、mingwMain。
所以我们有必要在 Linux 和 Mac 机器上同时执行Kotlin/Native 单元测试,但没有必要分别在 iOS 和 macOS 上执行 Kotlin/Native 单元测试,因为所有 Apple 平台的相关代码都在 appleMain source set 下,iOS 和 macOS上运行的 SQLlin 代码没有任何区别,保证相同的代码在 iOS 和 macOS 运行得到相同的结果是 Kotlin 编译器需要保证的事情,而不是库开发者。JVM 单元测试比较特殊,需要在三台机器上都运行,因为文件路径在三种不同的操作系统上的表示不同,这部分代码的区别可能就几个字符,但既然不是 100% 相同,那么就还是需要分别测试。
根据以上原则,我们需要执行的单元测试如下:
- Kotlin/JVM: JVM Unit Tests (Mac, Linux, Windows), Android Instrumented Tests (Android 9 以下版本,及最新 Android 版本)
- Kotlin/Native: macOS x64 Unit Tests, Linux x64 Unit Tests, MinGW x64 Unit Tests
5.2 合理的 Host 分配
Kotlin 支持众多平台,这里的平台是广义的,其中既包括操作系统原生产物,又包括一些非原生开发环境。比如 WASM、JavaScript、JVM、Android就属于非原生开发环境。WASM、JavaScript、JVM 这些技术的出现本身就是为了跨平台(这里是狭义的“平台”,特指各操作系统),而 Android 的 ART则是一个“非标准”的 JVM,这些编译产物的运行能力由其相对应的平台本身提供,不依赖特定 CPU 架构或操作系统 API,因此在任何机器上都能编译构建。
但Kotlin/Native 编译出的操作系统原生产物则不同,首先,所有的 Apple 平台(iOS、macOS、watchOS、tvOS)的编译构建都依赖 Xcode 命令行工具,而Apple 只提供 macOS 版本的 Xcode,因此,一个 Kotlin Multiplatform 应用或库如果要支持 Apple 平台,必须使用 Mac 电脑开发和构建;其次,由于Kotlin/Native 在 Windows 平台上依赖 MinGW,至少 Kotlin 1.7.20 之前的版本如果要构建 Windows 产物就必须使用 Windows 电脑,但在 1.7.20之后的某个版本开始,官方悄无声息的支持了 Mac 电脑编译 mingwx64 产物;而 Linux 系统的产物 Mac 电脑一直可以构建。SQLlin 支持的全部平台已经在“二. JVMTarget 支持”一节中详细列出。因此看似只需一台 Mac 电脑即可完成全部的 CI/CD 任务。
但我们必须确保 CI/CD 中的单元测试可以符合 5.1 小节中的原则。macOS 虽然可以编译构建 Linux 和 Windows 平台产物,但是无法执行这些平台的单元测试。所以我们至少需要Mac、Windows、Linux 三台机器来完成整个 CI/CD 过程。三台机器需要构建的产物如下:
- Mac:Android, JVM, iosX64, iosArm64, iosSimulatorArm64, macosX64, macosArm64, wachosX64, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosDeviceArm64, tvosX64, tvosX64, tvosArm64, tvosSimulatorArm64
- Windows: JVM, mingwX64
- Linux: Android, JVM, linuxX64, linuxArm64
仅从编译构建来看,Mac 的任务最重,Windows 的任务最轻。但没有办法,所有的 Apple 产物都只能在 Mac 上构建。为了尽量缩短各平台的 CI/CD pipeline运行过程的时间差以节省总时间,我们尽量合理分配一下单元测试任务。各平台执行的单元测试任务如下所示:
- Mac: macOS x64 Unit Tests, JVM Unit Tests, Android Instrumented Tests (Android 13)
- Windows: MinGW x64 Unit Tests, JVM Unit Tests
- Linux: Linux x64 Unit Tests, JVM Unit Tests, Android Instrumented Tests (Android 8)
实际上 native 和 JVM 单元测试的流程都非常快,但 Android 仪器测试的流程非常耗时(耗时甚至可能接近整个 CI/CD 流程耗时的一半),因为准备(没有缓存的话要创建)Android 模拟器非常耗时,连接Android 模拟器的测试过程也非常耗时,因此将两个不同版本的 Android 仪器测试分配到不同的机器上是非常有必要的,这也是为什么 Linux 机器上也要构建一次 Android 产物的原因。
5.3 缓存
由于每次执行 CI/CD 时,Github Actions 总是分配空闲的机器给你的项目运行 pipeline,因此每次 pipeline 执行完毕后,流程中下载的构建工具、依赖库、编译产物,以及创建的 Android模拟器都会被清除。在没有任何缓存的情况下每次重新运行 pipeline 会浪费大量时间。因此配置缓存策略是节省 CI/CD 运行时间的诀窍之一。
我们主要需要缓存的东西有三个:下载的构建工具、创建好的 Android 模拟器、Gradle 构建产物。一些和缓存有关的 yml 脚本中的 steps 代码如下:
- name: Cache Build Tooling
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.konan
key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }}
- name: Gradle Cache
uses: gradle/gradle-build-action@v2
- name: AVD Cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-33
- name: Create AVD and Generate Snapshot for Caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
target: google_apis
arch: x86_64
profile: pixel_6
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
实际效果也非常好,使用缓存之前整个 CI/CD 流程执行结束可能需要 26 分钟以上,使用缓存后降低至 10 分钟出头。其实可以想象每次我们在电脑上下载 Android 模拟器所需的镜像,然后再创建模拟器要花多长时间,就知道缓存是多么有用的时间优化手段。
六、社区推广
2022 年 SQLlin 刚开源之际,我在 2022 Kotlin 中文开发者大会上分享了 SQLlin 相关的内容:以 SQLlin 为例,分享如何构建自己的 KMP 库的经验。收效较好,SQLlin 在 Kotlin Multiplatform 中文社区内拥有了一定知名度。目前在 Github 上拥有 190 个 stars(2024.01.18),从 starts 数量上来看也许并不高,但Kotlin Multiplatform 开发者群体绝对数量目前仍然较低,与 Android、Java 等技术栈相比不在一个数量级,因此该成绩算是可以接受。
相较于国内的环境,英文社区对新技术的接受速度普遍更高,Kotlin Multiplatform 开发者的数量更大,因此将 SQLlin 的影响力扩大到英文社区是一个好的选择。
SQLlin 自诞生之初就拥有全套的英文文档,在这一整年的维护升级过程中,我发现国外开发者的 issue/PR 数量大概占一半,维护过程中我与过来自希腊、英国、巴西的开发者在issue 或 PR 中互动过。Stars 的来源也有大量国外开发者,包括美国、德国、韩国、俄罗斯等等。与国外开发者在 Github 合作、沟通是一种极为有趣的体验。
此外,一家美国初创的语言学习类 App 公司——Migaku 在生产环境使用 SQLlin,这是我发现的第一例在生产环境使用 SQLlin 的国外商业公司。他们的员工曾帮助提交PR(参考链接 4)协助修复了一个 Native 平台与 Android 平台行为不一致的问题,并请求我尽快发布新版,因为他们希望在 App 发布新版时可以使用问题修复后的新版SQLlin。
我也将 SQLlin 作为讲题内容申请成为哥本哈根 KotlinConf 2024 大会的 speaker,KotlinConf 是世界性质的行业大会,由 Kotlin 的开发商 JetBrains 举办。如果讲题被 JetBrains选中,这将是一个扩大 SQLlin 在世界范围内影响力的绝佳机会,同时也是向英文社区分享中国 Kotlin Multiplatform 开发经验、贡献知识的机会,还是一个能收获许多世界优秀开发者的反馈,提升个人技能、公司在相关领域技术实力的机会。
从 2022.11 ~ 2024.1,近一年的时间 Kotlin Multiplatform 技术迎来许多重要的变革。这其中包括 new MM 从实验性阶段转入稳定,也包括 Kotlin/Native 编译器支持的 targets 的更迭,其他的小更新及优化更是数不胜数。
事实上最近几个版本的 Kotlin 在新功能的迭代速度上已经放缓,其主要原因是官方最近将主要精力放在了 Kotlin 新编译器 K2 的优化上,2024 年 K2 正式版将会随 Kotlin 2.0 一起到来。目前 SQLlin 1.2.4 版本基于 Kotlin 1.9.22,1.9.22 应该会是 Kotlin 1.x 的最后一个发行版,而当 Kotlin 2.0 发布后,SQLlin 也会积极进行升级。随着 Kotlin 语言特性、标准库、生态环境的逐步提升,SQLlin 也会对内部实现进行重构和迭代,以求在性能和代码结构等方面带来更多的提升。
SQLlin 在未来还有众多的发展空间,例如更改表结构的 SQL 语句 DSL 化还没有实现,Join 子查询的 DSL 化也还没有实现,这些都已经规划到了未来的开发计划中。希望在未来 SQLlin 可以在携程机票及整个 Kotlin Multiplatform 技术社区中有更广泛的应用场景。
七、参考链接
开源项目 SQLiter:
https://github.com/touchlab/SQLiter
修复 SQliter Join 语句问题的 PR:
https://github.com/touchlab/SQLiter/pull/89
SQLlin 支持 JVM 相关的 issue:
https://github.com/ctripcorp/SQLlin/issues/15
Migaku 提交的修复 SQLlin bug 的 PR:
https://github.com/ctripcorp/SQLlin/pull/51