Перейти к содержимому
Все записи

Переход Android на страницы памяти 16 КБ: что реально ломается и как проверить свой APK

Google теперь требует, чтобы Android-приложения поддерживали страницы памяти 16 КБ. Что это значит на практике, почему ломаются именно нативные библиотеки и как проверить, что у тебя всё в порядке.

MFKAPPS 4 мин чтения

Если ваше приложение содержит хоть какой-то нативный код — свой JNI или просто сторонний SDK с .so-файлом внутри, — велика вероятность, что оно несёт мину, которая взрывается только на новом железе. Android теперь поддерживает страницы памяти по 16 КБ наряду с традиционными 4 КБ, и Google Play требует, чтобы приложения, ориентированные на недавние версии Android, корректно работали с обеими. Большинство приложений на чистом Kotlin проходят через это вообще без изменений. Приложения с нативными зависимостями — часто нет, и способ падения неприятный: не предупреждение линтера, а краш при установке или первом запуске, причём только на устройствах, которые действительно используют страницы по 16 КБ.

Я прошёл через эту миграцию по всей линейке приложений — почти всё обошлось без происшествий, кроме одной зависимости. Вот что на самом деле включает в себя эта проверка.

Почему размер страницы вообще важен

Страница памяти — это минимальная единица, которой ОС управляет памятью. Android с самого начала использовал страницы по 4 КБ; некоторые новые чипсеты теперь по умолчанию используют страницы по 16 КБ, потому что более крупные страницы снижают накладные расходы на управление памятью для нагрузок с большим потреблением RAM — меньше записей в таблице страниц, меньше промахов TLB, в среднем быстрее запуск приложений. ОС и Android Runtime обрабатывают это прозрачно для обычного кода на Kotlin и Java. Это невидимо для ViewModel, Room, Compose — никому из них нет дела до размера страницы под капотом.

Нативному коду есть дело, потому что нативные библиотеки — это ELF-бинарники, а их загружаемые сегменты выравниваются по фиксированной границе на этапе сборки. .so, собранный с сегментами, выровненными по 4 КБ, прекрасно работает на устройстве со страницами по 4 КБ. Загрузите тот же файл на устройстве со страницами по 16 КБ — и динамический компоновщик может не суметь корректно его отобразить в память: приложение не запускается или падает в момент обращения к этой библиотеке.

Что реально ломается

Не ваш код на Kotlin. Список реального риска короткий и конкретный:

  • Ваши собственные модули NDK/JNI, если вы их собираете.
  • Сторонние AAR со встроенными предсобранными нативными библиотеками — обработка изображений, отчёты о крашах, движки баз данных, ML-рантаймы. Кандидат — всё, у чего внутри AAR есть .so.
  • Вывод старой цепочки инструментов. Библиотека, собранная старым NDK, до того как тулчейн стал по умолчанию выравнивать сегменты по 16 КБ, — самый частый сценарий отказа, причём даже в SDK, который в остальном активно поддерживается: бинарник просто не пересобирали с тех пор, как поменялось выравнивание по умолчанию.

Что не ломается: чисто Kotlin-зависимости, всё, что поставляется только как .jar или .aar без нативного кода, и собственные библиотеки Google в недавних релизах — они уже пересобраны с правильным выравниванием.

Проверка того, что реально лежит в вашем APK

Прежде чем трогать конфигурацию сборки, узнайте, есть ли риск вообще:

unzip -l app-release.apk | grep '\.so$'

Если ничего не найдено — всё, вы свободны: выравнивать нечего. Если записи есть, проверьте выравнивание сегментов каждой библиотеки напрямую:

unzip -p app-release.apk lib/arm64-v8a/libsomething.so | \
  readelf -l - | grep LOAD

Посмотрите на колонку Align в каждом сегменте LOAD. 0x4000 (16 КБ) значит, что библиотека уже выровнена правильно. 0x1000 (4 КБ) значит, что нет, и именно за ней нужно охотиться — либо за обновлённой версией SDK, из которого она пришла, либо, если это ваш код, за пересборкой.

APK Analyzer в Android Studio показывает ту же информацию без командной строки: откройте APK, зайдите в .so-файл в папке lib/, и он напрямую пометит выравнивание только по 4 КБ.

Решение — в порядке того, сколько у вас контроля

  1. Ваш собственный нативный код: пересоберите с актуальным NDK. Недавние версии тулчейна по умолчанию выдают вывод, выровненный по 16 КБ, так что часто это просто обновление версии в build.gradle.kts плюс чистая пересборка — без изменений в исходном коде.
  2. Сторонний SDK со старым .so: обновите зависимость. Почти каждый активно поддерживаемый SDK уже выпустил сборку, выровненную по 16 КБ; решение — это обновление версии, а не обходной путь.
  3. Зависимость, которую не обновили: это неприятный случай. Реальные варианты — завести issue в SDK, сделать форк и пересобрать самому, если это open source, или отказаться от зависимости. Не существует трюка на уровне APK, который перевыравнивает .so, собранный не вами.

Библиотека, которая меня подловила, — это как раз случай 2: расширение для базы данных, поставлявшее предсобранные нативные бинарники для шифрования на диске, лежащего в основе истории транзакций Granyn. Небольшое обновление версии принесло выровненную сборку; в коде интеграции ничего не изменилось.

Тестирование на реальном устройстве с 16 КБ

Проверки выравнивания говорят, что должно произойти; тестирование на реальных страницах по 16 КБ говорит, что происходит на самом деле. В Android Emulator есть системные образы, собранные специально для этого — найдите в SDK Manager вариант «16 KB Page Size» для недавнего уровня API, создайте из него AVD и установите APK точно так же, как для релиза. Если что-то, использующее невыровненную библиотеку, должно упасть, оно упадёт именно там — сразу и воспроизводимо, а это куда лучшее место для находки, чем тикет в поддержку с устройства, которого у вас нет.

Вывод

Для большинства приложений, ориентированных на Kotlin, эта миграция невидима — нет нативного кода, нет риска, нечего делать. Для всего, что тянет нативные зависимости, решение почти всегда — обновление версии зависимости, а не переписывание: найдите невыровненный .so с помощью readelf, обновите то, что его поставило, пересоберите и проверьте на системном образе с 16 КБ перед релизом. Проверка занимает десять минут. Пропустить её — значит узнать об этом позже из отчёта о краше.

// По теме

Ещё из журнала

MFKAPPS 4 мин чтения

In-App Review API в Android: как просить оценку, не раздражая пользователя

Практическое руководство по In-App Review API от Google — как он на самом деле работает, когда его запускать и почему обычный попап «оцените нас» незаметно портит вашу оценку в Play Store.

#android #engineering #kotlin
MFKAPPS 5 мин чтения

Ярлыки приложений Android и плитка быстрых настроек: логирование воды без открытия приложения

Практическое руководство по динамическому ShortcutManager API и TileService в Android — как позволить действию в один тап полностью обойти приложение, и о подводных камнях, на которых спотыкается большинство реализаций.

#android #engineering #kotlin
MFKAPPS 4 мин чтения

Типы foreground-сервисов в Android в 2026 году: как выбрать тот, что реально подходит вашей функции

Практическое руководство по ограничениям типов foreground-сервисов Android — dataSync, mediaPlayback, specialUse, shortService — и как выбрать правильный, не получив ни принудительной остановки, ни отказа в проверке.

#android #engineering #kotlin