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 构建体系:从目标声明到任务图与增量编译 版本目录 libs.versions.toml 统一 Kotlin / 依赖版本 Kotlin Gradle Plugin 接入编译器与平台目标 生成源集与编译任务 多目标声明 jvm / android / ios / js 自动派生平台源集 依赖配置 commonMain 平台源集 源集层级与依赖传递 commonMain 协程 / 序列化 / Ktor jvmMain JVM 特有 API androidMain Android SDK iosMain Foundation / UIKit 任务图与增量编译 compileCommon commonMain 编译 compileJvm JVM 目标编译 compileIosArm64 iOS 目标编译 增量编译判定 输入变化 → 重编译 源集边界清晰 + 输入稳定 + 输出可追踪 = 增量编译生效

构建变体与平台差异

构建变体在 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 库。

版本目录、多目标声明和增量编译是 KMP 构建体系的三根支柱。版本目录保证版本一致,多目标声明保证平台覆盖,增量编译保证开发效率。三者配合,才能让 KMP 工程在多个平台之间稳定、快速地构建。