Сборка и компилятор
Между исходником Kotlin и готовым артефактом стоит конвейер, а не одна кнопка. Сначала работает компилятор: его фронтенд разбирает текст, разрешает имена и типы, проверяет программу и строит семантическую модель, а бэкенд опускает эту модель в байткод JVM, нативный код или JS. Поверх компилятора Gradle оркеструет сборку — разбивает проект на модули, кэширует и перекомпилирует только изменившееся, между делом гоняет обработчики аннотаций (kapt или KSP), генерирующие код. И только в самом конце, уже под Android, к байткоду применяются пакетные шаги: сборка нужного build-варианта и прогон R8, который ужимает и обфусцирует релиз. Скорость сборки и корректность релиза рождаются на разных этажах этого конвейера — и ловушки там тоже разные.
Главные водоразделы темы стоит назвать сразу. K2 — это фронтенд, а не бэкенд и не плагин Gradle; он стал дефолтным и стабильным в Kotlin 2.0, а версия 2.4 удалила K1 полностью, так что «включать K2 флагом» уже не о чем — выбора нет. Обработка аннотаций идёт на этапе компиляции, а не в рантайме через рефлексию, и kapt с его Java-заглушками — тупиковая ветка рядом с KSP. Скорость инкрементальной сборки живёт в границах модулей: перекомпилирует не любая правка, а изменение ABI, и api против implementation решает, пойдёт ли это изменение каскадом. А самые дорогие в отладке баги — те, что есть только в release: их почти всегда оставляет R8, вырезавший код, до которого доходит лишь рефлексия. Разбор по слоям — ниже.
Карта темы
- Фронтенд K2 — что делает фронтенд компилятора против бэкенда, что такое FIR и почему K2 стал дефолтным и стабильным в Kotlin 2.0.
- Единый фронтенд и миграция — один анализ на все бэкенды, почему K2 строже старого фронтенда, что пришлось переписать в плагинах и как 2.4 удалил K1.
- kapt против KSP — обработка аннотаций как кодогенерация на этапе компиляции, Java-заглушки
kaptпротив прямого чтения символовKSP. - Gradle Kotlin DSL — компилируемые build-скрипты с типизированными аксессорами, convention-плагины против копипаста и version catalog.
- Модули и скорость сборки — почему разбиение на модули ускоряет сборку, ABI против тела функции и
apiпротивimplementationкак главный рычаг. - Build-варианты — вариант как произведение build-типа и product-flavor, свой source set у каждого и взрыв числа комбинаций.
- R8: сжатие и обфускация — tree-shaking, оптимизация, обфускация и ресурсы за один проход; классическая ловушка рефлексии и keep-правила.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Менять роли местами: код генерирует фронтенд, типы проверяет бэкенд | Наоборот: фронтенд разрешает имена и типы, бэкенд опускает результат в байткод/нативный код/JS |
| Называть K2 экспериментальным preview под флагом | K2 стабилен и дефолтен с 2.0, а в 2.4 K1 удалён — включать нечего |
| Считать K2 бэкендом или плагином Gradle | K2 — переписанный фронтенд; унифицирован именно он, а не бэкенды |
| Ждать, что смена фронтенда совместима на уровне исходников | K2 строже: всплывают латентные ошибки smart cast и nullability, которые K1 пропускал |
| Думать, что плагин под K1 просто продолжит работать под K2 | Плагины переписаны под FIR-API; неперенесённый плагин под K2 не запускается |
| Думать, что обработка аннотаций идёт в рантайме через рефлексию | Это кодогенерация на этапе компиляции: обработчик читает объявления и пишет новые исходники |
Считать, что процессор kapt заработает под KSP без правок | Это разные API; переход идёт per-library, и каждая библиотека обязана поставлять процессор KSP |
Приписывать ускорение KSP параллелизму | Оно от отказа от прохода с Java-заглушками и javac, а не от параллельного запуска тех же обработчиков |
| Продавать Kotlin DSL как более быструю сборку | Он даёт типизацию и рефакторинг, а холодная сборка от компиляции скриптов даже чуть медленнее |
| Ждать, что дублирование уберёт сам DSL | Дедупликацию даёт convention-плагин, а не переход на .kts |
| Думать, что любая правка перекомпилирует всю цепочку модулей | Тело почти ничего не перекомпилирует; каскад запускает только изменение ABI |
| Считать правку тела дорогой, а смену сигнатуры дешёвой | Всё наоборот: тело локально, а сигнатура — это ABI, по которой пересобираются зависящие |
Выставлять всё через api, чтобы модули видели друг друга | api протекает в compile-classpath потребителей, и изменение снова идёт каскадом — берите implementation |
| Резать монолит по слоям, а не по фичам | Правка одной фичи всё равно задевает каждый слой-модуль — выигрыша по инкрементальности нет |
| Считать build-вариант флагом времени выполнения внутри одного артефакта | Вариант — отдельная сборка со своим source set: build-тип × product-flavor |
Думать, что R8 только сжимает архив или шифрует байткод | Он делает tree-shaking, оптимизацию, переименование и сжатие ресурсов — код удаляется и переименовывается |
Верить, что R8 не тронет класс, раз до него доходят рефлексией в рантайме | Без keep-правила статически недостижимый класс вырезается; чинится @Keep, а не isMinifyEnabled = false |
Значение для собеседований
Тулчейн сборки — раздел, где интервьюер проверяет не словарь, а модель конвейера: в какой момент какой этап что делает и что именно ломает его кэширование. Классический разделяющий вопрос — «что такое K2»: правильный ответ не «новая версия», а «переписанный фронтенд компилятора, стабильный и дефолтный с 2.0, единственный после 2.4». Следующий уровень — практический: почему сборка медленная (граница модулей и ABI, а не число строк) и почему release падает, а debug нет (R8, который есть только там). Кандидат, который чинит эти симптомы отключением минификации или добавлением heap демону Gradle, не понимает, что происходит под капотом.
Что обычно проверяют:
- Роли фронтенда и бэкенда и то, что K2 — именно фронтенд, стабильный с 2.0, а K1 удалён в 2.4.
- Почему один общий фронтенд важен для мультиплатформы и что пришлось переписать в плагинах.
- Чем
KSPбыстрееkapt— что источник ускорения — отказ от Java-заглушек, а не параллелизм. - Что реально даёт Kotlin DSL и где дедупликацию делает convention-плагин, а не сам DSL.
- Что обесценивает инкрементальную сборку: ABI против тела,
apiпротивimplementation, резать по фичам. - Что такое build-вариант и что у него свой source set.
- Что
R8делает с release и почему рефлексивной модели нужен keep-правило.
Типичный неверный ответ: «K2 — это новый быстрый бэкенд, который включают флагом ради скорости сборки». Здесь три ошибки в одной фразе. Унифицирован фронтенд, а не бэкенды; K2 не «включают» — он дефолт с 2.0 и единственный с 2.4; и он не про скорость сборки, а про единый анализ и более строгую проверку — из-за строгости миграция как раз означает починку новых ошибок, а не бесплатное ускорение. Тот же кандидат обычно верит, что плагин под K1 переживёт апгрейд сам собой, — а неперенесённый под FIR плагин под K2 просто не запустится.