Сборка и компилятор
Что происходит между исходником и APK — фронтенд K2, обработка аннотаций kapt против KSP, build-варианты, сжатие и обфускация через R8, Gradle Kotlin DSL и разбиение на модули ради скорости инкрементальной сборки.
11 вопросов
JuniorТеорияЧастоЧто такое build-вариант в Android-проекте на Gradle и откуда он берётся?
Что такое build-вариант в Android-проекте на Gradle и откуда он берётся?
Вариант — декартово произведение build-типа (debug/release: отладочность, минификация, ключ подписи) и product-flavor (free/paid: свой код, ресурсы, app id). Каждый вариант — отдельный артефакт со своим source set, собираемый независимо.
Типичные ошибки
- ✗Думать, что вариант — это флаг времени выполнения внутри одного артефакта, а не отдельная сборка
- ✗Путать build-тип с product-flavor или упускать, что вариант — их произведение
- ✗Не знать, что у варианта есть свой source set, который добавляет или подменяет код и ресурсы
Уточняющие вопросы
- →В каком варианте обычно включают минификацию и почему не во всех?
- →Как дать flavor
freeдругую реализацию одного класса?
MiddleТеорияЧастоПочему KSP намного быстрее kapt и чего стоит переход на него?
Почему KSP намного быстрее kapt и чего стоит переход на него?
kapt генерирует Java-заглушку на каждый файл Kotlin, чтобы её прочитал обработчик javac, — лишний проход компиляции. KSP читает разрешённые символы компилятора Kotlin: ни заглушек, ни javac, вдвое быстрее. Цена: процессор KSP должна поставлять каждая библиотека.
Типичные ошибки
- ✗Думать, что ускорение даёт параллелизм, а не отказ от прохода с заглушками и
javac - ✗Считать, что существующий обработчик
kaptзаработает подKSPбез изменений - ✗Полагать, что
KSPправит байткод, а не генерирует исходники, которые затем видит компилятор
Уточняющие вопросы
- →Какие популярные Android-библиотеки уже публикуют процессор
KSP? - →Как измерить, во что обработчик реально обходится вашей сборке?
SeniorДизайнЧастоВаше Android-приложение — один модуль :app на 200 тысяч строк. Правка в одну строку во view model запускает пересборку на одиннадцать минут, и команда уже копит изменения пачками, лишь бы не ждать. Модуль использует kapt для внедрения зависимостей и для локальной базы, а его сборочный скрипт разросся до 400 строк скопированной конфигурации. Руководство выделило время на скорость сборки, но не на переписывание фич. Спроектируйте раскладку модулей, к которой вы перейдёте: как разрежете код, что это даст инкрементальной сборке, что измените в обработке аннотаций и сборочных скриптах и как потом докажете, что стало быстрее.
Ваше Android-приложение — один модуль :app на 200 тысяч строк. Правка в одну строку во view model запускает пересборку на одиннадцать минут, и команда уже копит изменения пачками, лишь бы не ждать. Модуль использует kapt для внедрения зависимостей и для локальной базы, а его сборочный скрипт разросся до 400 строк скопированной конфигурации. Руководство выделило время на скорость сборки, но не на переписывание фич. Спроектируйте раскладку модулей, к которой вы перейдёте: как разрежете код, что это даст инкрементальной сборке, что измените в обработке аннотаций и сборочных скриптах и как потом докажете, что стало быстрее.
Один модуль: любая правка перекомпилирует все 200 тысяч строк. Режьте по фичам и слоям, чтобы правка задевала один небольшой модуль, а швы держите узкими, чтобы изменение ABI не шло каскадом. Переведите kapt на KSP, вынесите 400 строк в convention-плагин и меряйте до и после.
Типичные ошибки
- ✗Резать по слоям, а не по фичам, из-за чего правка одной фичи всё равно задевает все модули
- ✗Выставлять всё через
api, из-за чего изменение ABI по-прежнему каскадом идёт по графу - ✗Объявлять победу без замера инкрементальной сборки до и после
Уточняющие вопросы
- →Каким отчётом Gradle вы покажете, куда реально уходят одиннадцать минут?
- →Как не дать общему модулю
:coreстать новым узким местом?
JuniorТеорияИногдаЧто R8 делает с release-сборкой и почему debug-сборку он не трогает?
Что R8 делает с release-сборкой и почему debug-сборку он не трогает?
R8 за один проход ужимает, оптимизирует и обфусцирует: он обходит граф достижимого от ваших keep-правил, выбрасывает недостижимое, переписывает код, а оставшееся переименовывает в короткие имена. debug его пропускает: он стоит времени сборки и портит стектрейсы.
Типичные ошибки
- ✗Думать, что
R8только сжимает архив, а не удаляет и переименовывает код - ✗Считать, что обфускация шифрует байткод, а не переименовывает символы
- ✗Забывать, что без keep-правила
R8удалит класс, до которого доходит только рефлексия
Уточняющие вопросы
- →Что позволяет сделать mapping-файл с обфусцированным отчётом о падении?
- →Какой код заставляет писать явное keep-правило?
MiddleДизайнИногдаВ вашем Android-проекте 30 модулей, и каждый build.gradle написан на Groovy. Сборочная логика годами копировалась: каждый модуль повторяет один и тот же compileSdk, ту же конфигурацию подписи, ту же обвязку debug и release, а три модуля молча расходятся в том, какой product-flavor вообще существует. Опечатки всплывают, только когда задача реально запускается, и скриптам в команде никто не доверяет настолько, чтобы их рефакторить. Старший разработчик предлагает за спринт перевести все скрипты на Gradle Kotlin DSL. Примите решение и обоснуйте его: что Kotlin DSL реально даёт сборке такого размера, чего он стоит и как вы выстроите миграцию, чтобы сборка всё это время оставалась зелёной.
В вашем Android-проекте 30 модулей, и каждый build.gradle написан на Groovy. Сборочная логика годами копировалась: каждый модуль повторяет один и тот же compileSdk, ту же конфигурацию подписи, ту же обвязку debug и release, а три модуля молча расходятся в том, какой product-flavor вообще существует. Опечатки всплывают, только когда задача реально запускается, и скриптам в команде никто не доверяет настолько, чтобы их рефакторить. Старший разработчик предлагает за спринт перевести все скрипты на Gradle Kotlin DSL. Примите решение и обоснуйте его: что Kotlin DSL реально даёт сборке такого размера, чего он стоит и как вы выстроите миграцию, чтобы сборка всё это время оставалась зелёной.
Kotlin DSL компилируется: типизированные аксессоры, автодополнение, рефакторинг, а опечатка в имени flavor валится на конфигурации, а не посреди задачи. Цена — компиляция скриптов на холодной сборке. Переводите модуль за модулем, повтор логики — в convention-плагин.
Типичные ошибки
- ✗Продавать Kotlin DSL как более быструю сборку, а не как статически типизированную и рефакторимую
- ✗Ждать, что дублирование уберёт сам DSL, а не convention-плагин
- ✗Игнорировать плату за компиляцию скриптов на холодной сборке
Уточняющие вопросы
- →Что именно позволяет разделять convention-плагин, чего не может скопированный блок скрипта?
- →Какие ошибки сборки статическая типизация всё равно не поймает?
MiddleДебаггингИногдаСборка release падает на разборе JSON, а debug работает нормально
Сборка release падает на разборе JSON, а debug работает нормально
Только в release включён isMinifyEnabled, поэтому R8 работает только там. Статически к членам модели никто не обращается, R8 считает их мёртвыми и переименовывает или удаляет; поиск рефлексией падает. Чинится keep-правилом или @Keep, а не отключением минификации.
Типичные ошибки
- ✗Винить JSON или сетевой слой вместо прохода
R8, который есть только вrelease - ✗Чинить установкой
isMinifyEnabled = false, выбрасывая всё сжатие целиком - ✗Считать, что
R8не тронет класс, раз до него доходят рефлексией в рантайме
Уточняющие вопросы
- →Почему
@Keepобычно лучше рукописного keep-правила с подстановкой? - →Что подсказал бы mapping-файл по стектрейсу release-сборки?
JuniorТеорияРедкоЧто такое обработка аннотаций в сборке Kotlin и какие два инструмента её выполняют?
Что такое обработка аннотаций в сборке Kotlin и какие два инструмента её выполняют?
Обработчик аннотаций читает аннотированные объявления во время компиляции и генерирует по ним дополнительные исходники — DAO для Room, компонент Dagger. Kotlin выполняет её через kapt, объявленный устаревшим, или через KSP — актуальный инструмент.
Типичные ошибки
- ✗Думать, что обработка аннотаций идёт в рантайме через рефлексию, а не во время компиляции
- ✗Считать, что обработчик правит ваши существующие классы, а не генерирует новые исходники
- ✗Считать
kaptиKSPодним инструментом под двумя именами
Уточняющие вопросы
- →Почему обработчик аннотаций удлиняет сборку, даже если ничего из его зависимостей не менялось?
- →Какие библиотеки в типичном Android-модуле опираются на сгенерированный код?
JuniorТеорияРедкоЧто делает фронтенд компилятора и что такое K2 в компиляторе Kotlin?
Что делает фронтенд компилятора и что такое K2 в компиляторе Kotlin?
Фронтенд разбирает исходник, разрешает имена и типы и сообщает об ошибках; бэкенд опускает его результат в байткод JVM, нативный код или JS. K2 — переписанный фронтенд Kotlin, стабильный и дефолтный с 2.0, и теперь единственный: в 2.4 K1 удалили.
Типичные ошибки
- ✗Менять роли местами — считать, что код генерирует фронтенд, а типы проверяет бэкенд
- ✗Называть K2 экспериментальным: он стабилен и по умолчанию с 2.0, а K1 в 2.4 удалён
- ✗Думать, что K2 — это бэкенд или плагин Gradle, а не фронтенд компилятора
Уточняющие вопросы
- →Почему один фронтенд на все бэкенды важен для мультиплатформенного проекта?
- →Что приходится менять в плагине компилятора, когда фронтенд заменяют?
MiddleТеорияРедкоЧто на самом деле обесценивает инкрементальную компиляцию в многомодульной сборке Gradle?
Что на самом деле обесценивает инкрементальную компиляцию в многомодульной сборке Gradle?
Правка тела почти ничего не перекомпилирует. Изменение ABI модуля — сигнатуры, public-константы, нового члена — перекомпилирует каждый зависящий модуль. kapt усугубляет: он генерирует заглушки и может вызвать полную перекомпиляцию. Держите поверхность узкой, берите KSP.
Типичные ошибки
- ✗Думать, что любая правка перекомпилирует всю цепочку модулей, а не только изменение ABI
- ✗Упускать, что
kaptзаново генерирует заглушки и может вызвать полную перекомпиляцию модуля - ✗Считать, что правка тела дорога, а смена сигнатуры дешева, — всё ровно наоборот
Уточняющие вопросы
- →Почему изменение
internal-объявления обходится дешевле, чемpublic? - →Как выбор между api- и implementation-зависимостью меняет объём перекомпиляции?
MiddleТеорияРедкоЧто изменил K2, поставив один общий фронтенд перед каждым бэкендом?
Что изменил K2, поставив один общий фронтенд перед каждым бэкендом?
K1 анализировал код раздельными путями под платформы, поэтому общий код мог разрешаться по-разному на JVM и Native. K2 анализирует один раз для всех бэкендов, поэтому вывод типов, smart cast и expect/actual ведут себя везде одинаково. Цена — переписка плагинов компилятора.
Типичные ошибки
- ✗Думать, что K2 унифицировал бэкенды, а не питающий их фронтенд
- ✗Считать, что общий фронтенд снимает нужду в
expect/actualи платформенных source set - ✗Полагать, что плагины компилятора перешли на K2 без правок
Уточняющие вопросы
- →Почему согласованный анализ важнее всего в иерархических source set мультиплатформы?
- →Что ломается в проекте, у чьего плагина компилятора нет сборки под K2?
SeniorДизайнРедкоВы отвечаете за Android-кодовую базу из 40 модулей, всё ещё прибитую к Kotlin 1.9 — то есть компилируемую старым фронтендом K1. Две ваши зависимости перестали публиковать артефакты, работающие на 1.9, нужный вам компилятор Compose выходит только под 2.x, а в одном модуле живёт самописный плагин компилятора, написанный под K1. Команда откладывала апгрейд два года, потому что никто не мог назвать конкретную выгоду, а продакт не готов оплатить квартал чистого рефакторинга. Изложите, как вы переведёте эту базу на актуальный Kotlin: что вы скажете продакту о том, опционален ли этот апгрейд, как выстроите порядок работ по 40 модулям и что сделаете с плагином компилятора и обработчиками аннотаций.
Вы отвечаете за Android-кодовую базу из 40 модулей, всё ещё прибитую к Kotlin 1.9 — то есть компилируемую старым фронтендом K1. Две ваши зависимости перестали публиковать артефакты, работающие на 1.9, нужный вам компилятор Compose выходит только под 2.x, а в одном модуле живёт самописный плагин компилятора, написанный под K1. Команда откладывала апгрейд два года, потому что никто не мог назвать конкретную выгоду, а продакт не готов оплатить квартал чистого рефакторинга. Изложите, как вы переведёте эту базу на актуальный Kotlin: что вы скажете продакту о том, опционален ли этот апгрейд, как выстроите порядок работ по 40 модулям и что сделаете с плагином компилятора и обработчиками аннотаций.
Он не опционален: K2 — дефолт с 2.0, а в 2.4 K1 удалили, так что 1.9 — тупик, уже блокирующий библиотеки. Поднимайте версию, а не код: собирайте модуль за модулем, чините то, что отвергает более строгий фронтенд, портируйте или выбросьте плагин под K1, переведите kapt на KSP.
Типичные ошибки
- ✗Подавать K2 как добровольный выбор — в 2.4 K1 удалили, и 1.9 стал тупиком
- ✗Считать, что смена фронтенда совместима на уровне исходников и не даёт новых ошибок
- ✗Думать, что плагин под K1 продолжит работать или что переход на
KSPсам притянет K2
Уточняющие вопросы
- →Как удержать 40 модулей выпускаемыми, пока мигрировала лишь часть из них?
- →Что вы скажете продакту о выгоде апгрейда на его языке?