Kotlin 多平台:KMM 与 expect/actual 跨平台抽象设计
从 KMM 架构、expect/actual 机制、CInterop 出发,解析 iOS Framework 导出、Swift 互操作边界,以及实际项目中的平台差异化策略。
1. KMM 架构概览
1.1 Kotlin Multiplatform 定位
Kotlin Multiplatform(KMP,原 KMM)是一种代码共享策略,允许在 commonMain 中编写平台无关逻辑,通过 expect/actual 机制处理平台差异,最终编译为各平台原生产物。
1.2 Gradle Source Set 布局
KMP 项目通过 Gradle 配置多平台源集,每个平台有独立的 source set:
// kotlin { sourceSets } 配置
kotlin {
sourceSets {
val commonMain = getByName("commonMain")
val androidMain = getByName("androidMain")
val iosArm64Main = getByName("iosArm64Main")
val iosSimulatorArm64Main = getByName("iosSimulatorArm64Main")
commonMain.dependencies {
// Kotlin stdlib
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
iosArm64Main.dependsOn(commonMain)
iosSimulatorArm64Main.dependsOn(commonMain)
androidMain.dependsOn(commonMain)
}
}
2. expect/actual 机制
2.1 基本语法
expect/actual 是 KMP 处理平台差异的核心语法。expect 声明在 commonMain 中,声明要实现的功能;actual 在各平台特定 source set 中实现:
// commonMain/kotlin/Platform.kt
// expect 声明:平台无关接口
expect class Platform() {
val name: String
expect fun getPlatformType(): PlatformType
}
// androidMain/kotlin/Platform.kt
// actual 实现:Android 平台
actual class Platform() {
actual val name: String = "Android"
actual fun getPlatformType(): PlatformType = PlatformType.ANDROID
}
// iosMain/kotlin/Platform.kt
// actual 实现:iOS 平台
actual class Platform() {
actual val name: String = "iOS"
actual fun getPlatformType(): PlatformType = PlatformType.IOS
}
2.2 expect interface vs expect class
expect 支持声明为 class 或 interface。选择依据是平台实现是否有继承/组合需求:
// commonMain: 推荐接口便于多平台实现
expect interface ImageLoader {
expect fun load(url: String): Image
expect suspend fun loadAsync(url: String): Image
}
// iosMain: Swift 风格的实现
actual interface ImageLoader {
actual fun load(url: String): Image {
// 调用 iOS SDWebImage
}
}
// androidMain: 使用 Coil
actual interface ImageLoader {
actual fun load(url: String): Image {
// 调用 Android Coil
}
}
expect/actual 是一对一映射——每个 expect 必须有且仅有一个 actual。如果某平台不需要特殊实现,可以使用 expect val x: T get() = "common" 在 commonMain 中直接实现。
3. CInterop 与原生互操作
3.1 Kotlin/Native C 互操作
Kotlin/Native 内置了 C Interop 机制(cinterop),可以导入 C 库并在 Kotlin 中直接调用。这是 KMP 连接原生系统能力的核心途径:
// kotlin-native/cinterop 配置
// 方式1: build.gradle.kts 中配置
kotlin {
sourceSets {
iosArm64Main {
binaries.getFramework("MyFramework")
}
}
}
// 方式2: 通过 .def 文件定义 C 头文件
// src/nativeInterop/cinterop/mylib.def
headers = lib.h
library = System
// 然后在 build.gradle 中引用
// Kotlin 调用 C 函数
@SymbolName("myCFunction")
external fun myCFunction(arg: Cpointer<Byte>): Int
3.2 iOS Framework 导出
KMP 最终产物可以是 iOS Framework(.framework),通过 export 关键字控制暴露给 Swift 的 API:
// build.gradle.kts
iosArm64 {
binaries {
val framework = getFramework("MySDK")
framework.export(:common:commonMain)
framework.export(:feature:featureMain)
}
}
// Swift 中导入使用
// import MySDK
let platform = Platform()
let imageLoader: ImageLoader = DefaultImageLoader()
4. iOS 导出与 Swift 互操作
4.1 可导出类型限制
并非所有 Kotlin 类型都能导出到 Swift。Kotlin/Native 编译器只支持有限的类型系统:
| Kotlin 类型 | 导出到 Swift | 备注 |
|---|---|---|
| 基础类型(Int, Long, String) | ✅ | 直接映射 |
| 数据类(data class) | ✅ | 映射为 Swift struct |
| 枚举类(enum) | ✅ | 映射为 Swift enum |
| 密封类(sealed class) | ⚠️ 部分 | 需 @Exported 注解 |
| suspend 函数 | ✅ | 导出为 async/await |
| Flow | ⚠️ | 建议转为 Callback 或 NSStream |
| 泛型类 | ❌ | 泛型会被擦除 |
4.2 Swift 互操作边界
KMP 与 Swift 的互操作存在一些边界需要注意:
// ✅ 可以:普通类/枚举导出
data class User(val id: Long, val name: String)
enum PlatformType { ANDROID, IOS, JS }
// ⚠️ 注意:内联函数中的 suspend 无法导出
// ❌ 无法导出
inline fun inlineSuspend(block: suspend () → Unit)
// ❌ 无法导出:泛型参数会擦除
class Wrapper<T>(val value: T)
// Swift 使用示例
// let user = User(id: 1, name: "John")
// let type = PlatformType.IOS
5. 平台差异化策略
5.1 实际项目分层架构
成熟的 KMP 项目通常采用三层架构:业务层(common)→ 领域层(domain)→ 平台层(platform):
// src/commonMain/kotlin/
// 业务层:纯 Kotlin,无平台依赖
class UserRepository(
private val api: UserApi,
private val cache: UserCache
) {
suspend fun getUser(id: Long): Result<User> {
cache.get(id) ?: api.fetchUser(id).also { cache.put(it) }
}
}
// src/androidMain/kotlin/
// 平台层:Android 特定实现
actual class UserCache {
// 使用 SharedPreferences 或 Room
}
// src/iosMain/kotlin/
// 平台层:iOS 特定实现
actual class UserCache {
// 使用 UserDefaults
}
5.2 平台差异决策树
在决定是否将某功能放在 common 还是 platform 时,使用以下决策树:
✅ 放 commonMain
- 业务逻辑(Repository、UseCase)
- 数据模型(data class、sealed class)
- 网络 API 接口定义
- 纯算法、无副作用逻辑
❌ 放 platform source set
- UI 组件(Compose vs SwiftUI)
- 本地存储(Room vs CoreData)
- 平台特定 API(传感器、通知)
- 原生库调用(cinterop)
不要试图用 expect/actual 实现"平台差异很小"的功能——这会导致代码重复和维护负担。优先使用策略模式或依赖注入在 commonMain 中处理差异。