Skip to content

从 0 到 1 快速上手 KMP

源:Kotlin Multiplatform 官方文档

Kotlin Multiplatform (KMP) 不再是未来,而是当下。它允许您在保持 100% 原生性能的同时,在 Android、iOS、Desktop 和 Web 之间共享核心业务逻辑。

环境准备清单

在开始之前,请确保您的开发机器满足以下要求:

  • JDK: 版本 17 或更高。
  • Android Studio: 推荐 Koala (2024.1.1) 或更高版本,并安装 Kotlin Multiplatform 插件。
  • Xcode: (仅 iOS 开发需要) 建议最新稳定版。
  • Kotlin: 本教程基于 Kotlin 2.3.0

自动化检查工具

推荐运行 kdoctor 工具来诊断环境问题:

bash
brew install kdoctor
kdoctor

创建首个 KMP 项目

最简单的方式是使用官方的 KMP Wizard

  1. 选择目标平台:Android, iOS (Share UI via Compose)iOS (Native UI), Desktop
  2. 输入项目名称和包名。
  3. 下载生成的项目压缩包并解压。
  4. 在 Android Studio 中打开 build.gradle.kts

项目骨架拆解

一个典型的 KMP 项目(以 shared 模块为核心)结构如下:

  • shared/src/commonMain: 核心共享代码(逻辑、数据模型、API 调用)。
  • shared/src/androidMain: Android 特有的 API 实现。
  • shared/src/iosMain: iOS 特有的 API 实现(可直接调用 Apple 框架)。
  • composeApp: (如果选择了 Compose Multiplatform) 共享 UI 代码。

核心配置:build.gradle.kts

shared 模块中,关键配置如下:

kotlin
kotlin {
    // 1. 定义目标平台
    androidTarget()
    
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "SharedFramework"
            isStatic = true
        }
    }

    // 2. 配置源集依赖
    sourceSets {
        commonMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
        }
        androidMain.dependencies {
            // Android 专用库
        }
        iosMain.dependencies {
            // iOS 专用库
        }
    }
}

跨平台运行

bash
# 在 AS 中直接点击 Run 按钮,或者:
./gradlew :composeApp:installDebug
bash
# 需要在 AS 中配置 iOS 模拟器运行项,
# 或者在 Xcode 中打开 iosApp 目录。
bash
./gradlew :composeApp:run

关键能力详解

本节围绕 shared 模块的关键能力展开,内容遵循 API 核心签名说明 → 标准代码块 → 底层机制说明 的结构,便于直接落地。

源集与 expect/actual 设计

API 核心签名说明

  • expect fun platformName(): String
  • actual fun platformName(): String

标准代码块

kotlin
// commonMain
expect fun platformName(): String

class Greeting {
    fun greeting(): String = "Hello, ${platformName()}!"
}

// androidMain
actual fun platformName(): String = "Android ${android.os.Build.VERSION.SDK_INT}"

// iosMain
import platform.UIKit.UIDevice

actual fun platformName(): String = UIDevice.currentDevice.systemName

底层机制说明

编译器如何解析 expect/actual

K2 在 commonMain 先生成只包含符号的 metadata(.klib 的 common metadata)。 expect 声明被记录为平台未绑定的符号,只保留签名、可见性与注解。 这些符号在 commonMain 不产生可执行实现体,也不会生成可调用入口。 每个目标平台在编译时加载 common metadata,并进入实际化校验阶段。 实际化会遍历 platformMain 中的 actual 声明并建立符号映射表。 匹配规则要求包名、类型参数、可见性与修饰符兼容,否则直接报错。 当匹配成功,调用点在 IR 阶段会绑定到 actual 符号。 JVM 目标上,expect 不产生字节码,只在 Kotlin metadata 中可见。 Native 目标上,expect/actual 会在 konan IR 降级前完成解析。 链接器只看到 actual 的实现,expect 不会出现在导出符号中。 这保证了运行期没有动态分发或反射查询的额外成本。 如果 commonMain 引用了平台能力,编译器会强制你通过 expect 隔离。 当 expect 签名变化时,所有平台模块都会重新触发一致性验证。 K2 在前端就完成匹配,使后续优化可以直接内联 actual 体。 因此性能与平台手写实现一致,差异主要体现在编译期约束。 在大型工程中,保持 expect/actual 的稳定签名能显著减少重新构建范围。

协程调度与跨平台主线程

API 核心签名说明

  • interface CoroutineScope
  • fun CoroutineScope.launch(block: suspend CoroutineScope.() -> Unit): Job
  • suspend fun <T> withContext(context: CoroutineContext, block: suspend () -> T): T
  • object Dispatchers { val Main: CoroutineDispatcher; val Default: CoroutineDispatcher }

标准代码块

kotlin
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class UserPresenter(
    private val mainDispatcher: CoroutineDispatcher,
    private val ioDispatcher: CoroutineDispatcher,
    private val repo: UserRepository,
) {
    private val scope = CoroutineScope(SupervisorJob() + mainDispatcher)

    fun refresh() {
        scope.launch {
            val user = withContext(ioDispatcher) { repo.fetch() }
            onUserLoaded(user)
        }
    }

    fun clear() {
        scope.cancel()
    }

    private fun onUserLoaded(user: User) { /* 平台层实现 */ }
}

// androidMain
fun createPresenter(repo: UserRepository) = UserPresenter(
    mainDispatcher = Dispatchers.Main.immediate,
    ioDispatcher = Dispatchers.IO,
    repo = repo
)

// iosMain
fun createPresenter(repo: UserRepository) = UserPresenter(
    mainDispatcher = Dispatchers.Main,
    ioDispatcher = Dispatchers.Default,
    repo = repo
)

底层机制说明

协程状态机与调度器在多平台的实现

suspend 函数在编译期被拆解为带 Continuation 参数的状态机。 每个挂起点都会生成一个 label,并把局部变量保存到字段中。 JVM 目标会生成 invokeSuspend 方法,并在字节码中实现状态跳转。 Native 目标在 IR 降级时同样生成状态机,只是进入 konan 后端。 launch 会创建 StandaloneCoroutine,并把 Job 放入 CoroutineContext。 SupervisorJob 会改变子协程的失败传播逻辑,避免级联取消。 withContext 会创建 DispatchedCoroutine,并在恢复时切换到目标调度器。 Dispatchers.Main 通过 MainDispatcherFactory 延迟加载主线程实现。 Android 上主线程调度由 HandlerDispatcher 绑定 Looper 实现。 iOS 上主线程调度绑定 dispatch_get_main_queue,保证 UI 线程安全。 Dispatchers.Default 在 JVM 上使用 DefaultScheduler 的共享线程池。 Native 上 Default 绑定一个受限 worker 池,避免过多线程开销。 Dispatchers.IO 仅存在于 JVM,用于标记阻塞任务并扩容线程池。 取消检查在挂起点与恢复点发生,避免在热路径中频繁检测。 如果缺少 MainDispatcher,MainDispatcherLoader 会在首次访问时报错。 因此多平台工程通常将调度器作为依赖注入,确保平台差异显式化。

依赖补充

kotlin
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
        }
        androidMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
        }
    }
}
groovy
kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0"
            }
        }
        androidMain {
            dependencies {
                implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
            }
        }
    }
}
toml
[versions]
kotlinx-coroutines = "1.9.0"

[libraries]
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }

最新稳定版本查看链接:https://github.com/Kotlin/kotlinx.coroutines/releases

序列化与跨平台数据模型

API 核心签名说明

  • @Serializable
  • interface KSerializer<T>
  • fun Json.encodeToString(value: T): String
  • fun Json.decodeFromString(string: String): T

标准代码块

kotlin
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

@Serializable
data class User(
    val id: String,
    val name: String,
)

private val json = Json {
    ignoreUnknownKeys = true
}

fun encodeUser(user: User): String = json.encodeToString(user)

fun decodeUser(payload: String): User = json.decodeFromString(payload)

底层机制说明

序列化编译器插件的生成过程

@Serializable 会触发编译器插件在 IR 阶段生成序列化实现类。 插件为每个模型生成 User$$serializer 并实现 KSerializer<User>。 生成类会提供 serializer() 入口,供 Json 在运行时直接调用。 SerialDescriptor 在编译期构建,包含字段名、可空性与默认值信息。 这些元数据用于决定字段的序号与编码顺序,避免反射扫描。 插件会生成 serialize 与 deserialize 方法,调用 Encoder/Decoder 接口。 encodeStructure 会依次写入字段索引,decodeStructure 通过索引读取。 Decoder 会调用 decodeElementIndex 确定字段位置与顺序。 默认参数通过位掩码与合成构造函数完成,避免重复分配。 JVM 上生成物是普通字节码类,不依赖反射,序列化成本可预测。 Native 与 JS 目标会生成同等逻辑的实现,保证跨平台一致性。 Json.encodeToString 会优先读取生成的 serializer,而不是动态推断。 SerializersModule 提供多态与上下文序列化的运行期绑定能力。 ignoreUnknownKeys 由 JsonDecoder 在解析阶段过滤未知字段。 当模型字段变化时,编译器会重新生成 descriptor,避免运行期偏差。 因此序列化行为由编译期固定,运行时只做结构化读写与配置判断。

依赖补充

kotlin
plugins {
    kotlin("multiplatform")
    kotlin("plugin.serialization")
}

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
        }
    }
}
groovy
plugins {
    id "org.jetbrains.kotlin.multiplatform"
    id "org.jetbrains.kotlin.plugin.serialization"
}

kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
            }
        }
    }
}
toml
[versions]
kotlin = "2.3.0"
kotlinx-serialization = "1.7.3"

[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

最新稳定版本查看链接:https://github.com/Kotlin/kotlinx.serialization/releases

原生框架导出与集成

API 核心签名说明

  • binaries.framework { baseName: String; isStatic: Boolean }
  • freeCompilerArgs += listOf("-Xobjc-generics")

标准代码块

kotlin
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

kotlin {
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    targets.withType<KotlinNativeTarget>().configureEach {
        binaries.framework {
            baseName = "SharedKit"
            isStatic = true
            freeCompilerArgs += listOf("-Xobjc-generics")
        }
    }
}

底层机制说明

原生框架构建的编译与链接过程

commonMain 先被编译为平台无关的 metadata 并打包为 .klib。 每个 iOS 目标会生成目标平台的 .klib,并进入 native 后端。 compileKotlinIosArm64 将 IR 转换为 LLVM IR 并编译为对象文件。 linkDebugFrameworkIosArm64 会把对象文件链接成 .framework 产物。 .framework 内包含二进制、Headers 与 Modules,用于 Xcode 导入。 Kotlin/Native 根据 public API 生成 Objective-C 头文件并暴露符号。 -Xobjc-generics 让生成的头文件带上泛型信息,提升 Swift 侧类型提示。 isStatic = true 生成静态 framework,动态 framework 则会生成 dylib。 模拟器与真机需要不同目标,iosSimulatorArm64 用于 Apple Silicon。 Gradle 任务会分别产出每个目标的 framework,再由脚本拷贝到 Xcode。 assembleXCFramework 可以把多目标 framework 合并为单个 XCFramework。 调试构建通常还会生成 dSYM 以便 Xcode 符号化。 Kotlin/Native 运行时提供 ObjCExport 桥接,处理 Swift/ObjC 调用。 内存管理由 Kotlin/Native 运行时与 ARC 互操作完成引用计数同步。 Swift 侧看到的是 Objective-C 接口,因此可用标准的 Swift 调用方式。 缓存与增量编译按目标与编译参数区分,避免不必要的全量重编。

跨平台测试与验证

API 核心签名说明

  • @Test
  • kotlin.test.assertEquals(expected: Any?, actual: Any?)
  • kotlin.test.assertTrue(actual: Boolean)
  • kotlin.test.assertFailsWith<T : Throwable>(block: () -> Unit)

标准代码块

kotlin
import kotlin.test.Test
import kotlin.test.assertTrue

class GreetingTest {
    @Test
    fun greetingHasPlatformName() {
        val message = Greeting().greeting()
        assertTrue(message.isNotBlank())
    }
}
kotlin
import kotlin.test.Test
import kotlin.test.assertTrue

class AndroidPlatformTest {
    @Test
    fun platformNameContainsAndroid() {
        val name = platformName()
        assertTrue(name.contains("Android"))
    }
}
kotlin
import kotlin.test.Test
import kotlin.test.assertTrue

class IosPlatformTest {
    @Test
    fun platformNameContainsSystemName() {
        val name = platformName()
        assertTrue(name.isNotBlank())
    }
}

底层机制说明

多平台测试任务的编译与执行流程

kotlin("test") 在 commonMain 仅暴露统一的断言与 @Test 注解。 这些声明在 commonMain 是 expect,实际实现由各平台的 kotlin-test 变体提供。 JVM 目标使用 kotlin-test-junit 或 kotlin-test-junit5 连接 JUnit 引擎。 Gradle 会为 JVM 与 Android 目标注册 Test 任务并生成测试报告。 androidUnitTest 运行在本地 JVM,不依赖设备或模拟器。 Kotlin/Native 会把测试源码编译成独立的测试二进制。 编译器会生成测试注册表,收集带 @Test 标注的函数引用。 测试入口在 Native 侧由 kotlin-test 运行器自动生成 main。 Gradle 通过 KotlinNativeTest 任务执行测试二进制并解析输出。 iosSimulatorArm64Test 会在模拟器进程中启动该二进制完成测试。 当存在 expect/actual 时,各目标会分别编译并执行以完成实际化校验。 断言失败抛出的 AssertionError 会被测试运行器捕获并回传给 Gradle。 kotlin.test 不依赖反射,因此在 Native 目标上也能稳定运行。 公共逻辑应放入 commonTest,平台 API 测试放入各自的 targetTest。 这样可以在 CI 中并行执行不同平台测试并缩短反馈周期。 公共代码变更会触发多目标测试重编译,保证跨平台一致性。

依赖补充

kotlin
kotlin {
    sourceSets {
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
        androidUnitTest.dependencies {
            implementation(kotlin("test-junit"))
        }
    }
}
groovy
kotlin {
    sourceSets {
        commonTest {
            dependencies {
                implementation "org.jetbrains.kotlin:kotlin-test:2.3.0"
            }
        }
        androidUnitTest {
            dependencies {
                implementation "org.jetbrains.kotlin:kotlin-test-junit:2.3.0"
            }
        }
    }
}
toml
[versions]
kotlin = "2.3.0"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }

最新稳定版本查看链接:https://github.com/JetBrains/kotlin/releases

常见新手坑位

  • Cocoapods 冲突:

推荐优先使用 Apple Framework 导出模式,除非必须集成大量遗留 Pods。

  • 资源访问: KMP 中访问图片和字符串资源需要使用专门的库(如 Compose Multiplatform Resources)。
  • 编译速度: 首次构建 iOS 框架可能较慢,这是因为 Kotlin/Native 编译器正在进行 LLVM 优化。