В 2025 году Google Play начал постепенно вводить новые требования для приложений, ориентированных на Android 15+ (targetSdk 35). Одно из самых неприятных — совместимость с устройствами, использующими 16 KB страницы памяти (16KB page size).
На практике это выливается в ситуацию:
- приложение собирается нормально,
- локально работает,
- но при публикации в Google Play появляется ошибка, что приложение не поддерживает 16KB page size,
- и виноваты… нативные библиотеки
.so, которые лежат внутри APK/AAB.
В этой статье — как быстро проверить сборку до загрузки в Play, найти проблемные .so и автоматизировать проверку через Gradle.
Что такое 16KB page size и почему это ломает публикацию
Раньше подавляющее большинство Android-устройств работали с размером страницы памяти 4 KB. Но на части новых устройств (особенно на Android 15 и новее) используется 16 KB.
Если ваше приложение содержит нативный код (NDK, движки, SDK, ML, игры, и т.д.), то в APK/AAB попадают .so файлы.
И вот важный момент:
Совместимость определяется не “zipalign”, не настройками Gradle и не версией targetSdk, а тем, как собраны ELF-библиотеки.
У .so есть параметр выравнивания сегментов загрузки (LOAD Align), который можно увидеть через readelf.
Если .so собрана с Align = 0x1000 (4 KB) — она может быть несовместима с 16 KB page size и Google Play заблокирует публикацию.
Какие ABI реально важны
Требование 16 KB page size актуально в первую очередь для 64-битных ABI:
arm64-v8ax86_64
А вот 32-битные ABI (armeabi-v7a, x86) чаще всего не критичны (и реальные устройства на них почти всегда 4 KB). Но в зависимости от конкретной проверки Play Console, иногда ошибки могут показываться и по 32-битным библиотекам.
В своей проверке я в итоге оставил только 64-битные ABI — так результат соответствует реальной проблеме.
Почему “вчера опубликовалось, а сегодня не проходит”
Очень частый кейс:
- старый релиз уже опубликован,
- вы делаете небольшой фикс,
- собираете обновление,
- и вдруг Google Play начинает ругаться.
Это происходит потому что:
- Play постепенно усиливает проверки,
- или вы обновили targetSdk / AGP,
- или обновились зависимости,
- или просто новая сборка подтянула другие
.so.
Особенно часто такое случается с:
- игровыми движками (libGDX, Unity, Godot),
- ML/vision SDK,
- рекламными SDK,
- “готовыми” AAR/JAR, внутри которых лежат
.so.
Как понять, какие .so реально проблемные
Для проверки нужны два инструмента:
Вариант A: readelf (Linux/macOS)
На Linux он обычно есть сразу. На macOS можно поставить через brew.
Вариант B (универсальный): llvm-readelf из Android NDK
Это самый удобный вариант, потому что:
- работает на Windows,
- работает стабильно,
- всегда есть, если установлен Android NDK.
Что считать “валидным” Align
Google говорит про совместимость с 16 KB.
Значит корректные значения выравнивания:
0x4000(16 KB) ✅0x8000(32 KB) ✅0x10000(64 KB) ✅
То есть правило простое:
Alignдолжен быть не меньше 0x4000 и кратен 0x4000.
А вот значения:
0x1000(4 KB) ❌0x2000(8 KB) ❌
— это проблема.
Быстрая проверка APK/AAB (Python-утилита)
Я сделал небольшой скрипт, который:
- распаковывает APK или AAB
- находит все
.so - проверяет
LOAD Alignчерезllvm-readelf - выводит список проблемных библиотек
- завершает работу с ошибкой (можно использовать в CI)
Скачать скрипт
👉 Ссылка на скачивание:
app_checker.py
Пример запуска
python app_checker.py app-release.apk
Вывод в случае проблемы выглядит так:
16KB page-size check failed:
[bad align=0x1000] artifact=app-release.apk abi=arm64-v8a file=lib/arm64-v8a/libgdx.so
[bad align=0x1000] artifact=app-release.apk abi=x86_64 file=lib/x86_64/libgdx-freetype.so
Что делать, если нашли 0x1000
Если проблема в ваших собственных .so (вы собираете NDK-код сами) — решение обычно:
- обновить NDK до свежей версии,
- пересобрать
.so.
Если проблема в сторонней библиотеке — варианты:
- обновить SDK/движок до версии, где
.soуже пересобраны под 16KB, - или заменить библиотеку,
- или (в редких случаях) собрать natives самостоятельно.
Автоматическая проверка в Gradle (до загрузки в Google Play)
Ручная проверка полезна, но лучше автоматизировать всё так, чтобы сборка падала сразу, если в APK/AAB попали несовместимые .so.
Я добавил Gradle task check16kPageSize, который:
- ищет release APK/AAB,
- распаковывает,
- проверяет
.soчерезllvm-readelfиз Android NDK, - проверяет только
arm64-v8aиx86_64, - падает с понятным списком проблемных файлов.
Требования
Чтобы Gradle-скрипт работал, нужно:
- установленный Android NDK
- и чтобы Gradle мог найти
llvm-readelf
Обычно NDK уже установлен через Android Studio:
Tools → SDK Manager → SDK Tools → NDK (Side by side)
Gradle task check16kPageSize
Вставьте код в android/build.gradle (или в модуль, который собирает APK/AAB).
task check16kPageSize {
group = "verification"
description = "Checks all .so in release APK/AAB and verifies LOAD Align is 16KB-compatible for 64-bit ABIs using NDK llvm-readelf."
doLast {
def llvmReadelf = resolveLlvmReadelf()
def outputsRoots = [file("$buildDir/outputs"), file("$projectDir/release")].findAll { it.exists() }
def artifacts = files(outputsRoots.collect { root ->
fileTree(root) {
include "**/*.apk"
include "**/*.aab"
}
}).files.findAll { it.isFile() }.sort { a, b -> a.absolutePath <=> b.absolutePath }
artifacts = artifacts.findAll {
it.name.toLowerCase().contains("release") || it.parentFile.name.toLowerCase().contains("release")
}
if (artifacts.isEmpty()) {
def searched = [file("$buildDir/outputs"), file("$projectDir/release")]*.absolutePath.join(", ")
throw new GradleException("No release APK/AAB found. Searched: ${searched}")
}
def errors = []
def tmpRoot = file("$buildDir/tmp/check16kPageSize")
tmpRoot.mkdirs()
// Check only 64-bit ABIs
def allowedAbis = ["arm64-v8a", "x86_64"] as Set
artifacts.each { artifact ->
def unpackDir = new File(tmpRoot, artifact.name + "-" + artifact.lastModified())
if (unpackDir.exists()) unpackDir.deleteDir()
unpackDir.mkdirs()
copy {
from zipTree(artifact)
into unpackDir
}
def soFiles = fileTree(unpackDir) { include "**/*.so" }.files
soFiles.each { soFile ->
def relPath = soFile.absolutePath.substring(unpackDir.absolutePath.length() + 1).replace('\\', '/')
def abiMatcher = (relPath =~ /(arm64-v8a|armeabi-v7a|x86_64|x86)/)
def abi = abiMatcher.find() ? abiMatcher.group(1) : "unknown"
if (!allowedAbis.contains(abi)) {
return
}
def stdout = new ByteArrayOutputStream()
def execResult = exec {
commandLine llvmReadelf.absolutePath, "-lW", soFile.absolutePath
standardOutput = stdout
errorOutput = stdout
ignoreExitValue = true
}
if (execResult.exitValue != 0) {
errors << "[readelf failed] artifact=${artifact.name} abi=${abi} file=${relPath}"
return
}
def output = stdout.toString("UTF-8")
output.eachLine { line ->
def load = (line =~ /^\s*LOAD\s+.*\s(0x[0-9a-fA-F]+)\s*$/)
if (load.matches()) {
def alignStr = load.group(1).toLowerCase()
long alignVal = Long.parseLong(alignStr.substring(2), 16)
// Valid if >= 0x4000 and multiple of 0x4000
if (alignVal < 0x4000L || (alignVal % 0x4000L) != 0L) {
errors << "[bad align=${alignStr}] artifact=${artifact.name} abi=${abi} file=${relPath}"
}
}
}
}
}
if (!errors.isEmpty()) {
throw new GradleException("16KB page-size check failed:\n" + errors.join("\n"))
}
logger.lifecycle("check16kPageSize: OK (${artifacts.size()} artifact(s) scanned)")
}
}
tasks.matching { it.name in ["assembleRelease", "bundleRelease"] }.configureEach {
finalizedBy check16kPageSize
}
Как запускать
После сборки релиза:
./gradlew assembleRelease
или
./gradlew bundleRelease
проверка выполнится автоматически.
Можно запускать и отдельно:
./gradlew check16kPageSize
Почему проблема чаще всего в сторонних SDK
Самое неприятное в этой истории:
- вы можете обновить Gradle, AGP, targetSdk
- но если в вашем APK лежит
.so, собранная сторонним SDK сAlign = 0x1000, то ничего не изменится.
Именно поэтому эта проблема массово бьёт по:
- libGDX (особенно
libgdx.soиlibgdx-freetype.so) - старым версиям TensorFlow Lite / ML Kit
- рекламным SDK
- любым AAR/JAR с embedded natives
Итог
Если ваше приложение содержит .so, лучше прямо сейчас:
- ✅ прогнать проверку APK/AAB локально
- ✅ добавить проверку в Gradle/CI
- ✅ заранее поймать проблемные библиотеки до загрузки в Play
Потому что когда Play Console начинает ругаться — обычно дедлайн уже рядом, а менять SDK в последний момент крайне неприятно.
Полезные ссылки
- Документация Android про page size:
https://developer.android.com/guide/practices/page-sizes - Пост Android Developers Blog про переход на 16 KB:
https://android-developers.googleblog.com/
Если статья помогла — можете написать в комментариях, с какими SDK/движками вы столкнулись с проблемой 16KB. Я добавлю в пост список “самых частых виновников” и решений.