С ростом количества устройств с разными процессорными архитектурами (ABI) размер APK‑файла становится одной из главных проблем. Универсальный APK, содержащий .so-библиотеки для всех поддерживаемых ABI, может весить несколько мегабайт больше, чем действительно необходимо пользователю. К счастью, Android Gradle Plugin (AGP) позволяет автоматически разбивать приложение на несколько APK, каждый из которых содержит только нужные нативные библиотеки. В этой статье мы подробно разберём, как правильно сконфигурировать build.gradle для генерации отдельных APK под каждый ABI, а также рассмотрим нюансы версии кода, именования файлов, CI‑интеграции и отладки.
Содержание
- Зачем нужны split‑APK по ABI?
- Базовая конфигурация splits.abi
- Управление versionCode для разных ABI
- Именование и вывод артефактов
- Разделение только для релиз‑сборок
- Интеграция в CI/CD (GitHub Actions, GitLab CI)
- Продвинутые настройки: фильтры, universal APK, exclude ABI
- Типичные ошибки и как их избежать
Зачем нужны split‑APK по ABI?
Основные причины:
- Сокращение размера загрузки. Пользователь получает только те нативные библиотеки, которые соответствуют его устройству.
- Уменьшение объёма трафика. При распространении через Google Play каждый APK будет иметь меньший размер, что положительно сказывается на конверсии.
- Оптимизация кеша. Устройства кэшируют только нужный APK, а не весь набор библиотек.
- Упрощённый анализ. При тестировании можно быстро собрать APK только под нужный ABI.
Пример: если ваш проект использует библиотеку Realm, то в универсальном APK она может занимать почти 5 МБ (только нативные .so‑файлы). При split‑APK размер загрузки может уменьшиться на 3‑4 МБ.
Базовая конфигурация splits.abi
Ниже, минимальная настройка, которая генерирует отдельные APK для armeabi‑v7a, arm64‑v8a, x86 и x86_64. Эта конфигурация подходит для большинства проектов.
android {
// ... остальные настройки (compileSdkVersion, defaultConfig, etc.)
// 1️⃣ Включаем split‑APK по ABI
splits {
abi {
// Включаем генерацию отдельных APK
enable true
// Сбрасываем список ABI по умолчанию (все поддерживаемые)
reset
// Указываем, какие именно ABI хотим собрать
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
// Если нужен универсальный APK, задаём true; иначе false
universalApk false
}
}
// 2️⃣ Настраиваем versionCode (см. раздел ниже)
defaultConfig {
// …
versionCode 100
versionName "1.0.0"
}
// 3️⃣ При необходимости задаём отдельные подписи для релиз‑сборок
signingConfigs {
release {
// путь к keystore, пароли и т.д.
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
// Debug‑сборки обычно без split‑APK (см. раздел 4)
}
}
}
После выполнения команды ./gradlew assembleRelease в каталоге app/build/outputs/apk/release/ появятся файлы:
app-arm64-v8a-release.apkapp-armeabi-v7a-release.apkapp-x86-release.apkapp-x86_64-release.apk
Управление versionCode для разных ABI
Google Play требует уникального versionCode для каждого APK в наборе Multi‑APK. Самый простой способ – добавить к базовому коду небольшое смещение, зависящее от ABI.
Пример функции‑расширения
def abiVersionCodeOffset = [
"armeabi-v7a": 1,
"arm64-v8a" : 2,
"x86" : 3,
"x86_64" : 4
]
// Функция, которая возвращает текущий ABI из имени задачи
def getAbiFromTask {
def taskName = gradle.startParameter.taskNames.find { it.contains("assemble") }
if (taskName == null) return null
// Пример: assembleArm64V8aRelease → arm64-v8a
return abiVersionCodeOffset.keySet.find { taskName.toLowerCase.contains(it.replace("-", "")) }
}
// Переопределяем versionCode только для релиз‑сборок с split‑APK
android.applicationVariants.all { variant ->
if (variant.buildType.name == "release" && variant.productFlavors.isEmpty) {
def abi = getAbiFromTask
if (abi != null) {
variant.outputs.each { output ->
// Базовый versionCode из defaultConfig + смещение
output.versionCodeOverride = defaultConfig.versionCode + abiVersionCodeOffset[abi]
}
}
}
}
Что происходит?
- Базовый
versionCodeзадаётся вdefaultConfig(например,100). - Для каждого ABI к нему прибавляется своё смещение (1‑4). В итоге получаем коды 101‑104.
- Google Play воспринимает их как отдельные версии, но пользователь получит только один APK, соответствующий его устройству.
Если вы используете versionCode в виде даты (20230802), то к нему также можно добавить смещение, например 20230802 * 10 + offset, чтобы сохранить порядок версий.
Именование и вывод артефактов
По умолчанию Gradle генерирует имена вида app-. При необходимости можно задать собственный шаблон, используя outputFileName:
android.applicationVariants.all { variant ->
if (variant.buildType.name == "release") {
variant.outputs.each { output ->
def abi = output.getFilter(com.android.build.OutputFile.ABI)
if (abi != null) {
// Пример: myapp_v1.0.0_arm64-v8a.apk
def newName = "myapp_v${defaultConfig.versionName}_${abi}.apk"
outputFileName = newName
}
}
}
}
Это удобно, если вы публикуете APK вручную (например, в корпоративный магазин) и хотите, чтобы имя сразу отражало версию и целевую архитектуру.
Разделение только для релиз‑сборок
Для отладки обычно удобно иметь один «fat‑apk», содержащий все ABI, чтобы не переключать устройство. Чтобы отключить split‑APK в debug‑режиме, добавьте проверку на текущий тип сборки:
android {
splits {
abi {
// Включаем только если собираем релиз
enable project.gradle.startParameter.taskNames.any { it.contains("Release") }
reset
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
universalApk false
}
}
}
Или используйте системное свойство, которое удобно задавать из CI:
def splitApks = project.hasProperty('splitApks') && project.property('splitApks') == 'true'
android {
splits {
abi {
enable splitApks
reset
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
universalApk false
}
}
}
Тогда запуск ./gradlew assembleDebug создаст один универсальный APK, а ./gradlew assembleRelease -PsplitApks=true – набор split‑APK.
Интеграция в CI/CD
Ниже пример конфигурации GitHub Actions, который собирает отдельные APK для каждой архитектуры и загружает их в артефакты:
name: Build split APKs
on:
push:
branches: [ main ]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
env:
GRADLE_OPTS: "-Dorg.gradle.jvmargs='-Xmx3g -Dfile.encoding=UTF-8'"
steps:
‒ name: Checkout repository
uses: actions/checkout@v4
‒ name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
⎯ name: Cache Gradle packages
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('/.gradle', '/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
⎯ name: Build release split APKs
run: ./gradlew clean assembleRelease -PsplitApks=true
⎯ name: Upload APKs as artifacts
uses: actions/upload-artifact@v4
with:
name: split-apks
path: app/build/outputs/apk/release/*.apk
Для GitLab CI процесс аналогичен – достаточно добавить переменную splitApks=true в скрипт сборки;
Продвинутые настройки
Добавление universal APK
Если вам всё же нужен «универсальный» APK (например, для тестов), установите universalApk true. При этом будет создан дополнительный файл app-universal-release.apk, содержащий все ABI.
Исключение конкретных ABI
Иногда требуется собрать APK только для arm64‑v8a и x86_64, исключив остальные. Делается так:
splits {
abi {
enable true
reset
include "arm64-v8a", "x86_64"
universalApk false
}
}
Комбинация с splits.density
Можно одновременно разбивать по плотности экранов и по ABI. В этом случае будет сгенерировано ABI × density комбинаций. Чтобы избежать экспоненциального роста количества APK, рекомендуется использовать только один из вариантов (обычно ABI) или ограничить набор плотностей.
splits {
density {
enable true
reset
include "mdpi", "hdpi", "xhdpi", "xxhdpi"
universalApk false
}
abi {
enable true
reset
include "armeabi-v7a", "arm64-v8a"
}
}
Работа с Android App Bundle (AAB)
Если вы публикуете приложение в виде AAB, то Google Play автоматически делает split‑APK на стороне сервера. Тем не менее, иногда хочется протестировать локально отдельные ABI‑пакеты. Для этого можно использовать bundletool:
./gradlew bundleRelease
bundletool build-apks
--bundle=app-release.aab
--output=app.apks
--mode=universal
--device-spec=device-spec.json
unzip -p app.apks universal.apk > app-arm64-v8a.apk
Типичные ошибки и как их избежать
- Неправильный
versionCode→ конфликт в Play Console. Убедитесь, что каждый ABI имеет уникальный код. Лучше использовать автоматическое смещение, как показано в разделе 3. - Отсутствие нативных библиотек в
jniLibs→ split‑APK не создаёт файлы. Проверьте, что.so‑файлы находятся вsrc/main/jniLibs/<abi>или собираются через CMake/NDK. - Слишком много комбинаций (ABI × density) → превышение лимита 100 APK. Google Play ограничивает количество APK в наборе. Оставляйте только необходимые варианты.
- Отладка с split‑APK. При запуске из Android Studio IDE может выбрать неверный APK. Отключайте split‑APK для
debug(см. раздел 5) или используйтеRun > Edit Configurations > Deploy APK from app bundle. - Кеширование в CI. При изменении списка ABI необходимо сбрасывать кеш Gradle, иначе могут остаться старые артефакты.
Разделение APK по ABI – простой и эффективный способ уменьшить размер загрузки, ускорить установку и сэкономить трафик пользователей. Основные шаги:
- Включить
splits.abiвbuild.gradle. - Указать нужные ABI через
includeи отключитьuniversalApk, если он не нужен; - Обеспечить уникальный
versionCodeдля каждой комбинации (см. функцию‑смещение). - Настроить именование файлов, если требуется специфический шаблон.
- Отключить split‑APK для debug‑сборок, чтобы упростить отладку.
- Интегрировать процесс в CI/CD, используя параметры
-PsplitApks=trueдля гибкого управления.
Следуя этим рекомендациям, вы сможете легко поддерживать несколько оптимизированных APK, улучшить пользовательский опыт и сократить расходы на доставку приложений.
Автор: ваш виртуальный помощник. Дата: 17:28:10