新聞中心
作者簡(jiǎn)介

創(chuàng)新互聯(lián)公司服務(wù)項(xiàng)目包括威信網(wǎng)站建設(shè)、威信網(wǎng)站制作、威信網(wǎng)頁(yè)制作以及威信網(wǎng)絡(luò)營(yíng)銷(xiāo)策劃等。多年來(lái),我們專(zhuān)注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢(shì)、行業(yè)經(jīng)驗(yàn)、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,威信網(wǎng)站推廣取得了明顯的社會(huì)效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶(hù)以成都為中心已經(jīng)輻射到威信省份的部分城市,未來(lái)相信會(huì)繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶(hù)的支持與信任!
禹昂,攜程移動(dòng)開(kāi)發(fā)專(zhuān)家,Google 開(kāi)發(fā)者專(zhuān)家(Android),上海 Kotlin User Group 組織者,圖書(shū)《Kotlin 編程實(shí)踐》譯者。
2022 年底,我們?cè)跀y程的 Github organization 下開(kāi)源了 SQLlin,SQLlin 是一款基于 Kotlin DSL 及 KSP 技術(shù)的,支持眾多平臺(tái)的 Kotllin Multipllatform SQLite 數(shù)據(jù)庫(kù)框架。感興趣且不了解 SQLlin 的讀者可以參考:《攜程機(jī)票跨端 Kotlin DSL 數(shù)據(jù)庫(kù)框架 SQLlin》一文。
SQLlin作為攜程機(jī)票移動(dòng)端團(tuán)隊(duì)最為完備的一款開(kāi)源項(xiàng)目,在接近 1 年的時(shí)間內(nèi)經(jīng)歷了不少升級(jí)與換血式的更新,也見(jiàn)證了這一年 Kotlin Multiplatform 技術(shù)的演進(jìn)及社區(qū)生態(tài)的變化。本文將帶領(lǐng)大家梳理這些更新,并探求這些更新背后所涉及到的 Kotlin Multiplatform 技術(shù)棧在這一年來(lái)的更迭與進(jìn)化。
一、重寫(xiě) native 驅(qū)動(dòng)層
我們先來(lái)回顧一下最初的 SQLlin 架構(gòu)圖:
最初,SQLlin 在 Kotlin/Native 平臺(tái)上基于開(kāi)源項(xiàng)目 SQLiter(見(jiàn)參考鏈接 1),目的是避免重復(fù)造輪子。雖然 SQLliter 是來(lái)自 Touchlab的優(yōu)秀開(kāi)源項(xiàng)目,但最近一年維護(hù)更新緩慢。在本文撰寫(xiě)時(shí),SQLiter 于 2023 年 11 月發(fā)布了 1.3.0 和 1.3.1 兩個(gè)版本(1.3.1升級(jí)到了 Kotlin 1.9.21,用于修復(fù) 1.9.20 的 Kotlin/Native 庫(kù)版本號(hào)相關(guān)的問(wèn)題)。但在這之前的版本,即 1.2.1 發(fā)布于 2022年 8 月,基于 Kotlin 1.6.20,一年以上沒(méi)有更新。對(duì)于 2023 年的項(xiàng)目來(lái)說(shuō),1.6.20 過(guò)于老舊。老舊的版本導(dǎo)致了如下一些問(wèn)題。
1.1 Targets 更新維護(hù)不及時(shí)
Kotlin 在 1.8.20 版本廢棄了一眾 32 位 Kotlin/Native targets(目標(biāo)平臺(tái)),包括:iosArm32、watchosX86、wasm32、mingwX86、linuxArm32Hfp、linuxMips32、linuxMipsel32。這些目標(biāo)平臺(tái)幾乎已經(jīng)完全被淘汰,市面上已經(jīng)極少有可以運(yùn)行這些targets 的設(shè)備,繼續(xù)支持已無(wú)意義。因此 Kotlin 決定將這些 targets 標(biāo)記為“deprecated”,并在 1.9.20 版本將它們完全移除。
這些即將被移除的 targets 中,iosArm32、watchosX86、mingwX86 受到 SQLiter 及 SQLlin 的支持。由于 SQLiter 不更新版本,所以這些 targets 將繼續(xù)存在于 SQLiter 當(dāng)中,雖然 sqllin-driver 可以在上層移除對(duì)這些平臺(tái)的支持,但長(zhǎng)久來(lái)說(shuō)由于編譯器版本的更迭,仍然不是最佳做法。
如果說(shuō)在 sqllin-driver 中移除對(duì)舊編譯目標(biāo)的支持可以暫時(shí)解決“廢棄舊 targets 不及時(shí)”的問(wèn)題,那么“對(duì)新 targets 的支持”則無(wú)計(jì)可施。
Kotlin 在 1.8.0 版本開(kāi)始支持 watchosDeviceArm64 新目標(biāo)平臺(tái),對(duì)應(yīng)于全新的 64 位 Apple Watch 設(shè)備。雖然可以預(yù)見(jiàn)使用 Kotlin Multiplatform 技術(shù)開(kāi)發(fā) Apple Watch 應(yīng)用的開(kāi)發(fā)者不會(huì)很多,但 SQLlin 原本支持所有的 watchOS 相關(guān) targets,不支持最新的 Arm64 架構(gòu)并不合理。由于 SQLiter 不支持 watchosDeviceArm64,因此 SQLlin 也無(wú)法支持。
1.2 Bug 無(wú)法及時(shí)修復(fù)
在 SQL 中我們會(huì)遇到一個(gè)常見(jiàn)的用法——join,在 join 查詢(xún)時(shí)遇到兩個(gè)表?yè)碛邢嗤值牧幸彩浅R?jiàn)現(xiàn)象。在 SQLiter的原始實(shí)現(xiàn)中,后查詢(xún)出來(lái)的同名列值會(huì)覆蓋掉先查詢(xún)出來(lái)的同名列值:
override val columnNames: Map by lazy {
val map = HashMap(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 月提交了一個(gè) PR 以修復(fù)此問(wèn)題(參考鏈接 2),但 SQLliter 的維護(hù)者沒(méi)有任何回復(fù),同樣是直到 2023 年 11 月才合并該 PR。
無(wú)法支持的新平臺(tái)導(dǎo)致有剛需的用戶(hù)無(wú)法繼續(xù)使用 SQLlin,而無(wú)法修復(fù)的問(wèn)題導(dǎo)致了特定場(chǎng)景必定出錯(cuò)的硬傷。一年沒(méi)有任何維護(hù)讓我對(duì) SQLiter 感到疑慮,此時(shí)自行實(shí)現(xiàn)已經(jīng)變成了必然選擇。
1.3 Native 驅(qū)動(dòng)層重寫(xiě)
重寫(xiě) Native 驅(qū)動(dòng)層并不困難,我們可以參考 SQLiter 的不少設(shè)計(jì)理念。
首先,SQLite 在不同的 Native 平臺(tái)上都提供相同的 C API,所以我們絕大部分代碼是平臺(tái)(這里特指 Kotlin/Native 的諸多目標(biāo)平臺(tái))無(wú)關(guān)的。根據(jù)官方 KMP 工程的架構(gòu)約定,這部分平臺(tái)無(wú)關(guān)的代碼可以全部放在 nativeMain source set 下。但由于我們構(gòu)建了一套面向?qū)ο箫L(fēng)格的 API,加上需要處理例如線(xiàn)程同步等問(wèn)題,因此還是會(huì)依賴(lài)一些系統(tǒng)平臺(tái) API。比如說(shuō)如果要在 nativeMain 中使用線(xiàn)程鎖,需要用 expect 關(guān)鍵字定義待實(shí)現(xiàn)的API,在各平臺(tái)相關(guān) source set 中使用 actual 關(guān)鍵字定義相關(guān)實(shí)現(xiàn)。比如說(shuō)在 Apple 平臺(tái)上我們使用 Apple Foundation 中的 Objective-C 類(lèi) NSRecursiveLock,而在 Linux 和Windows 平臺(tái)上則使用 Posix C 中的 pthread_mutex_xxx 系列 C API。
我們將 SQLite 的 C 庫(kù)頭文件放在 include 路徑下(與 nativeMain 平級(jí)),然后編寫(xiě) .def 文件并放在 nativeInterop 路徑下(同樣與 nativeMain 平級(jí)),然后在 build.gradle.kts 文件中配置頭文件的路徑以及 SQLite C 庫(kù)的 linkerOpts(編譯鏈接參數(shù)),即可在所有 native 相關(guān)的 sourceSet 中調(diào)用 SQLite C 函數(shù),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")
}
}
}這是一個(gè) native 目標(biāo)平臺(tái)可調(diào)用的擴(kuò)展函數(shù),使所有 native targets 都調(diào)用它即可。其中 linkerOpts 在 Linux 和 Windows 平臺(tái)上都指向常見(jiàn)的 SQLite 安裝路徑(使用常見(jiàn)的包管理器),但為了確保 native 單元測(cè)試可以順利在任何 Linux 或 Windows host 上運(yùn)行,SQLlin 的源碼目錄中實(shí)際上附帶了針對(duì) Linux 及 Windows 的 SQLite .a 庫(kù),因此當(dāng)鏈接過(guò)程無(wú)法在常見(jiàn)路徑下找到 SQLite .a文件時(shí),最終會(huì)鏈接到 SQLlin 源碼路徑下的版本。
但再次強(qiáng)調(diào),以上場(chǎng)景僅限單元測(cè)試,如果你是使用 SQLlin 的應(yīng)用開(kāi)發(fā)者,且你的應(yīng)用支持 Linux 和 Windows,需要確保用戶(hù)的電腦安裝了SQLite,或者在應(yīng)用程序工程中附帶 SQLite C 庫(kù),并自行添加 linkerOpts 鏈接到 SQLite .a 文件。至于 Apple 相關(guān)平臺(tái)(iOS、macOS、watchOS、tvOS),系統(tǒng)框架中已經(jīng)自帶了SQLite,因此不必?fù)?dān)心以上問(wèn)題,sqllin-driver 中添加的編譯鏈接參數(shù)可以正確鏈接到系統(tǒng)框架中自帶的版本。最后我們來(lái)看一下 nativeMain 下的源碼結(jié)構(gòu):
cinterop 包包含所有對(duì) SQLite C 函數(shù)直接互操作的代碼,通過(guò)單獨(dú)的包將其與其它代碼隔離;platform 包則存放所有待平臺(tái)實(shí)現(xiàn)的相關(guān)代碼,真正的實(shí)現(xiàn)則位于 appleMain、linuxMain、mingwMain 幾個(gè) source sets 中;其余代碼是 sqllin-driver-native 的核心實(shí)現(xiàn),都位于根目錄包下。
二、JVM Target 支持
起初,根據(jù)預(yù)測(cè),我認(rèn)為使用 Kotlin Multiplatform 技術(shù)開(kāi)發(fā) JVM 桌面應(yīng)用的人并不多。但由于 Compose Multiplatform 最初支持的平臺(tái)便是 Android 與 JVM,因此吸引了大量 Kotlin Multiplatform 開(kāi)發(fā)者將自己的多平臺(tái)應(yīng)用的支持范圍擴(kuò)展到 JVM。在部分用戶(hù)提交了一些 issue(參考鏈接 3)后,我決定著手進(jìn)行 JVM 平臺(tái)的支持工作。而支持 JVM 平臺(tái)也有助于調(diào)研將 SQLlin 支持的數(shù)據(jù)庫(kù)擴(kuò)展到 MySQL、H2、Oracle 等后端數(shù)據(jù)庫(kù)的可能性,因?yàn)樗鼈兌蓟?JDBC。
JVM 平臺(tái)的實(shí)現(xiàn)基于 SQLite 官方的 JVM driver:sqlite-jdbc,庫(kù)的使用者通過(guò) JDBC 連接到 sqlite-jdbc,而 sqlite-jdbc 底層則通過(guò) JNI 操作 SQLite C 庫(kù)。由于 sqlite-jdbc本身就是 Java 庫(kù),因此 API 的抽象程度比 native 平臺(tái)上直接調(diào)用 C API 高的多。所以 jvmMain 中的代碼實(shí)現(xiàn)比 nativeMain 要簡(jiǎn)單很多。
但也有幾個(gè)點(diǎn)值得一提:
首先,Windows平臺(tái)上的文件路徑分隔符是 ‘\’,而 Linux 和 macOS 上都是 ‘/’,因此在處理用戶(hù)傳入的路徑參數(shù)時(shí),即使是在 jvmMain 中也要判斷當(dāng)前運(yùn)行的操作系統(tǒng)是不是 Windows。
其次,由于sqlite-jdbc 中沒(méi)有對(duì) sqlite3_config C 函數(shù)的調(diào)用,因此目前 lookasideSlotSize 和 lookasideSlotCount 兩個(gè)參數(shù)在 JVM 平臺(tái)上無(wú)法生效,后續(xù)我計(jì)劃通過(guò)提交 PR 的方式參與sqlite-jdbc 的開(kāi)發(fā),使其支持 sqlite3_config,但目前還沒(méi)有具體的時(shí)間表。
當(dāng)然,支持 JVM 平臺(tái)的開(kāi)發(fā)過(guò)程還遇到過(guò)其他的細(xì)節(jié)問(wèn)題,例如表示查詢(xún)結(jié)果集的 java.sql.ResultSet 類(lèi)型起始下標(biāo)是 1 而不是 Android 平臺(tái) android.database.Cursor 和 Native 平臺(tái) C API 中的 0。不過(guò)這類(lèi)問(wèn)題都較為容易處理,在此不多做贅述。
在重寫(xiě)了 native 平臺(tái)的 driver 和支持了 JVM 平臺(tái)后,SQLlin 的架構(gòu)圖如下所示:
目前 SQLlin 支持的完整目標(biāo)平臺(tái)列表如下:
- 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 并發(fā)安全
sqllin-driver 作為低階 SQLite 框架,可以通過(guò) SQLite 本身的線(xiàn)程安全機(jī)制來(lái)實(shí)現(xiàn)一定程度上的線(xiàn)程安全,我寫(xiě)過(guò)一篇文章《關(guān)于 SQLite 多線(xiàn)程行為的結(jié)論》討論過(guò)相關(guān)知識(shí)。
簡(jiǎn)而言之,在多數(shù)情況下 SQLite 的默認(rèn)線(xiàn)程模式都是:Multi-thread,在單連接多線(xiàn)程的情況下是可以保證線(xiàn)程安全的。因此我們只需盡量避免多連接多線(xiàn)程的情形即可,將同一個(gè)連接在多個(gè)線(xiàn)程間共享是個(gè)好方法。
現(xiàn)在我們來(lái)回顧一下 sqllin-dsl 的基本用法,以便理解本節(jié)接下來(lái)的內(nèi)容:
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 = 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 中,一個(gè) Database 對(duì)象中只會(huì)建立一個(gè)數(shù)據(jù)庫(kù)鏈接。但上述示例中如果我們將對(duì)象 db(類(lèi)型為 Database)在多個(gè)線(xiàn)程(或運(yùn)行在不同線(xiàn)程上的協(xié)程)中共享,幾乎必然會(huì)出現(xiàn)問(wèn)題。
原因在于 Database 對(duì)象內(nèi)部使用一個(gè)雙向鏈表來(lái)進(jìn)行一組 SQL 語(yǔ)句的構(gòu)建,一個(gè) Database 對(duì)象持有一個(gè)雙向鏈表,每次子句的連接都會(huì)直接拼接到鏈表頭部的 SQL語(yǔ)句上,而當(dāng) SQL 語(yǔ)句組執(zhí)行完畢后鏈表會(huì)被清空。
如果在多個(gè)線(xiàn)程/協(xié)程中同事使用 db 對(duì)象,可以想象這可能會(huì)出現(xiàn) SQL 語(yǔ)句拼接混亂的問(wèn)題,例如線(xiàn)程 A 和 線(xiàn)程 B 都在構(gòu)建自己的SQL 語(yǔ)句,由于沒(méi)有同步機(jī)制,線(xiàn)程 B 中的子句可能被拼接到線(xiàn)程 A 中已經(jīng)創(chuàng)建出的 SQL 語(yǔ)句后面,造成 SQL 語(yǔ)法錯(cuò)誤。也有可能出現(xiàn)線(xiàn)程 A 還在構(gòu)建 SQL 語(yǔ)句,但線(xiàn)程 B 已經(jīng)進(jìn)入SQL 語(yǔ)句執(zhí)行階段,線(xiàn)程 B 很可能會(huì)將還未構(gòu)建完成的 SQL 語(yǔ)句傳給 SQLite,造成運(yùn)行錯(cuò)誤。
SQLlin 最初之所以沒(méi)有設(shè)計(jì)線(xiàn)程同步機(jī)制主要是基于 Kotlin 版本的考量。在 SQLlin 第一個(gè)版本發(fā)布的 Kotlin 1.7.20 時(shí)期,Kotlin/Native new Memory Management(新內(nèi)存管理器,后文簡(jiǎn)稱(chēng) new MM)還未進(jìn)入正式版,不少開(kāi)發(fā)者還在使用舊內(nèi)存管理器。在 Kotlin/Native 的舊內(nèi)存模型中,對(duì)象是不能直接跨線(xiàn)程訪問(wèn)的,必須要手動(dòng)進(jìn)行對(duì)象子圖分離和再綁定操作,對(duì)象才能將自己的所有權(quán)轉(zhuǎn)移到另一個(gè)線(xiàn)程,這種設(shè)計(jì)其實(shí)是強(qiáng)制開(kāi)發(fā)者在編譯期就保證對(duì)象在同一時(shí)刻只能被一個(gè)線(xiàn)程訪問(wèn)。
關(guān)于舊內(nèi)存模型在本人以往的文章中討論過(guò)很多次,并且在當(dāng)下 Kotlin 1.9.20 時(shí)代已經(jīng)被徹底淘汰,這里也不再過(guò)多討論。基于以上的時(shí)代背景,在不能確定用戶(hù)是否使用新內(nèi)存管理器的情況下,做線(xiàn)程同步的設(shè)計(jì)非常困難,因此最好的方式就是不處理,并且建議用戶(hù)不要在多線(xiàn)程間共享 Database 對(duì)象。但如今 2023 年末,在 Kotlin 1.9.2x 版本作為最新版本的背景下,new MM早已經(jīng)被絕大部分開(kāi)發(fā)者所使用,因此此時(shí)基于 new MM 的設(shè)計(jì)進(jìn)行線(xiàn)程同步機(jī)制的開(kāi)發(fā)非常合適。
在 sqllin-dsl 新版本的設(shè)計(jì)中,新增了掛起函數(shù) API suspendScope,用于在并發(fā)環(huán)境下取代 operator 函數(shù) invoke,并且管理 SQL 語(yǔ)句構(gòu)建的雙向鏈表被改成成員變量,只有在每次invoke 或 suspendScope 函數(shù)被調(diào)用時(shí)才創(chuàng)建,在 SQL 語(yǔ)句執(zhí)行完畢后會(huì)被就會(huì)被拋棄。由于函數(shù)調(diào)用棧是線(xiàn)程私有的,因此這樣的設(shè)計(jì)可以在不同的線(xiàn)程同時(shí)構(gòu)建 SQL語(yǔ)句時(shí)隔離運(yùn)行,既提高效率又保證了線(xiàn)程安全。
在 SQL 語(yǔ)句運(yùn)行階段,由于每次 SQL 語(yǔ)句構(gòu)建完畢后執(zhí)行的都是一組 SQL,為了避免不同線(xiàn)程同時(shí)執(zhí)行 SQL語(yǔ)句時(shí)的順序的不確定性,例如線(xiàn)程 A 需要執(zhí)行 SQL 語(yǔ)句 a、b、c,線(xiàn)程 B 需要執(zhí)行 SQL 語(yǔ)句 d、e、f,不加任何同步機(jī)制同時(shí)執(zhí)行可能會(huì)導(dǎo)致 a、b、c、d、e、f的執(zhí)行順序不確定,從而導(dǎo)致不可預(yù)知的問(wèn)題,因此 SQL 語(yǔ)句執(zhí)行階段必須加入?yún)f(xié)程鎖 Mutex 來(lái)保證并發(fā)安全,suspendScope 的實(shí)現(xiàn)如下:
private val executiveMutex by lazy { Mutex() }
public suspend infix fun suspendedScope(block: suspend DatabaseScope.() -> T): T {
val databaseScope = DatabaseScope(databaseConnection, enableSimpleSQLLog)
val result = databaseScope.block()
executiveMutex.withLock {
databaseScope.executeAllStatements()
}
return result
} 由于使用了協(xié)程鎖 Mutex,因此自 1.2.2 版本起, sqllin-dsl 依賴(lài) Kotlin 官方協(xié)程框架 kotlinx.coroutines。
四、Android 低版本向下兼容
Android 系統(tǒng)曾在 API 28(Android 9)版本對(duì) framework 中的 SQLite Java APIs 進(jìn)行了一次升級(jí),這次升級(jí)提供了許多新 API 可以讓開(kāi)發(fā)者對(duì) SQLite進(jìn)行具體的參數(shù)配置,這些參數(shù)包括:日志模式、同步模式、連接超時(shí)時(shí)間、lookaside memory,這在之前的版本都是不可以的。由于 SQLlin 最低支持的Android 版本是 API 23(Android 6),因此在 Android 9 以下的設(shè)備上,以上提到的參數(shù)都無(wú)法生效。
但最初的認(rèn)知并不準(zhǔn)確,因?yàn)槿罩灸J?、同步模式兩個(gè)參數(shù)都使用 PRAGMA 語(yǔ)句配置,因此只需要在 sqllin_driver 內(nèi)自行構(gòu)建 PRAGMA 語(yǔ)句并執(zhí)行,即可在舊Android 系統(tǒng)上也能進(jìn)行日志模式與同步模式的設(shè)置。因此,自 1.2.0 版本起,SQLlin 在舊 Android 設(shè)備上也支持設(shè)置日志模式與同步模式。但基于 SQLite C API才能配置的連接超時(shí)時(shí)間和 lookaside memory 仍然無(wú)法在舊設(shè)備上生效。
五、CI/CD 優(yōu)化
在 SQLlin 開(kāi)源之初沒(méi)有進(jìn)行 CI/CD 環(huán)境的搭建。CI/CD 對(duì)于驗(yàn)證 push、PR 的準(zhǔn)確性,保證版本發(fā)布的 bug 率等方面具有重要意義。同時(shí)也是向 MavenCentral發(fā)布新版本的最佳途徑。起初的發(fā)布都在本人的工作電腦上進(jìn)行(Macbook Pro),由于 Mac 電腦的 Kotlin/Native 編譯器不支持編譯 Windows 平臺(tái)的產(chǎn)物,導(dǎo)致1.0 版本的 SQLlin 不支持 MinGW 目標(biāo)平臺(tái)。
在 2023 年 1 月,SQLlin 第一個(gè)版本的 CI/CD pipeline 上線(xiàn)。此后經(jīng)過(guò)持續(xù)的優(yōu)化,如今已經(jīng)進(jìn)入較為完備的體系和狀態(tài)。在搭建、優(yōu)化的過(guò)程中,我認(rèn)為以下幾點(diǎn)內(nèi)容頗為重要:
5.1 單元測(cè)試/儀器測(cè)試原則
單元測(cè)試對(duì)任何項(xiàng)目都具有重要意義,可以在一定程度上驗(yàn)證代碼的修改不會(huì)導(dǎo)致原有預(yù)期行為的改變,因此單元測(cè)試是 CI/CD 流程中的關(guān)鍵步驟。我們可以先回看“二. JVMTarget 支持”一節(jié)中的 SQLlin 最終架構(gòu)設(shè)計(jì)圖,SQLlin 在任何一個(gè)平臺(tái)上運(yùn)行在底層都會(huì)涉及平臺(tái)相關(guān)代碼,因此單元測(cè)試必須覆蓋所有平臺(tái)相關(guān)代碼。
例如,如果我們只在 macOS機(jī)器上執(zhí)行單元測(cè)試,可以保證平臺(tái)無(wú)關(guān)代碼(sqllin-dsl、sqllin-processor、sqllin-driver(commonMain))以及 macOS 平臺(tái)相關(guān)代碼(sqllin-driver(nativeMain、appleMain))的正確性,但是無(wú)法驗(yàn)證其他平臺(tái)相關(guān)的代碼,例如 sqllin-driver 中的 androidMain、jvmMain、linuxMain、mingwMain。
所以我們有必要在 Linux 和 Mac 機(jī)器上同時(shí)執(zhí)行Kotlin/Native 單元測(cè)試,但沒(méi)有必要分別在 iOS 和 macOS 上執(zhí)行 Kotlin/Native 單元測(cè)試,因?yàn)樗?Apple 平臺(tái)的相關(guān)代碼都在 appleMain source set 下,iOS 和 macOS上運(yùn)行的 SQLlin 代碼沒(méi)有任何區(qū)別,保證相同的代碼在 iOS 和 macOS 運(yùn)行得到相同的結(jié)果是 Kotlin 編譯器需要保證的事情,而不是庫(kù)開(kāi)發(fā)者。JVM 單元測(cè)試比較特殊,需要在三臺(tái)機(jī)器上都運(yùn)行,因?yàn)槲募窂皆谌N不同的操作系統(tǒng)上的表示不同,這部分代碼的區(qū)別可能就幾個(gè)字符,但既然不是 100% 相同,那么就還是需要分別測(cè)試。
根據(jù)以上原則,我們需要執(zhí)行的單元測(cè)試如下:
- 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 支持眾多平臺(tái),這里的平臺(tái)是廣義的,其中既包括操作系統(tǒng)原生產(chǎn)物,又包括一些非原生開(kāi)發(fā)環(huán)境。比如 WASM、JavaScript、JVM、Android就屬于非原生開(kāi)發(fā)環(huán)境。WASM、JavaScript、JVM 這些技術(shù)的出現(xiàn)本身就是為了跨平臺(tái)(這里是狹義的“平臺(tái)”,特指各操作系統(tǒng)),而 Android 的 ART則是一個(gè)“非標(biāo)準(zhǔn)”的 JVM,這些編譯產(chǎn)物的運(yùn)行能力由其相對(duì)應(yīng)的平臺(tái)本身提供,不依賴(lài)特定 CPU 架構(gòu)或操作系統(tǒng) API,因此在任何機(jī)器上都能編譯構(gòu)建。
但Kotlin/Native 編譯出的操作系統(tǒng)原生產(chǎn)物則不同,首先,所有的 Apple 平臺(tái)(iOS、macOS、watchOS、tvOS)的編譯構(gòu)建都依賴(lài) Xcode 命令行工具,而Apple 只提供 macOS 版本的 Xcode,因此,一個(gè) Kotlin Multiplatform 應(yīng)用或庫(kù)如果要支持 Apple 平臺(tái),必須使用 Mac 電腦開(kāi)發(fā)和構(gòu)建;其次,由于Kotlin/Native 在 Windows 平臺(tái)上依賴(lài) MinGW,至少 Kotlin 1.7.20 之前的版本如果要構(gòu)建 Windows 產(chǎn)物就必須使用 Windows 電腦,但在 1.7.20之后的某個(gè)版本開(kāi)始,官方悄無(wú)聲息的支持了 Mac 電腦編譯 mingwx64 產(chǎn)物;而 Linux 系統(tǒng)的產(chǎn)物 Mac 電腦一直可以構(gòu)建。SQLlin 支持的全部平臺(tái)已經(jīng)在“二. JVMTarget 支持”一節(jié)中詳細(xì)列出。因此看似只需一臺(tái) Mac 電腦即可完成全部的 CI/CD 任務(wù)。
但我們必須確保 CI/CD 中的單元測(cè)試可以符合 5.1 小節(jié)中的原則。macOS 雖然可以編譯構(gòu)建 Linux 和 Windows 平臺(tái)產(chǎn)物,但是無(wú)法執(zhí)行這些平臺(tái)的單元測(cè)試。所以我們至少需要Mac、Windows、Linux 三臺(tái)機(jī)器來(lái)完成整個(gè) CI/CD 過(guò)程。三臺(tái)機(jī)器需要構(gòu)建的產(chǎn)物如下:
- 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
僅從編譯構(gòu)建來(lái)看,Mac 的任務(wù)最重,Windows 的任務(wù)最輕。但沒(méi)有辦法,所有的 Apple 產(chǎn)物都只能在 Mac 上構(gòu)建。為了盡量縮短各平臺(tái)的 CI/CD pipeline運(yùn)行過(guò)程的時(shí)間差以節(jié)省總時(shí)間,我們盡量合理分配一下單元測(cè)試任務(wù)。各平臺(tái)執(zhí)行的單元測(cè)試任務(wù)如下所示:
- 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)
實(shí)際上 native 和 JVM 單元測(cè)試的流程都非??欤?Android 儀器測(cè)試的流程非常耗時(shí)(耗時(shí)甚至可能接近整個(gè) CI/CD 流程耗時(shí)的一半),因?yàn)闇?zhǔn)備(沒(méi)有緩存的話(huà)要?jiǎng)?chuàng)建)Android 模擬器非常耗時(shí),連接Android 模擬器的測(cè)試過(guò)程也非常耗時(shí),因此將兩個(gè)不同版本的 Android 儀器測(cè)試分配到不同的機(jī)器上是非常有必要的,這也是為什么 Linux 機(jī)器上也要構(gòu)建一次 Android 產(chǎn)物的原因。
5.3 緩存
由于每次執(zhí)行 CI/CD 時(shí),Github Actions 總是分配空閑的機(jī)器給你的項(xiàng)目運(yùn)行 pipeline,因此每次 pipeline 執(zhí)行完畢后,流程中下載的構(gòu)建工具、依賴(lài)庫(kù)、編譯產(chǎn)物,以及創(chuàng)建的 Android模擬器都會(huì)被清除。在沒(méi)有任何緩存的情況下每次重新運(yùn)行 pipeline 會(huì)浪費(fèi)大量時(shí)間。因此配置緩存策略是節(jié)省 CI/CD 運(yùn)行時(shí)間的訣竅之一。
我們主要需要緩存的東西有三個(gè):下載的構(gòu)建工具、創(chuàng)建好的 Android 模擬器、Gradle 構(gòu)建產(chǎn)物。一些和緩存有關(guān)的 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."實(shí)際效果也非常好,使用緩存之前整個(gè) CI/CD 流程執(zhí)行結(jié)束可能需要 26 分鐘以上,使用緩存后降低至 10 分鐘出頭。其實(shí)可以想象每次我們?cè)陔娔X上下載 Android 模擬器所需的鏡像,然后再創(chuàng)建模擬器要花多長(zhǎng)時(shí)間,就知道緩存是多么有用的時(shí)間優(yōu)化手段。
六、社區(qū)推廣
2022 年 SQLlin 剛開(kāi)源之際,我在 2022 Kotlin 中文開(kāi)發(fā)者大會(huì)上分享了 SQLlin 相關(guān)的內(nèi)容:以 SQLlin 為例,分享如何構(gòu)建自己的 KMP 庫(kù)的經(jīng)驗(yàn)。收效較好,SQLlin 在 Kotlin Multiplatform 中文社區(qū)內(nèi)擁有了一定知名度。目前在 Github 上擁有 190 個(gè) stars(2024.01.18),從 starts 數(shù)量上來(lái)看也許并不高,但Kotlin Multiplatform 開(kāi)發(fā)者群體絕對(duì)數(shù)量目前仍然較低,與 Android、Java 等技術(shù)棧相比不在一個(gè)數(shù)量級(jí),因此該成績(jī)算是可以接受。
相較于國(guó)內(nèi)的環(huán)境,英文社區(qū)對(duì)新技術(shù)的接受速度普遍更高,Kotlin Multiplatform 開(kāi)發(fā)者的數(shù)量更大,因此將 SQLlin 的影響力擴(kuò)大到英文社區(qū)是一個(gè)好的選擇。
SQLlin 自誕生之初就擁有全套的英文文檔,在這一整年的維護(hù)升級(jí)過(guò)程中,我發(fā)現(xiàn)國(guó)外開(kāi)發(fā)者的 issue/PR 數(shù)量大概占一半,維護(hù)過(guò)程中我與過(guò)來(lái)自希臘、英國(guó)、巴西的開(kāi)發(fā)者在issue 或 PR 中互動(dòng)過(guò)。Stars 的來(lái)源也有大量國(guó)外開(kāi)發(fā)者,包括美國(guó)、德國(guó)、韓國(guó)、俄羅斯等等。與國(guó)外開(kāi)發(fā)者在 Github 合作、溝通是一種極為有趣的體驗(yàn)。
此外,一家美國(guó)初創(chuàng)的語(yǔ)言學(xué)習(xí)類(lèi) App 公司——Migaku 在生產(chǎn)環(huán)境使用 SQLlin,這是我發(fā)現(xiàn)的第一例在生產(chǎn)環(huán)境使用 SQLlin 的國(guó)外商業(yè)公司。他們的員工曾幫助提交PR(參考鏈接 4)協(xié)助修復(fù)了一個(gè) Native 平臺(tái)與 Android 平臺(tái)行為不一致的問(wèn)題,并請(qǐng)求我盡快發(fā)布新版,因?yàn)樗麄兿M?App 發(fā)布新版時(shí)可以使用問(wèn)題修復(fù)后的新版SQLlin。
我也將 SQLlin 作為講題內(nèi)容申請(qǐng)成為哥本哈根 KotlinConf 2024 大會(huì)的 speaker,KotlinConf 是世界性質(zhì)的行業(yè)大會(huì),由 Kotlin 的開(kāi)發(fā)商 JetBrains 舉辦。如果講題被 JetBrains選中,這將是一個(gè)擴(kuò)大 SQLlin 在世界范圍內(nèi)影響力的絕佳機(jī)會(huì),同時(shí)也是向英文社區(qū)分享中國(guó) Kotlin Multiplatform 開(kāi)發(fā)經(jīng)驗(yàn)、貢獻(xiàn)知識(shí)的機(jī)會(huì),還是一個(gè)能收獲許多世界優(yōu)秀開(kāi)發(fā)者的反饋,提升個(gè)人技能、公司在相關(guān)領(lǐng)域技術(shù)實(shí)力的機(jī)會(huì)。
從 2022.11 ~ 2024.1,近一年的時(shí)間 Kotlin Multiplatform 技術(shù)迎來(lái)許多重要的變革。這其中包括 new MM 從實(shí)驗(yàn)性階段轉(zhuǎn)入穩(wěn)定,也包括 Kotlin/Native 編譯器支持的 targets 的更迭,其他的小更新及優(yōu)化更是數(shù)不勝數(shù)。
事實(shí)上最近幾個(gè)版本的 Kotlin 在新功能的迭代速度上已經(jīng)放緩,其主要原因是官方最近將主要精力放在了 Kotlin 新編譯器 K2 的優(yōu)化上,2024 年 K2 正式版將會(huì)隨 Kotlin 2.0 一起到來(lái)。目前 SQLlin 1.2.4 版本基于 Kotlin 1.9.22,1.9.22 應(yīng)該會(huì)是 Kotlin 1.x 的最后一個(gè)發(fā)行版,而當(dāng) Kotlin 2.0 發(fā)布后,SQLlin 也會(huì)積極進(jìn)行升級(jí)。隨著 Kotlin 語(yǔ)言特性、標(biāo)準(zhǔn)庫(kù)、生態(tài)環(huán)境的逐步提升,SQLlin 也會(huì)對(duì)內(nèi)部實(shí)現(xiàn)進(jìn)行重構(gòu)和迭代,以求在性能和代碼結(jié)構(gòu)等方面帶來(lái)更多的提升。
SQLlin 在未來(lái)還有眾多的發(fā)展空間,例如更改表結(jié)構(gòu)的 SQL 語(yǔ)句 DSL 化還沒(méi)有實(shí)現(xiàn),Join 子查詢(xún)的 DSL 化也還沒(méi)有實(shí)現(xiàn),這些都已經(jīng)規(guī)劃到了未來(lái)的開(kāi)發(fā)計(jì)劃中。希望在未來(lái) SQLlin 可以在攜程機(jī)票及整個(gè) Kotlin Multiplatform 技術(shù)社區(qū)中有更廣泛的應(yīng)用場(chǎng)景。
七、參考鏈接
開(kāi)源項(xiàng)目 SQLiter:
https://github.com/touchlab/SQLiter
修復(fù) SQliter Join 語(yǔ)句問(wèn)題的 PR:
https://github.com/touchlab/SQLiter/pull/89
SQLlin 支持 JVM 相關(guān)的 issue:
https://github.com/ctripcorp/SQLlin/issues/15
Migaku 提交的修復(fù) SQLlin bug 的 PR:
https://github.com/ctripcorp/SQLlin/pull/51
本文名稱(chēng):從 SQLlin 的更新看 Kotlin Multiplatform 技術(shù)更迭
本文地址:http://m.fisionsoft.com.cn/article/cohihpd.html
其他資訊
- 如何使用DB2遠(yuǎn)程導(dǎo)出數(shù)據(jù)庫(kù)表?(db2遠(yuǎn)程導(dǎo)出數(shù)據(jù)庫(kù)表)
- 我有一個(gè)萬(wàn)網(wǎng)的域名想轉(zhuǎn)到新網(wǎng)互聯(lián),如何操作?(新網(wǎng)互聯(lián)域名如何過(guò)戶(hù)到新網(wǎng)站)
- 大體說(shuō)明VisualStudio2005軟件的技巧正規(guī)手段
- 服務(wù)器租賃平臺(tái)有哪些?(想出租服務(wù)器怎么辦理)
- Redis自動(dòng)暫停優(yōu)雅處理系統(tǒng)負(fù)載(redis自動(dòng)暫停)


咨詢(xún)
建站咨詢(xún)
