4. Gradle与KMP构建体系
Kotlin Multiplatform 的工程能力几乎全部建立在 Gradle 之上。理解 KMP,本质上要先理解 Gradle 如何把一份 Kotlin 源码编译到多个平台。Kotlin Gradle Plugin(简称 KGP)是这套机制的入口:它把 Kotlin 编译器、平台目标、源集、依赖和任务接入 Gradle 的生命周期。没有 KGP,Gradle 只是一个通用构建工具;有了 KGP,Gradle 才知道“commonMain 里的代码要同时参与 Android、iOS、JVM 的编译”。
KGP 与多目标声明
在 build.gradle.kts 中,KMP 工程通过 kotlin { } 块声明目标。每个目标代表一个编译产物平台,例如 jvm()、androidTarget()、iosArm64()、iosSimulatorArm64()、js(IR) { browser() }。目标声明不是简单罗列,它会自动生成对应的源集:commonMain、jvmMain、androidMain、iosMain 等。源集之间通过 dependsOn 形成层级,公共代码放在上层,平台特有代码放在下层。
一个典型的多目标声明如下:
kotlin {
androidTarget()
jvm()
iosArm64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
}
val commonTest by getting {
dependencies { implementation(kotlin("test")) }
}
}
}
这里的关键是:commonMain 的依赖会自动参与所有目标的编译,而 iosMain 只参与 iOS 目标。KGP 会根据目标自动创建 iosMain 这样的中间源集,避免为每个 iOS 架构重复写依赖。
依赖配置与版本目录
KMP 的依赖配置分为 commonMain、commonTest 和各平台源集。依赖坐标必须支持对应平台,否则会在解析阶段失败。例如 kotlinx-coroutines-core 是 multiplatform 库,可以放在 commonMain;而 androidx.activity:activity-compose 只能放在 androidMain。
版本目录(Version Catalog)通过 gradle/libs.versions.toml 统一管理版本,避免多模块版本漂移。示例:
[versions]
kotlin = "2.0.20"
coroutines = "1.8.1"
[libraries]
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
在构建脚本中通过 libs.kotlinx.coroutines.core 和 libs.plugins.kotlinMultiplatform 引用。版本目录的价值在于:升级 Kotlin 版本时只改一处,所有模块和插件同步生效。
构建变体与任务图
KMP 的构建变体来自“目标 + 编译类型”的组合。例如 Android 目标有 debug/release,iOS 目标有 debug/release,JVM 目标也有 main/test。每个变体会生成一组任务:compileKotlinJvm、compileKotlinIosArm64、compileDebugKotlinAndroid 等。这些任务通过 dependsOn 形成任务图:commonMain 的编译任务先执行,平台源集任务依赖它,最终链接成可执行产物。
任务图不是线性的,而是有向无环图。理解任务图有助于定位构建失败:如果 compileKotlinIosArm64 失败,通常先检查 commonMain 是否使用了 iOS 不支持的 API,或者 iosMain 的依赖是否缺失。
增量编译
KGP 支持增量编译:只重新编译受影响的源文件和依赖。增量编译依赖三个条件:输入稳定、输出可追踪、任务可缓存。常见破坏增量的做法包括:在构建脚本中动态生成源码、使用 buildConfig 写入时间戳、在 commonMain 中引用随机值。保持源集边界清晰、避免跨源集循环依赖,是增量编译生效的前提。
构建变体与平台差异
构建变体在 KMP 中体现为“目标 × 编译类型”。Android 目标会生成 debug 和 release 两个变体,iOS 目标同样有 debug/release。不同变体可以有不同的依赖和编译选项。例如在 androidMain 中,debug 变体可以依赖 chucker 网络调试库,release 变体则排除它。KGP 通过 androidTarget { compilations.all { ... } } 或 buildTypes 配置变体差异。
平台差异还体现在编译选项上:JVM 目标可以设置 jvmTarget = "17",iOS 目标可以设置 freeCompilerArgs += "-Xbinary=bundleId=com.example.app"。这些选项只影响对应目标,不会污染其他平台。
任务图与增量编译的实践判断
当构建变慢时,先看任务图:执行 ./gradlew :shared:tasks --all 可以列出所有任务。如果 compileKotlinIosArm64 每次都重新执行,说明增量编译未生效。常见原因包括:commonMain 中使用了 System.currentTimeMillis()、构建脚本中动态生成源码、或者源集之间出现了循环依赖。修复方式是:把时间戳逻辑移到平台源集,把生成代码的任务声明为 @CacheableTask,并确保 commonMain 只依赖稳定的 multiplatform 库。