> Как работает компиляция в Kotlin Multiplatform (Kotlin, Android)
Уровень: senior · Роль: mobile · Категория: Технические вопросы
Компании: IBS
Стек: Kotlin, Android
> Пример ответа
Короткий ответ
Kotlin Multiplatform компилирует общий код в промежуточное представление (IR), а затем - в нативные бинарники или байткод для каждой целевой платформы. Для Android и iOS используется один и тот же frontend (Kotlin compiler), но разные backend'и: JVM-байткод для Android и Kotlin/Native (LLVM) для iOS. Платформенный код остаётся на Kotlin/JVM или Swift/Objective-C, связь обеспечивается через expect/actual и сгенерированные фреймворки.
Подробное объяснение
Компиляция в Kotlin Multiplatform - это многоступенчатый процесс, который зависит от выбранных таргетов. Основные этапы:
-
Frontend (общий для всех таргетов): исходный код на Kotlin парсится, проверяется типизация, строится семантическая модель. Здесь работает expect/actual резолвинг: компилятор сопоставляет ожидаемые объявления с их фактическими реализациями для конкретной платформы.
-
Backend для JVM (Android): генерируется байткод JVM, который затем упаковывается в DEX (через d8/r8) и интегрируется в APK. Здесь нет ничего специфичного для KMP - это стандартный Kotlin/JVM, но с учётом иерархии промежуточных исходных наборов (source sets).
-
Backend для Kotlin/Native (iOS): используется LLVM. Код компилируется в нативный машинный код для конкретной архитектуры (arm64 для устройств, x86_64 для симулятора). На выходе получается статическая библиотека или фреймворк, который затем линкуется с Swift/Objective-C кодом.
-
Иерархия source sets: компилятор собирает код из общего source set (
commonMain), платформенных (androidMain,iosMain) и промежуточных (например,iosArm64Main). Для каждого таргета строится своя комбинация. -
Kotlin/Native компиляция в два прохода: сначала компилируется сам Kotlin-код в LLVM IR, затем этот IR оптимизируется и линкуется с рантаймом Kotlin/Native и зависимостями (например, с cinterop-библиотеками).
-
Генерация Objective-C header: для iOS Kotlin/Native автоматически генерирует заголовочный файл и экспортирует Kotlin-классы как Objective-C классы, что позволяет вызывать их из Swift.
Важный нюанс: компиляция в KMP не является "компиляцией один раз - запуск везде". Это скорее "написание один раз - компиляция для каждой платформы отдельно". Общий код компилируется повторно для каждого таргета, что увеличивает время сборки, но даёт нативную производительность.
На практике
На практике процесс выглядит так:
- Gradle-плагин (
kotlin-multiplatform) создаёт для каждого таргета отдельную компиляционную задачу. Например,compileKotlinIosArm64,compileKotlinAndroid. - Android-таргет использует стандартный Kotlin/JVM компилятор, поэтому интеграция с Android Gradle Plugin происходит через KotlinCompile task.
- iOS-таргет требует настройки
binaries.framework, который генерирует.frameworkдля последующего использования в Xcode. Обычно создаются два варианта: debug и release, а также отдельно для симулятора и устройства. - CocoaPods или Swift Package Manager используются для подключения сгенерированного фреймворка к iOS-проекту.
- Рантайм Kotlin/Native включается в итоговый бинарник, поэтому размер приложения увеличивается (обычно на 1-3 МБ на архитектуру).
- Для iOS симулятора и устройства компиляция происходит отдельно, поэтому часто используют
iosX64(симулятор на Intel),iosArm64(устройство) иiosSimulatorArm64(симулятор на Apple Silicon). Это три разные компиляции одного и того же кода.
Пример кода
// commonMain/kotlin/Platform.kt expect fun platformName(): String // androidMain/kotlin/Platform.android.kt actual fun platformName(): String = "Android" // iosMain/kotlin/Platform.ios.kt actual fun platformName(): String = "iOS"
// build.gradle.kts kotlin { androidTarget { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } listOf( iosArm64(), iosSimulatorArm64(), iosX64() ).forEach { iosTarget -> iosTarget.binaries.framework { baseName = "Shared" isStatic = true } } sourceSets { commonMain.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") } androidMain.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0") } } }
Как отвечать на собеседовании
Начните с общего описания двух backend'ов и того, что общий код компилируется отдельно для каждой платформы. Затем перейдите к деталям: упомяните expect/actual, иерархию source sets, генерацию Objective-C header для iOS. Если спросят про производительность - подчеркните, что нативный код не требует рантайма JVM на iOS, но рантайм Kotlin/Native всё равно присутствует. Если спросят про время сборки - честно скажите, что KMP увеличивает время компиляции, особенно для iOS, и что это решается кэшированием и параллельными тасками.
Хорошо показать понимание разницы между iosArm64, iosX64 и iosSimulatorArm64 - это демонстрирует практический опыт. Также можно упомянуть, что для Android используется стандартный Kotlin/JVM, поэтому никаких сюрпризов там нет.
Что проверяет интервьюер
Интервьюер проверяет:
- понимание того, что KMP - это не "write once, run anywhere", а компиляция под каждую платформу;
- знание роли expect/actual в процессе компиляции;
- понимание иерархии source sets и как Gradle это моделирует;
- осведомлённость о генерации Objective-C фреймворков для iOS;
- способность объяснить, почему для iOS нужно несколько таргетов (симулятор/устройство, Intel/ARM);
- понимание trade-off'ов: время сборки, размер бинарника, сложность настройки.
Типичные ошибки
- Утверждение, что Kotlin/Native компилирует код в байткод, который выполняется в виртуальной машине - это неверно, там LLVM и нативный машинный код.
- Игнорирование того, что Android-таргет использует обычный Kotlin/JVM, а не Kotlin/Native.
- Путаница между
iosArm64иiosSimulatorArm64- это разные архитектуры, и код компилируется отдельно. - Заявление, что общий код компилируется один раз и переиспользуется - на самом деле он компилируется для каждого таргета.
- Непонимание, что expect/actual резолвится на этапе компиляции, а не в рантайме.
- Забывание про рантайм Kotlin/Native, который линкуется в итоговый бинарник и увеличивает его размер.
> Похожие задачи по mobile
Как управлять жизненным циклом корутины в Kotlin
Какой диспетчер использовать для загрузки ресурса с сети в Kotlin корутинах
Какие проблемы возникают при оборачивании launch в try-catch в Kotlin Coroutines
В чем разница между Int и Int? в Kotlin и когда Int рассматривается как объект, а когда как примитив
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью