
Привет, Хабр. На связи Илья Новиков, технический директор команды разработчиков «Исходный код».
Сегодня я хочу разобрать инструмент, который мы все используем десятки раз в день, но часто по-прежнему воспринимаем как «черный ящик».
Без этого мы бы до сих пор собирали проекты вручную, писали бы скрипты Bash для каждой команды и каждой машины, и боролись бы со старой проблемой, когда проект собирается у одного разработчика, но не собирается у другого.
Gradle предоставляет нам единый источник достоверной информации, возможность воспроизводимых сборок и способ масштабирования одного и того же подхода к сборке от небольшого проекта до крупного многомодульного монорепозитория.
Давайте заглянем под капот, чтобы настройка сборки перестала быть проблемой.
Что такое Gradle и как он работает?
Gradle — это система сборки. Она автоматизирует компиляцию, тестирование, упаковку и публикацию.
В основе этой системы лежат три ключевые идеи.
DAG (Directed Acyclic Graph — ориентированный ациклический граф). Перед началом сборки Gradle создает граф всех задач и их зависимостей. Именно так он понимает точный порядок выполнения и то, что можно распараллелить.
Поэтапная сборка. Gradle не должен повторять работу, которая уже была выполнена.
Кэш. Он может быть локальным или удаленным.
Основная часть работы строится вокруг трех концепций: Проект, Задача и Плагин.
Project — это модуль сборки. Он может быть корневым проектом или вложенным. Каждый проект может иметь свой собственный файл build.gradle или build.gradle.kts.
Task — это наименьшая единица работы. Например, компиляция Kotlin, тестирование или сборка Debug.
Plugin — это код, который расширяет функциональность Gradle и изменяет возможности сборки.

Основные концепции Gradle
Project
Один файл build.gradle (.kts) означает один проект.
Многомодульный формат означает наличие нескольких проектов.
Task
Задача — это наименьшая единица работы.
Примеры:
compileKotlin
test
assembleDebug
Plugin
Плагин — это код, расширяющий функциональность Gradle.
Плагин:
Регистрирует задачи.
Добавляет DSL.
Подключается к жизненному циклу сборки.

Плагины добавляют задачи, DSL и логику сборки. Они могут быть внешними или локальными.
Локальный плагин, запускаемый через buildSrc, работает, но имеет ряд неприятных особенностей.
Всегда конфигурируется.
Замедляет синхронизацию.
Плохо масштабируется.
Характерен для всего проекта.

Локальный плагин, созданный с помощью составной сборки, работает иначе.
Gradle может объединять несколько независимых сборок в одну. В корневом файле settings.gradle это связано следующим образом:
includeBuild ( "build-logic" )



На рисунках представлено несколько примеров плагинов.
com.google.devtools.ksp
Обработка символов в Kotlin.
Это замена kapt, обычно более быстрая и с меньшим количеством сложностей, связанных с обработкой аннотаций.
Kotlin
Плагин Kotlin подключает Kotlin к сборке, настраивает компиляцию и связывает Kotlin с Android при необходимости.
com.android.application
Это плагин Android Gradle.
Это дает нам:
android {} DSLbuild variantsAAPTDEXpackaging
Это также, как правило, самый ресурсоемкий плагин в проекте Android.
Composite build
Композитная сборка важна для общей логики сборки. Вместо того чтобы помещать все в buildSrc и заставлять весь проект платить за это при каждой синхронизации, мы можем перенести логику сборки в отдельную включаемую сборку.
Это делает процесс сборки более чистым и масштабируемым.
Dependency
Зависимость — это внешняя библиотека или другой модуль в том же проекте.
Жизненный цикл сборки — это часть, которая находится «под капотом»
Понимая эти три этапа, вы уже можете решить большинство проблем, связанных со скоростью сборки.
Initialization
Gradle считывает файл settings.gradle и понимает, какие модули участвуют в сборке.
Configuration
Gradle выполняет скрипты build.gradle, создает объекты задач и строит граф задач.
Execution
Gradle выполняет только необходимые задачи и делает это в правильном порядке. Важно то, что настройка происходит до выполнения. Именно здесь многие команды случайно замедляют сборку. Сборка еще не начала выполнять полезную работу, но Gradle уже тратит время на скрипты, плагины, объекты задач и создание модели проекта.
Почему этап Configuration — главная проблема?
Самая распространенная ошибка — это размещение сложной логики в конфигурации.
На данном этапе не следует читать файлы, обращаться к сети, запускать shell-скрипты командной оболочки или выполнять сложные вычисления.
Результатом использования подобного кода всегда является одно и то же: медленная синхронизация с IDE, громоздкая сборка и команда, которая начинает ненавидеть Gradle.


На рисунках примеры плохого кода с val config = file(…).readText() и более удачный подход с использованием ленивого API.
Правильный подход прост. Конфигурация должна быть декларативной.
Используйте отложенный API. Забудьте о tasks.create в тех местах, где задача не должна существовать немедленно. Используйте tasks.register.
При использовании tasks.create задача создается во время настройки.
С помощью tasks.register задача регистрируется отложено и создается только тогда, когда она действительно необходима.
Это делает настройку дешевле. В небольших проектах разница может показаться незначительной, но в крупном проекте Android разница очень заметна, особенно во время синхронизации с Android Studio.
Именно поэтому логика сборки не должна вести себя как логика приложения. Скрипт сборки — не место для предварительных расчетов всего. Он должен описывать возможные сценарии и позволять Gradle решать, что действительно необходимо сделать для текущей команды.
Поэтапная сборка: отслеживание актуального состояния
В процессе выполнения инкрементальной задачи происходят только изменения.
Gradle сравнивает три вещи.
Inputs (входные данные): файлы, параметры и значения, используемые в задаче.
Outputs (результаты): сгенерированные файлы и артефакты.
Implementation (реализация): путь к classpath задачи, код плагина и зависимости плагина.
Если ничего не изменилось, задача получает статус UP-TO-DATE или FROM-CACHE и не запускается.
Вы можете проверить это следующим образом:
./gradlew assembleDebug --info
Если почти все данные в выходных данных выполняются, значит, инкрементальная сборка где-то нарушена.
Это важный сигнал. Gradle не сможет определить, можно ли безопасно пропустить задачу, если в ней четко не описаны входные и выходные данные.
Например, задача может казаться безобидной, но если она считывает файл, не указывая этот файл в качестве входных данных, Gradle не сможет построить на его основе стабильную модель зависимостей.
То же самое относится и к выходным данным. Если задача записывает данные в неожиданное место, непредсказуемо изменяет файлы или выдает разные результаты для одних и тех же входных данных, кэширование и инкрементальная сборка становятся ненадежными.
В этом отношении Gradle проявляет строгость, но эта строгость полезна. Как только входные и выходные данные, а также реализация описаны правильно, сборка перестает повторять работу без видимых причин.
Специфика сборок Android и причины замедления работы
Плагин Android Gradle, или AGP, представляет собой большую надстройку над Gradle.
Он генерирует сотни задач и управляет ресурсами, DEX, вариантами сборки, типами сборки, вариантами сборки, упаковкой и многими другими этапами, специфичными для Android (build types, flavors).
Именно поэтому сборки для Android часто кажутся более ресурсоемкими, чем обычные сборки для JVM.
Синхронизация против сборки
Синхронизация в Android Studio — это не то же самое, что сборка.
Синхронизация — это настройка и создание модели проекта IDE. Android Studio необходимо понимать модули, зависимости, варианты, наборы исходных файлов, сгенерированные исходные файлы, задачи и плагины.
Это дорогостоящая операция.
В крупных проектах синхронизация может занимать больше времени, чем сама сборка. Именно поэтому плохая логика конфигурации так сильно вредит. Разработчик может всего лишь открыть проект или изменить небольшую настройку, но IDE все равно платит за это.
Kapt как тихий убийца
Обработка аннотаций с помощью kapt часто значительно снижает скорость сборки.
Переход на KSP обычно является правильным решением. KSP работает быстрее, чем kapt, зачастую в два раза быстрее или даже больше. В крупных проектах это может сэкономить минуты на каждой сборке.
Преимущество заключается не только в скорости. KSP также сокращает объем скрытой работы в процессе сборки. Уменьшается количество генерируемой логики Java-мостика, сокращается число странных промежуточных шагов и уменьшается количество случаев, когда сборка ведет себя как неудобный набор кода.
Ресурсы обходятся дороже, чем код
Изменение одного XML-файла может обойтись дороже, чем изменение одного файла Kotlin.
Причина кроется в протоколе AAPT2 и цепочке задач обработки ресурсов Android. Небольшое изменение ресурса может запустить множество связанных задач.
Вот почему сборка Android может казаться непоследовательной. Небольшая корректировка кода приводит к быстрой сборке. Небольшая корректировка XML внезапно затрагивает гораздо большую часть конвейера.
Золотые правила, или как спокойно спать
Вот контрольный список, который я использую, когда нам нужно навести порядок в процессе сборки.
Всегда используйте Gradle Wrapper, gradlew, он гарантирует правильную версию Gradle для всех членов команды и в системе непрерывной интеграции (CI).
Обновите корректно. Сначала AGP, затем Gradle, потому что они тесно связаны.
По возможности используйте только отложенный API. Регистрируйте задачи через tasks.register и используйте Provider.
Ограничьте операции ввода-вывода в конфигурации. Любое чтение файлов, запросы или внешние вызовы должны выполняться внутри действий задачи, таких как doFirst или doLast, или через API поставщика.
Включите кэширование сборки. Оно кэширует результаты выполнения детерминированных задач.
Сократите количество плагинов. Каждый дополнительный плагин может добавить десятки задач и нарушить работу кэша.
Типичная базовая конфигурация выглядит следующим образом:
org.gradle.jvmargs=-Xmx4g
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
Оптимизация JVM и параметры Gradle.
Эти флаги помогают только в том случае, если сборка написана таким образом, что Gradle может ее оптимизировать.
Параметр org.gradle.parallel=true помогает, когда модули и задачи действительно могут выполняться параллельно.
Параметр org.gradle.caching=true помогает, когда задачи детерминированы и их входные и выходные данные указаны корректно.
Параметр org.gradle.configuration-cache=true помогает в случаях, когда конфигурацию можно безопасно использовать повторно.
Параметр org.gradle.jvmargs=-Xmx4g предоставляет Gradle достаточно памяти для работы без постоянной борьбы с нехваткой памяти в куче.
Не существует волшебного флага, который исправил бы запутанную логику сборки.
Если проект создает задачи в режиме немедленного выполнения, считывает файлы во время настройки, добавляет слишком много плагинов или поддерживает в рабочем состоянии старые конвейеры обработки аннотаций, сборка все равно будет медленной.
Заключение
Gradle оказался для нас мощным инструментом, который не прощает небрежной настройки.
Большинство проблем со скоростью сборки Android-приложений возникли из-за масштаба проекта и особенностей конфигурации сборки.
Обычная схема: проект растет, модули множатся, плагины накапливаются, конфигурация становится все более громоздкой, kapt продолжает работать, ресурсы запускают длинные цепочки задач, и в какой-то момент каждая синхронизация начинает восприниматься как наказание.
Это решение также практично.
Прекратите выносить сложную логику в конфигурацию.
Перенесите создание задач в ленивый API. Опишите входные и выходные данные.
Используйте Provider.
Включите кэширование.
Сократите количество плагинов.
Переходите от kapt к KSP там, где это имеет смысл.
Сохраняйте логику сборки отдельной и масштабируемой, особенно с помощью составной сборки, вместо того, чтобы выкладывать все в buildSrc.
Мы внедрили эти правила в свою работу, и сборка стала заметно стабильнее.
Главный вывод для меня таков: Gradle — не враг. Враг — это небрежная настройка сборки.
Когда build.gradle превращается в место, где навсегда хранятся все плагины, скрипты, прочитанные файлы и быстрые обходные пути, сборка начинает разваливаться. Когда же к сборке относятся как к инженерной задаче, у Gradle достаточно инструментов, чтобы сделать ее быстрой и предсказуемой.
Начните с файлов build.gradle.
Проверьте, что происходит во время настройки.
Посмотрите, что всегда выполняется.
Найдите задачи, которые не обновляются.
Удалите ненужные плагины.
Замените немедленное создание задач на отложенную регистрацию.
Обычно именно там и скрывается настоящая скорость. Поделитесь в комментариях, что больше всего замедляло ваши сборки и как вы это исправили.