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