📅 2026-05-24 👤 陈刚 🏷️ Kotlin Multiplatform · iOS · 跨平台

Kotlin 多平台:KMM 与 expect/actual 跨平台抽象设计

从 KMM 架构、expect/actual 机制、CInterop 出发,解析 iOS Framework 导出、Swift 互操作边界,以及实际项目中的平台差异化策略。

1. KMM 架构概览

1.1 Kotlin Multiplatform 定位

Kotlin Multiplatform(KMP,原 KMM)是一种代码共享策略,允许在 commonMain 中编写平台无关逻辑,通过 expect/actual 机制处理平台差异,最终编译为各平台原生产物。

commonMain
Android Lib
iOS Framework
JS Bundle
KMM 编译产物架构

1.2 Gradle Source Set 布局

KMP 项目通过 Gradle 配置多平台源集,每个平台有独立的 source set:

KMP Gradle 配置
// 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 中实现:

expect/actual 示例
// 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。选择依据是平台实现是否有继承/组合需求:

expect 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 连接原生系统能力的核心途径:

C Interop 配置
// 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:

iOS Framework 导出配置
// 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 的互操作存在一些边界需要注意:

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)

KMP 分层架构
// 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 中处理差异。