找回密码
 立即注册
查看: 301|回复: 0

携程机票 App KMM 跨端 KV 存储库 MMKV-Kotlin

[复制链接]
发表于 2022-6-23 15:39 | 显示全部楼层 |阅读模式
作者:Kotlin上海用户组
转载地址:https://juejin.cn/post/7109788544381485086
一. 背景


携程机票移动端研发团队自 2021 年始就一直在移动端实践 Kotlin Multiplatform 技术(请见参考链接 1)。由于目前 Kotlin Multiplatform 生态尚处于起步阶段,大部分 Kotlin 开源库都是 JVM only 的,因此在我们团队的日常开发过程中迫切需要一些能够支持 KMM(Kotlin Multiplatform Mobile)的基础库或框架。

在原生移动端开发中,Android SDK 提供了 SharedPreferences,iOS 提供了 NSUserDefaults 用于 KV 存储功能,但这二者在性能要求较高的情况下不能满足需求。后来虽然 Google 推出了 Jetpack Datastore 用于替换 SharedPreferences,但它仅仅支持 Android 平台。携程的基础框架团队经过了一系列评估后决定使用腾讯的开源库 MMKV (参考链接 2)用于满足携程 App 的 KV 存储需求。相较于 SharedPreferences 与 NSUserDefaults,MMKV 拥有更强大的性能;相较于 Jetpack Datastore,MMKV 同时支持多个平台,双端业务逻辑一致性会更好;此外,MMKV 的优势还包括:支持多进程访问、进程被突然杀死时存储依然可以生效等。因此,携程机票移动端研发团队决定基于 MMKV 二次开发,使 MMKV 支持 Kotlin Multiplatform 技术栈。

MMKV-Kotlin 因此应运而生,它拥有极为便捷的集成方式,与 MMKV 高度相似的 API 等诸多特点。对于有 MMKV 使用经验的原移动端开发人员来说,学习迁移成本很低。 在经过了大半年的线上实验证明了其稳定性与功能的完整性后,携程机票研发团队决定将其开源,为 Kotlin Multiplatform 开源生态添砖加瓦。MMKV-Kotlin Github 地址详见参考链接 3。
二. 简单使用


我们先来简单介绍一下 MMKV-Kotlin 的用法,便于读者对其有个较为直观的认识,也便于后文讨论其内部设计。
2.1 安装与导入


对于 KMM 开发者,在 common source set 中导入 MMKV-Kotlin,在 Gradle 脚本(kts)中添加:
dependencies {         implementation("com.ctrip.flight.mmkv:mmkv-kotlin:1.0.0")}
如果您是使用 Kotlin 编写纯 Android 程序的用户,则导入方式为在 Gradle 脚本(kts)中添加:
dependencies {         implementation("com.ctrip.flight.mmkv:mmkv-kotlin-android:1.0.0")}
对于纯 Android 开发者来说,虽然没有跨平台的需求,但 MMKV-Kotlin 的 API 有针对 Kotlin 语法作出的优化。
注意,截至文章发布前,MMKV-Kotlin 的最新版本是 1.2.0,基于 Kotlin 1.7.0,MMKV 1.2.13。
2.2 初始化


MMKV 在使用前需要进行初始化,由于 MMKV-Android 强依赖于 Context 类型,因此 MMKV-Kotlin 的初始化 API 在两端有所区别,需要在 Android 与 iOS 的主工程或 KMM 的平台相关 source set 中分别初始化:

Android:
import com.ctrip.flight.mmkv.initialize// In Android source setfun initializeMMKV(context: Context) {    val rootDir = initialize(context)    Log.d("MMKV Path", rootDir)}
iOS:
import com.ctrip.flight.mmkv.initialize// In iOS source setfun initializeMMKV(rootDir: String) {    initialize(rootDir)    Log.d("MMKV Path", rootDir)}2.3 简单的读写操作

import com.ctrip.flight.mmkv.defaultMMKVfun demo() {    val kv = defaultMMKV()    kv.set("Boolean", true)    kv.set("Int", Int.MIN_VALUE)    kv.set("String", "Hello from mmkv")    println("Boolean: ${kv.takeBoolean("Boolean")}")    println("Int: ${kv.takeInt("Int")}")    println("String: ${kv.takeString("String")}")}
使用方式与 MMKV 的 Java 及 Objective-C API 高度相似。
三. 架构设计


MMKV core 采用 C++ 编写,其绝大部分功能都在 core 实现。例如 mmap 提供的内存-文件映射、数据根据 protobuf 协议序列化与反序列化、多进程实现等等。core 直接对外暴露 C++ API,在 Win32、POSIX 等系统上可由开发者直接使用。在 core 的外层 MMKV 提供了多种语言的包装,用于支持多种技术栈。例如:Java(Android)、Objective-C(iOS/macOS)、Dart(Flutter)、 JavaScript(React-Native,非腾讯开发与维护)。

MMKV-Kotlin 在底层需要依赖并调用 MMKV,对上希望暴露与 MMKV 类似的 API 并做一些符合语言特性的封装。

MMKV-Kotlin 需要在两个平台相关的 source set 分别集成 MMKV。在 Android source set 中,如果直接集成 MMKV core 需要手动编写 JNI 来做 JVM 层与 C++ 的交互,投入产出比太小, 因此我们选择直接在 Gradle 脚本中通过 Maven 依赖 MMKV-Android,在 Android source set 中直接调用其 Java API。而在 iOS source set 中,由于 Kotlin 目前只与 C 和 Objective-C 有较为完整的互操作能力,因此直接依赖提供 C++ API 的 MMKV core 也并不合适,我们选择在 Gradle 脚本中通过 CocoaPods 依赖 MMKV-iOS,在 iOS source set 中通过其 Objective-C API 完成对 MMKV 的调用。

MMKV-Kotlin 的总体设计见下图:

四. 实现简介


在《携程机票 App KMM 跨端生产实践》(参考链接 1)一文的 2.2 小节中我们曾以 MMKV 作为 demo 来介绍 KMM 的 expect-actual 技术。但本次开源的版本为了代码的健壮性与实用性, 调整了具体的实现方式,本节将会进行详细的探讨。
4.1 初始化函数


2.2 小节演示了 MMKV-Kotlin 的初始化,因此其初始化函数是在 Android、iOS 两个 source set 中分别定义与实现的。

先看看 Android:
import android.content.Contextimport com.tencent.mmkv.MMKVfun initialize(context: Context): String = MMKV.initialize(context)fun initialize(context: Context, rootDir: String): String = MMKV.initialize(context, rootDir)fun initialize(context: Context, loader: MMKV.LibLoader): String = MMKV.initialize(context, loader)fun initialize(context: Context, logLevel: MMKVLogLevel): String = MMKV.initialize(context, logLevel.rawValue)fun initialize(context: Context, rootDir: String, loader: MMKV.LibLoader): String = MMKV.initialize(context, rootDir, loader)fun initialize(context: Context, rootDir: String, logLevel: MMKVLogLevel): String = MMKV.initialize(context, rootDir, logLevel.rawValue)fun initialize(context: Context, loader: MMKV.LibLoader, logLevel: MMKVLogLevel): String = MMKV.initialize(context, loader, logLevel.rawValue)fun initialize(context: Context, rootDir: String, loader: MMKV.LibLoader, logLevel: MMKVLogLevel): String = MMKV.initialize(context, rootDir, loader, logLevel.rawValue)
初始化函数的实现仅仅是调用 MMKV Java API 中的 initialize 函数。Android 平台的初始化强依赖 Context 类型,还提供了 LibLoader 类型作为参数,用于在初始化时加载 so 库。我们希望尽可能满足 Android 平台的各种需求,因此将 MMKV-Android 中的初始化 API 全部暴露出来。

再看看 iOS:
import cocoapods.MMKV.MMKVfun initialize() = MMKV.initialize()fun initialize(rootDir: String): String = MMKV.initializeMMKV(rootDir)fun initialize(rootDir: String, logLevel: MMKVLogLevel): String = MMKV.initializeMMKV(rootDir, logLevel.rawValue)fun initialize(rootDir: String, groupDir: String, logLevel: MMKVLogLevel): String = MMKV.initializeMMKV(rootDir, groupDir, logLevel.rawValue)
相比之下 iOS 平台少了 Context 类型与 LibLoader 类型,因此初始化函数的重载要少很多。
4.2 MMKV 类型


在 MMKV 的 Java 与 Objective-C 版本中,MMKV 类型是具体 CRUD 功能的实现类。在 Java 版本中,写函数为一系列 encode 重载函数或统一命名为 putXXX,其中 putXXX 内部调用了 encode 函数,二者只是返回类型不同,读函数为统一命名为 decodeXXX 或 getXXX 的函数,二者行为一致 。而 Objective-C 版本中,写函数统一命名为 setXXX 函数,读函数统一命名为 getXXX 函数。虽然平台不同,但是具有相同功能的函数的参数数量、类型,以及返回类型都高度统一。因此这给我们定义 common source set 中的 MMKV 类型带来了便利。

我们需要在 common 层声明 MMKV 类型(为避免同名带来的混淆,我们将 common 层的 MMKV 类型命名为 MMKV_KMP),并且具体实现在各平台的 source set 中,MMKV 类型的实例需要持有 Java 或 Objective-C 的 MMKV 类型的实例,并将 CURD 操作委托给它们。我们的实现方式有两种可选:
    MMKV_KMP 声明为 class,通过 expect-actual 机制在平台相关层编写其实现。MMKV_KMP 声明为 interface,在平台相关层编写其实现类。

最终我们选择了方案二,原因在于:在平台相关的 source set 中编写的具体实现 class 需要实例化时需要同时构建 Java/Objective-C 的 MMKV 实例,且最好的方式是在其构造函数作为参数传入。 而 Java 与 Objective-C 的 MMKV 是两个完全没有任何关系的独立类型,因此我们在 common source set 中统一 MMKV_KMP 的构造函数非常不便。其次,在 MMKV 原本的设计中,MMKV 的实例本身也不是通过构造函数创建,而是通过一系列工厂方法创建,因此我们没有必要在 common 层定义其构造函数。

确定基本设计后,我们看看 MMKV_KMP 的定义:
interface MMKV_KMP {    operator fun set(key: String, value: String): Boolean    operator fun set(key: String, value: Boolean): Boolean    fun takeString(key: String, default: String = ""): String    fun takeBoolean(key: String, default: Boolean = false): Boolean    fun close()    // More other functions and properties}
双平台的实现如下,Android:
import com.tencent.mmkv.MMKVclass MMKVImpl internal constructor(internal val platformMMKV: MMKV) : MMKV_KMP {    override operator fun set(key: String, value: String): Boolean = platformMMKV.encode(key, value)    override operator fun set(key: String, value: Boolean): Boolean = platformMMKV.encode(key, value)    override fun takeString(key: String, default: String): String = platformMMKV.decodeString(key, default) ?: default    override fun takeBoolean(key: String, default: Boolean): Boolean = platformMMKV.decodeBool(key ,default)    override fun close() = platformMMKV.close()    // More other functions and properties
iOS:
import cocoapods.MMKV.MMKVimport platform.Foundation.NSSet@Suppress("UNCHECKED_CAST")class MMKVImpl internal constructor(internal val platformMMKV: MMKV) : MMKV_KMP {    override operator fun set(key: String, value: Int): Boolean = platformMMKV.setInt32(value, key)    override operator fun set(key: String, value: Boolean): Boolean = platformMMKV.setBool(value, key)    override fun takeString(key: String, default: String): String = platformMMKV.getStringForKey(key, default) ?: default    override fun takeBoolean(key: String, default: Boolean): Boolean = platformMMKV.getBoolForKey(key, default)    override fun close() = platformMMKV.close()    // More other functions and properties}
最后是创建 MMKV_KMP 类型的工厂函数,我们只需通过 expect-actual 机制实现即可,这些工厂函数的返回类型都指定为 MMKV_KMP,在平台 source set 中调用 Java 与 Objective-C 的对应工厂函数,得到 MMKV 实例后通过构造函数构建出 MMKVImpl 实例并返回即可。具体代码在此省略,可在 Github 中查看。
4.3 平台专属 API


在 Kotlin/Native 中,Kotlin 基本类型以及 String 还有部分集合类型都可以映射到 Objective-C 中的对应类型。例如 Kotlin 的 String 可以与 Objective-C 的 NSString 互相映射,在编写代码时被认为是同一种类型。因此 common source set 中支持 CURD 的数据类型就是 MMKV-Android 与 MMKV-iOS 支持 CURD 类型的交集,包括:
    Boolean、Int、Long、Float、Double、String、UInt、ULong、ByteArray、Set<String>

其中要注意的点是,Kotlin 的 ByteArray 并不能与 Objective-C 的 NSData 直接映射,但二者可以通过手写代码转换,因此在 iOS 中实现读写 ByteArray 也是基于这样的手动转换实现, 最终读写的还是 NSData。而 Set<String> 类型是 MMKV-Android 原本就支持的,但在 iOS source set 中则是通过读写 NSCoding 来实现的,Set<String> 可直接映射为 NSSet,而 NSSet 又是 NSCoding 协议的实现者。

除此之外,MMKV-Android 与 MMKV-iOS 还支持一些平台特有的类型,例如 Android 额外支持 Parcelable 接口的实现者,而 iOS 额外支持 NSCoding 协议的实现者及 NSDate ,这些额外支持的类型都在平台 source set 中通过扩展函数的方式提供,以便尽量完整保留 MMKV 原有的功能,并让开发者可以在平台 source set 中使用它们。
五. 单元测试


单元测试是开源项目必不可少的组成部分,鉴于 MMKV-Kotlin 的 API 与 MMKV 本身大体相同,因此单元测试的设计也参考了 MMKV 的单元测试。
5.1 API 功能测试


Kotlin 提供了一套 kotlin-test 单元测试框架,可以在 common 与 iOS source set 中使用。而在 Android source set 我们仍使用 JUnit。通常情况下我们只需要在 common source set 编写一套单元测试代码,而平台相关 source set 中甚至无需添加任何代码即可完成单元测试的构建。框架在运行后会针对已添加的平台分别运行测试。但在 MMKV-Kotlin 中 initialize 函数是分不同平台实现的,因此我们采取将 API 测试的核心代码放在 common,在 Android/iOS source set 初始化 MMKV 并构建测试。

Common 层的测试代码就是针对 MMKV-Kotlin API 的测试,参考了 MMKV 的设计,简单举例如下:
class MMKVKotlinTest {    companion object {        const val KEY_NOT_EXIST = "Key_Not_Exist"    }    lateinit var mmkv: MMKV_KMP        private set    fun setUp() {        mmkv = mmkvWithID("unitTest", cryptKey = "UnitTestCryptKey")    }    fun testDown() {        mmkv.clearAll()    }    fun testBoolean() {        val result = mmkv.set("Boolean", true)        assertEquals(result, true)        val value0 = mmkv.takeBoolean("Boolean")        assertEquals(value0, true)        val value1 = mmkv.takeBoolean(KEY_NOT_EXIST)        assertEquals(value1, false)        val value2 = mmkv.takeBoolean(KEY_NOT_EXIST, true)        assertEquals(value2, true)    }    // Other type test......}
setUp、testDown 分别负责 MMKV_KMP 的对象实例化及测试结束后的清理工作。针对每种具体数据类型的测试都独立在 testXXX 函数内,针对正常写读、读空值以及读空值时默认值是否生效三种情况进行了测试。

我们在平台 source set 中构建具体测试,并通过调用 common 层的测试代码来完成测试,iOS 平台的代码简单示例如下:
class MMKVKotlinTestIos {    private lateinit var mmkvTest: MMKVKotlinTest    @BeforeTest    fun setUp() {        initialize()        mmkvTest = MMKVKotlinTest().apply {            setUp()        }    }    @AfterTest    fun setDown() {        mmkvTest.testDown()    }    @Test    fun testCommon() = with(mmkvTest) {        testBoolean()        // Call other test functions    }    // Test NSDate and NSCoding......}
我们通过注解构建测试,并调用 common 层的代码执行具体测试,最后还需要编写仅 iOS 平台支持的 NSDate 与 NSCoding 类型的测试(代码在上面的示例中省略),单元测试即构建完成。
5.2 Android 插桩测试


MMKV-Kotlin 纯粹的单元测试在 Android 平台是无法正常运行的,原因在于 Android 的单元测试并不支持包含原生二进制代码的测试。前文提到过,MMKV core 是 C++ 编写的,在 Android 平台的构建产物为 so 库。MMKV-Android 构建出的 aar 以及 MMKV-Kotlin 构建出的 aar 都包含了这个 so 库。但该 so 库是针对 Android 平台的二进制程序,并不能在开发者常用的 Windows 或 Mac 电脑上运行。因此我们需要构建插桩测试(instrumented test)将我们的测试代码打包成测试 APK 在真机上运行,测试类的代码如下:
@RunWith(AndroidJUnit4ClassRunner::class)@SmallTestclass MMKVKotlinTestAndroid {    private lateinit var mmkvTest: MMKVKotlinTest    @Before    fun setUp() {        val context = ApplicationProvider.getApplicationContext<Context>()        initialize(context)        mmkvTest = MMKVKotlinTest().apply {            setUp()        }    }    @After    fun setDown() {        mmkvTest.testDown()    }    @Test    fun testCommon() = with(mmkvTest) {        testBoolean()        // Call other test functions    }    // Test Parcelable......    @Test    fun testIPCUpdateInt() { ... }    @Test    fun testIPCLock() { ... }}
测试的构建方式与 5.1 小节中 iOS 的构建方式并无二致。我们除了测试了通用类型及 Android 平台特定的 Parcelable 外,还添加了对 Android 平台跨进程访问的测试,即 testIPCUpdateInt 与 testIPCLock 函数。为了完善跨进程测试,我们还需额外定义一个运行在其他进程的 Service(代码见参考链接 4)。跨进程访问测试的设计也完全参考了 MMKV,见参考链接 5。

在 Android Studio 中点击“Make Project”(图标为一个小锤子)右边的下拉选项栏,然后点击“Edit Configurations...”选项,在弹窗中点击左上角的“+”然后选择“Android Instrumented Test”,即可开始配置插桩测试。配置的截图如下:

连接真机,然后运行即可。
六. Maven Central 发布


Maven Central 可谓是 Android 与 Java 技术领域内分发项目的关键一环,开源作者除了要将代码开源到 Github 以外,通常还要将项目的构建产物发布至 Maven Central,以便于用户以最便捷的方式集成开源库。使用 Gradle 进行发布的常见流程如下:
      注册 sonatype JIRA 账号,登录后提交一个 issue 用于注册自己发布时会用到的 group id。
      本地安装 GPG suit 后生成密钥,然后上传公钥。
      在 Gradle 脚本中引入 maven-publish 与 signing plugin。
      编写发布/签名脚本,配置发布参数。
      执行 publish task。
      登录 Nexus repository manager(参考链接 6,后文简称 Nexus)处理发布申请。


发布成功后,用户即可在 Gradle 以及 Maven 等构建工具中通过一行代码导入你的开源库。

我相信这个过程对于有 Maven 发布经验的 Android 及 Java 开发者来说并不陌生。但对于 Kotlin Multiplatform 开发者来说,部分细节有所不同,且网上资料较少,这里会记录一下踩坑记录。

Kotlin Multiplatform 工程通常的发布方式是将所有构建产物统一发布,这其中包括 Android 平台的 aar 文件,JVM 平台的 jar 文件,Kotlin/Native 的构建产物 klib 文件等。例如一次 publish 后,Nexus 上发布的内容目录结构如下:

我们可以看到共有 5 个目录,其中 mmkv-kotlin 代表 common 层,通常 Multiplatform 工程只需要在 common source set 中对它添加依赖,即可在各平台 source set 中自动获取依赖。

而 mmkv-kotlin-android 代表 Android 平台的产物,其内部的核心是个 aar 文件,与任何纯粹的 Android 库的结构没有任何区别。由于 Android 在 Gradle 中本身就有完整的构建发布体系, 所以 Android aar 的发布需要手动配置发布的变体,例如(kts):
kotlin {    android {        publishLibraryVariants("release")    }    // ......}
我们配置了只发布 release 变体,也可以同时传入 "debug" 参数,将 debug 变体一同发布。

另外三个是 iOS 构建产物,分别对应:iphone 真机(iosarm64)、M1 & M2 芯片的 Mac 上的 iOS 模拟器(iossimulatorarm64)、Intel 芯片的 Mac 上的 iOS 模拟器(iosx64)。它们的核心都是 klib 文件,klib 是纯 Kotlin 工程间互相引用的专用格式,例如 target 为 iOS 系统的纯 Kotlin/Native 工程可以单独添加对这几个 iOS klib 的依赖,从而使用 MMKV-Kotlin。但考虑到 Kotlin/Native 在 iOS 单平台开发中好像并不存在实际使用场景和需求,因此 MMKV-Kotlin 的文档中并没有将这几个 klib 的依赖代码列出。

最后看一下 Gradle 发布脚本(kts):
publishing {    publications.withType<MavenPublication> {        artifact(javadocJar)        with(pom) {            // pom setting......        }    }    repositories {        maven {            credentials {                username = NEXUS_USERNAME                password = NEXUS_PASSWORD            }            url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2")        }    }    signing {        sign(publishing.publications)    }}
在脚本中我们依次配置了 javadoc、pom 信息、仓库信息(用户名、密码、上传的地址)以及签名。上述 kts 代码添加到 gradle.build.kts 文件后,sync 项目,然后运行 publish Gradle task,即可完成发布。

最后有一个坑点需要注意,如果你不想将你的工程名称作为 artifact id,则可以在 publications.withType<MavenPublication> { ... } 内进行配置并覆盖,只需给 artifactId 属性重新赋值即可。但目前实测,覆盖该属性后只有 multiplatform 与 iOS 的 artifact id 会发生改变,对 Android 无效(Gradle 7.2,Kotlin 1.6.10、1.6.21),Android 会始终使用工程名作为 artifact id。这个坑需要尤为注意,避免 Android 的 artifact id 与其他平台皆不相同的情况出现。
七. 总结与未来计划


MMKV-Kotlin 利用了 Kotlin 在各原生平台能够与“土著语言”(Java、C、Objective-C,与 Swift 的交互正在开发中)直接交互的特性,将原本支持在多个平台运行的 MMKV 移植到了 Kotlin Multiplatform 技术栈。为了让原本 MMKV 用户有较小的迁移学习成本,MMKV-Kotlin 的 API 与 MMKV 保持了高度一致性,但从避免重名等因素考量,部分 API 的命名做了一些改变。例如:MMKV 之于 MMKV_KMP, encode 之于 set 等等。MMKV-Kotlin 也尽量完整保留了 MMKV 平台特有的特性,可以方便 Kotlin Multiplatform 开发者在平台相关的 source set 中使用。此外,MMKV-Kotlin 也设计了与 MMKV 类似的单元测试,覆盖了绝大部分核心 API,并在 Android 平台上设计了插桩测试用以检测多进程访问的正确性。

Kotlin Multiplaform 与 MMKV 都不仅仅支持 Android/iOS 两个平台。起初,MMKV-Kotlin 只支持 Android 与 iOS 两个移动端平台,但在 1.1.1 版本中已经添加了对 macOS(包括 Intel 与 M1&M2 芯片架构)的支持。导入的方式为在 Kotlin/Native 工程的 Gradle 脚本(kts)中添加:
dependencies {     // Intel 芯片    implementation("com.ctrip.flight.mmkv:mmkv-kotlin-macosx64:1.1.1")    // M1&M2 芯片    implementation("com.ctrip.flight.mmkv:mmkv-kotlin-macosarm64:1.1.1")}
如果你的 Kotln/Native 工程是可执行程序,记得在 CocoaPods 中添加对 MMKV 的依赖,并添加对 MMKV 及 MMKVCore 的 link 配置,具体方式可参见 MMKV-Kotlin 的 README(参考链接 7)。

由于 macOS 版本的 MMKV 也通过 Objective-C 暴露 API,且也可以通过 CocoaPods 集成,因此添加 macOS 的支持只需在 Gradle 构建脚本中添加对应的 source set 即可,实现起来并不困难。其他 Apple 操作系统( watchOS、tvOS)MMKV 暂未直接支持,因此 MMKV-Kotlin 对它们的支持还在论证之中,如果可行,后续会将所有 Apple 平台列入支持计划之中。由于 Win32、Linux 等平台的 MMKV 通过 C++ 暴露 API,鉴于 Kotlin/Native 与 C++ 的互操作性不完善,以及 JetBrains 官方未来对 C++ 互操作性开发持消极态度(已经移出了 Kotlin roadmap),且目前 Kotlin 开发者对这两个平台的开发需求没有那么迫切,因此暂不考虑列入支持计划。

由于 MMKV 与 Kotlin 会时常更新版本,因此 MMKV-Kotlin 会紧随二者进行迭代。若 MMKV 或 Kotlin 进行了升级,MMKV-Kotlin 未来都会进行跟进升级,请使用者确保 MMKV-Kotlin 依赖的 MMKV 或 Kotlin 版本与您使用的版本兼容。

后续携程机票移动端研发团队也会继续深耕 Kotlin Multiplatform 技术领域,为整个技术社区带来更多的干货与贡献。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-26 09:48 , Processed in 0.091225 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表