- PVSM.RU - https://www.pvsm.ru -

Привет! Летом я выступал на Summer Droid Meetup с докладом про сборку Android-приложения. Видеоверсию можно найти здесь: habr.com/ru/company/funcorp/blog/462825 [1]. А для тех, кто больше любит читать, я как раз и написал эту статью.
Речь пойдёт о том, что же это такое — Android-приложение. Мы соберём разными способами Hello, world!: начнём с консоли и посмотрим, что вообще происходит под капотом систем сборки, потом вернёмся немного в прошлое, вспомним про Maven и изучим современные решения Bazel и Buck. И, наконец, всё это сравним.
Мы задумались о возможной смене системы сборки, когда начинали новый проект. Нам казалось, что это неплохая возможность поискать какие-нибудь альтернативы Gradle. Тем более, что делать это проще на старте, чем переводить существующий проект. К этому шагу нас подтолкнули следующие недостатки Gradle:
Прежде всего вспомним, из чего состоит Android-приложение: скомпилированного кода, ресурсов и AndroidManifest.xml.

Исходники находятся в файле classes.dex (файлов может быть несколько, в зависимости от величины приложения) в специальном dex-формате, с которым умеет работать виртуальная машина Android. Нынче это ART, на более старых девайсах — Dalvik. Помимо этого можно встретить папку lib, где по подпапкам разложены нативные исходники. Они будут носить названия в зависимости от целевой архитектуры процессора, например x86, arm и т.д. Если вы используете exoplayer, то lib у вас наверняка присутствует. И папка aidl, которая содержит в себе интерфейсы межпроцессного взаимодействия. Они пригодятся, если нужно обратиться к сервису, запущенному в другом процессе. Такие интерфейсы используются и в самом Android, и внутри GooglePlayServices.
Различные некомпилируемые ресурсы вроде картинок лежат в папке res. Все компилируемые ресурсы, такие как стили, строки и т.д., сливаются в файл resource.arsc. В папку assets, как правило, складывают всё, что не укладывается в ресурсы, например кастомные шрифты.
Кроме всего этого, в APK содержится AndroidManifest.xml. В нём мы описываем различные компоненты приложения, такие как Activity, Service, разные разрешения и т.д. Он лежит в бинарном виде, и чтобы заглянуть внутрь, его надо будет сперва сконвертировать в человекочитаемый файл.
Теперь, когда мы знаем, из чего состоит приложение, можем попробовать собрать Hello, world! из консоли, используя инструменты, которые предоставляет Android SDK. Это довольно важный этап для понимания того, как работают системы сборки, потому что все они в той или иной мере опираются на эти утилиты. Так как проект написан на Kotlin, нам потребуется его компилятор для командной строки. Его несложно загрузить отдельно.
Сборку приложения можно поделить на следующие этапы:
function preparedir() {
rm -r -f $1
mkdir $1
}
PROJ="src/main"
LIBS="libs"
LIBS_OUT_DIR="$LIBS/out"
BUILD_TOOLS="$ANDROID_HOME/build-tools/28.0.3"
ANDROID_JAR="$ANDROID_HOME/platforms/android-28/android.jar"
DEBUG_KEYSTORE="$(echo ~)/.android/debug.keystore"
GEN_DIR="build/generated"
KOTLIN_OUT_DIR="$GEN_DIR/kotlin"
DEX_OUT_DIR="$GEN_DIR/dex"
OUT_DIR="out"
libs_res=""
libs_classes=""
preparedir $LIBS_OUT_DIR
aars=$(ls -p $LIBS | grep -v /)
for filename in $aars;
do
DESTINATION=$LIBS_OUT_DIR/${filename%.*}
echo "unpacking $filename into $DESTINATION"
unzip -o -q $LIBS/$filename -d $DESTINATION
libs_res="$libs_res -S $DESTINATION/res"
libs_classes="$libs_classes:$DESTINATION/classes.jar"
done
preparedir $GEN_DIR
$BUILD_TOOLS/aapt package -f -m
-J $GEN_DIR
-M $PROJ/AndroidManifest.xml
-S $PROJ/res
$libs_res
-I $ANDROID_JAR --auto-add-overlay
preparedir $KOTLIN_OUT_DIR
compiledKotlin=$KOTLIN_OUT_DIR/compiled.jar
kotlinc $PROJ/java $GEN_DIR -include-runtime
-cp "$ANDROID_JAR$libs_classes"
-d $compiledKotlin
preparedir $DEX_OUT_DIR
dex=$DEX_OUT_DIR/classes.dex
$BUILD_TOOLS/dx --dex --output=$dex $compiledKotlin
preparedir $OUT_DIR
unaligned_apk=$OUT_DIR/unaligned.apk
$BUILD_TOOLS/aapt package -f -m
-F $unaligned_apk
-M $PROJ/AndroidManifest.xml
-S $PROJ/res
$libs_res
-I $ANDROID_JAR --auto-add-overlay
cp $dex .
$BUILD_TOOLS/aapt add $unaligned_apk classes.dex
rm classes.dex
aligned_apk=$OUT_DIR/aligned.apk
$BUILD_TOOLS/zipalign -f 4 $unaligned_apk $aligned_apk
$BUILD_TOOLS/apksigner sign --ks $DEBUG_KEYSTORE $aligned_apk
По цифрам получается, что чистая сборка занимает 7 секунд, и инкрементальная от неё не отстаёт, потому что мы ничего не кешируем и каждый раз пересобираем всё заново.
Он был разработан ребятами из Apache Software Foundation для сборки Java-проектов. Билд-конфиги для него описываются на языке XML. Ранние ревизии Maven собирались Ant, а сейчас они перешли на последний стабильный релиз.
Плюсы Maven:
Минусы Maven:
Для сборки мы должны создать pom.xml, который содержит описание нашего проекта. В заголовке указываем базовые сведения о собираемом артефакте, а так же версию Kotlin.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>myapplication</artifactId>
<version>1.0.0</version>
<packaging>apk</packaging>
<name>My Application</name>
<properties>
<kotlin.version>1.3.41</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.google.android</groupId>
<artifactId>android</artifactId>
<version>4.1.1.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.simpligility.maven.plugins</groupId>
<artifactId>android-maven-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<sdk>
<platform>28</platform>
<buildTools>28.0.3</buildTools>
</sdk>
<failOnNonStandardStructure>false</failOnNonStandardStructure>
</configuration>
</plugin>
</plugins>
</build>
</project>
По цифрам всё выходит не слишком радужно. Чистая сборка занимает порядка 12 секунд, тогда как инкрементальная — 10. Это говорит о том, что Maven как-то плохо переиспользует артефакты предыдущих сборок, либо, что на мой взгляд более вероятно, плагин для сборки Android-проекта мешает ему это делать
Используют сейчас всё это, я думаю, прежде всего создатели плагина — ребята из simpligility. Больше достоверных сведений об этом вопросе найти не удалось.
Bazel изобрели инженеры в недрах Google для сборки своих проектов и относительно недавно перевели его в open source. Для описания билд-конфигов используется питоноподобный Skylark или Starlark, оба названия имеют место быть. Собирается с использованием своего же последнего стабильного релиза.
Плюсы Bazel:
Минусы Bazel:
Концептуально базовый конфиг Bazel состоит из WORKSPACE, где мы описываем всякие глобальные вещи для проекта, и BUILD, который содержит непосредственно таргеты для сборки.
Опишем WORKSPACE. Так как у нас Android-проект, то первое, что мы конфигурируем, — это Android SDK. Также тут импортируется правило для выгрузки конфигов. Потом, так как проект написан на Kotlin, мы должны указать правила для него. Тут мы делаем это, ссылаясь на конкретную ревизию прямо из git-репозитория.
android_sdk_repository(
name = "androidsdk",
api_level = 28,
build_tools_version = "28.0.3"
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
#
# KOTLIN RULES
#
RULES_KOTLIN_VERSION = "990fcc53689c8b58b3229c7f628f843a60cb9f5c"
http_archive(
name = "io_bazel_rules_kotlin",
url = "https://github.com/bazelbuild/rules_kotlin/archive/%s.zip" % RULES_KOTLIN_VERSION,
strip_prefix = "rules_kotlin-%s" % RULES_KOTLIN_VERSION
)
load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kotlin_repositories", "kt_register_toolchains")
kotlin_repositories()
kt_register_toolchains()
Теперь приступим к BUILD.
Сперва импортируем правило для сборки Kotlin и описываем то, что хотим собрать. В нашем случае это Android-приложение, поэтому используем android_binary, где задаём манифест, минимальный SDK и т.д. Наше приложение будет зависеть от исходников, поэтому упоминаем их в deps и переходим к тому, что они собой представляют и где их найти. Код также будет зависеть от ресурсов и библиотеки appcompat. Для ресурсов используем обычный таргет для сборки андроидных исходников, но задаём ему только ресурсы без java-классов. И описываем пару правил, которые импортируют сторонние библиотеки. Тут также упоминается appcompat_core, от которой зависит appcompat.
load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
android_binary(
name = "app",
custom_package = "com.example.myapplication",
manifest = "src/main/AndroidManifest.xml",
manifest_values = {
"minSdkVersion": "15",
},
deps = [
":lib",
],
)
kt_android_library(
name = "lib",
srcs = glob(["src/main/java/**/*"]),
deps = [
":res",
":appcompat",
],
)
android_library(
name = "res",
resource_files = glob(["src/main/res/**/*"]),
manifest = "src/main/AndroidManifest.xml",
custom_package = "com.example.myapplication",
)
aar_import(
name = "appcompat",
aar = "libs/appcompat.aar",
deps = [
":appcompat_core",
]
)
aar_import(
name = "appcompat_core",
aar = "libs/core.aar",
)
По цифрам для такого маленького проекта всё выглядит печально. Больше половины минуты на чистую сборку Hello, world! — очень много. Время инкрементальной сборки также далеко от совершенства.
Bazel используют его создатели (Google) для каких-то своих проектов, в том числе серверных, а также Dropbox и Huawei, которые собирают им мобильные приложения. И небезызвестный Dagger 2 также собирается Bazel.
Его придумали перебежчики из Google в Facebook. Для описания конфигов раньше он использовал Python, а потом мигрировал на упоминавшийся сегодня Skylark. Собирается же он, внезапно, с помощью системы Ant.
Плюсы Buck:
Минусы Buck:
Итак, как выглядит конфиг сборки Hello, world! посредством Buck? Тут мы описываем один файл конфигурации, где указываем, что хотим собирать Android-проект, который будет подписан дебажным ключом. Приложение аналогичным образом будет зависеть от исходников — lib в массиве deps. Дальше идёт таргет с настройками подписи. Я использую дебажный ключ, который идёт в комплекте с Android SDK. Сразу за ним следует таргет, который соберёт нам исходники Kotlin. Аналогично Bazel, он зависит от ресурсов и библиотек совместимости.
Описываем их. Для ресурсов в Buck есть отдельный таргет, поэтому велосипеды не пригодятся. Следом идут правила для скачанных сторонних библиотек.
android_binary(
name = 'app',
manifest = 'src/main/AndroidManifest.xml',
manifest_entries = {
'min_sdk_version': 15,
},
keystore = ':debug_keystore',
deps = [
':lib',
],
)
keystore(
name = 'debug_keystore',
store = 'debug.keystore',
properties = 'debug.keystore.properties',
)
android_library(
name = 'lib',
srcs = glob(['src/main/java/*.kt']),
deps = [
':res',
':compat',
':compat_core',
],
language = 'kotlin',
)
android_resource(
name = 'res',
res = "src/main/res",
package = 'com.example.myapplication',
)
android_prebuilt_aar(
name = 'compat',
aar = "libs/appcompat.aar",
)
android_prebuilt_aar(
name = 'compat_core',
aar = "libs/core.aar",
)
Собирается всё это дело очень резво. Чистая сборка занимает немногим более 7 секунд, тогда как инкрементальная — совершенно незаметные 200 миллисекунд. Я думаю, это очень хороший результат.
Так делают в Facebook. Кроме своего флагманского приложения, они собирают им Facebook Messenger. И Uber, которые сделали плагин для Gradle и Airbnb с Lyft.
Теперь, когда мы поговорили про каждую систему сборки, можно сравнить их между собой на примере Hello, world! Консольная сборка радует своей стабильностью. Время выполнения скрипта из терминала можно считать эталонным для сборки чистых билдов, потому что сторонние затраты на парсинг скриптов тут минимальны. Явным аутсайдером я бы в данном случае назвал Maven за чрезвычайно незначительное убыстрение инкрементальной сборки. Bazel очень долго парсит конфиги и инициализируется: есть мысль, что он кеширует как-то результаты инициализации, потому что инкрементальная сборка у него проходит существенно быстрее чистой. Buck — бесспорный лидер это подборки. Очень быстрая как чистая, так и инкрементальные сборка.

Сравним теперь все за и против. Не буду включать Maven в сравнение, потому что он явно проигрывает Gradle и уже практически не используется на рынке. Buck и Bazel я объединю, потому что они обладают примерно одинаковыми достоинствами и недостатками.
Итак, про Gradle:
Про Buck/Bazel:
Не забудем и про минусы.
Gradle за свою простоту расплачивается тем, что он медленный и неэффективный.
Buck/Bazel же, напротив, из-за своей скорости страдают от необходимости подробнее описывать в конфигах процесс сборки. Ну и так как появились на рынке они относительно недавно, то документации и разных шпаргалок немного.
Возможно, у вас возник вопрос, как мы собираем iFunny. Так же, как и многие — используя Gradle. И на то есть причины:
Если у вас проект собирается больше 45 минут и в команде Android-разработки человек 20, то есть смысл задуматься о смене системы сборки. Если вы со своим другом пилите стартап, то пользуйтесь Gradle и отбросьте эти мысли.

Буду рад обсудить перспективы альтернатив Gradle в комментариях!
Ссылка на проект [2]
Автор: Антон
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android-development/331988
Ссылки в тексте:
[1] habr.com/ru/company/funcorp/blog/462825: https://habr.com/ru/company/funcorp/blog/462825/
[2] Ссылка на проект: https://github.com/FlashLight13/BuildSystems
[3] Источник: https://habr.com/ru/post/469771/?utm_source=habrahabr&utm_medium=rss&utm_campaign=469771
Нажмите здесь для печати.