Уменьшение веса APK

Перед началом

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

Рекомендую сначала прочитать статью Playing APK Golf (или её перевод). Это крайне интересно, как исследовательская работа, но абсолютно неприменимо в реальной разработке.

Proguard

При сборке apk в его dex файл (или файлы) попадает весь код приложения и весь код всех подключенных библиотек, даже неиспользуемый. Хорошо бы от этого лишнего кода избавиться. Для этих целей есть специальный инструмент под названием proguard. Он уже включён в Android SDK и любой Android проект из коробки настроен для работы с ним. Данный инструмент делает следующее:

  • Удаляет неиспользуемые классы, методы и поля;
  • Оптимизирует Java байткод;
  • Слегка обфусцирует код переименованием классов, методов и полей.

Всё это происходит на уровне Java байткода (не dex байткода), то есть файлов .class. На вход подаются jar(aar) файлы, а на выходе получаются они же, но в оптимизированном виде. В результате вес байткода значительно уменьшается.

В простейшем случае для включения proguard нужно открыть build.gradle конфиг уровня приложения и в блоке android/buildType/release выставить свойству minifyEnabled значение true:

Но этого не всегда достаточно и приложение может сломаться. В том же блоке есть свойство proguardFiles с путями к файлу конфигурации proguard (proguard-rules.pro) и к файлу с правилами оптимизации(proguard-android-optimize.txt) — его в большинстве случаев трогать не нужно. А вот файл proguard-rules.pro нам понадобится. Его можно найти в структуре проекта.

Если в проекте есть классы, которые участвуют в рефлексии, нужно запретить их переименование, так как при попытке обращения к классам(и их свойствам и методам), которые были переименованы, приложение упадёт с исключением. Это также касается pojo(data) классов, которые маппятся, например, через Gson, Firebase Database или какой-нибудь ORM. Для того, чтобы оставить такие классы без изменений, нужно в файле proguard-rules.pro применить к ним правило -keep:

-keep class com.example.myapp.model.ModelItem1
-keep class com.example.myapp.model.ModelItem2
-keep class com.example.myapp.model.ModelItem3

Или для всего пакета:

-keep class com.example.myapp.model.** { *; }

Со всеми правилами можно ознакомиться по ссылке.

Кроме этого нужно внимательно прочитать документацию ко всем библиотекам, используемым в проекте. Если для библиотеки нужны правила proguard, это всегда указано — просто скопируйте эти правила в файл proguard-rules.pro.

Теперь при сборке apk будут произведены оптимизации и размер приложения значительно уменьшится. Для сравнения — приложение, собранное без оптимизаций и оно же, собранное с proguard:

Размер приложения уменьшился с семи мегабайт до трёх с половиной и это только за счёт оптимизации байткода. Посмотрим, что внутри через APK Analyzer:

Было два dex файла, а остался один, даже с запасом — multidex больше не нужен. Минус 3.2 мегабайта лишнего кода. Считаю, что это отличный результат. Именно поэтому proguard — один из важнейших инструментов для уменьшения веса приложения и c ним стоит научиться работать.

После сборки релизного apk будет сгенерировано ещё несколько вспомогательных файлов, которые можно найти по пути «project/app_module/build/outputs/mapping/release»:

  • mapping.txt — список соответствия оптимизированных и не оптимизированных имён классов, методов и полей;
  • seeds.txt — список классов, методов и полей, которые не были оптимизированы;
  • usage.txt — список удалённых классов, методов и полей.

Файл mapping.txt — очень полезная вещь. После сборки релизного apk и загрузки его в Google Play Console стоит также загружать файл mapping.txt в раздел Android Vitals/Deobfuscation Files для соответствующей версии приложения:

Вот зачем это нужно. После обработки proguard код становится трудночитаемым и выглядит примерно так. Если произойдёт эксепшен, понять его стектрейс будет очень сложно. mapping.txt помогает расшифровывать эти стектрейсы так, будто код и не был обфусцирован.

Также стоит упомянуть о существовании инструмента под названием DexGuard, который оптимизирует dex байткод, но он платный и простым смертным недоступен.

shrinkResources

Вырезание ресурсов — возможность android плагина для gradle. Удаляет из apk неиспользуемые ресурсы, в том числе из библиотек. Работает только при включенном proguard(или R8), так как после удаления неиспользуемого кода появляются ресурсы, на которые нет ссылок. Включается в блоке android/buildType/release в файле build.gradle уровня приложения:

Теперь при сборке apk можно увидеть следующую строку:

В итоге apk уменьшился на 32 килобайта:

Не очень много, но тоже хорошо. Тут всё зависит от проекта, иногда вырезается более ощутимый объём.

Данная опция не будет работать, если в приложении есть модули Dynamic Features и\или Instant App.

Удаление ненужных языковых ресурсов

Некоторые библиотеки, особенно гугловские support lib(androidx) и firebase, содержат ресурсы для множества языков. Чтобы их удалить нужно явно указать, какие языки используются в приложении. Это делается в build.gradle уровня приложения в блоке android/defaultConfig (или в конкретном конфиге):

В моём случае это русский и английский. При сборке в apk будут включены только ресурсы для указанных языков. Посмотрим, что получилось:

Вес apk уменьшился ещё на 265 килобайт. Неплохо. Посмотрим, что внутри:

В apk остались языковые ресурсы только для русского и английского. Было 86 языков, а осталось только два.

R8

Ещё одно средство для оптимизации байткода, но уже от гугла. Думаю, есть ненулевая вероятность того, что R8 когда-нибудь полностью заменит proguard. Этот инструмент встроен прямо в компилятор D8, который превращает Java байтод в dex байткод. В отличии от proguard, R8 работает в момент компиляции java байткода в dex байткод, что немного уменьшает общее время сборки apk.

Proguard:

R8:

R8 можно включить в Android Studio, начиная с версии 3.3, а с версии 3.4 он включен по-умолчанию. Для включения R8 нужно открыть файл gradle.properties и добавить строку:

android.enableR8=true

Кроме этого, можно включить агрессивный режим оптимизации строкой:

android.enableR8.fullMode=true

Но эта функция, пока что, нестабильна и не рекомендуется к использованию.

R8 поддерживает правила для proguard (упоминавшийся ранее файл proguard-rules.pro) и точно так же генерирует файлы mapping.txt, seeds.txt и usage.txt. Кроме вырезания ненужного кода и обфускации, R8 также оптимизирует dex байткод (помним, что proguard оптимизирует только java байткод). После сборки видим, что удалось сэкономить ещё 28 килобайт за счёт уменьшения dex файла:

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

App Bundle

Это очень простой способ уменьшения веса приложения, не требующий никаких дополнительных знаний и умений. Достаточно собрать не apk, а бандл (файл с расширением .aab — android app bundle). Возможность сборки бандлов появилась в Android Studio с версии 3.2. Как и с apk, есть два способа сборки — через меню Build/Generate Signed Bundle(Apk) или через gradle таски bundleRelease/bundleDebug.

После сборки получается файл:

Его вес больше, чем вес apk, но это не имеет значения. По сути, это сырой файл приложения из которого могут быть сгенерированы apk файлы, которые можно устанавливать на android устройства. Этот файл загружается в Google Play Console, вместо apk, и там происходит магия. Приложение разбивается на основной apk и на набор apk конфигурации. При установке на устройство приложение собирается по частям:

Эта возможность называется Dynamic Delivery, она позволяет загружать на устройство только те ресурсы, которые соответствуют его конфигурации. А если конфигурация меняется, например пользователь меняет язык операционной системы, недостающие языковые ресурсы будут загружены автоматически в фоне. Работает это начиная с версии Android 5.0. Для более ранних версий заранее генерируются отдельные APK с различными конфигурациями, думаю это связано с виртуальной машиной Dalvik — она на такие фокусы неспособна. Всё это происходит автоматически и не требует ничего ни от разработчика, ни от пользователей.

При загрузке бандла в Google Play Store можно увидеть, сколько будет весить приложение:

Получается, что примерный размер приложения — 2.6 мегабайта, при том, что изначальный был почти 7 мегабайт. Разница очень ощутима — и она будет только расти с ростом размера приложения и количества его ресурсов.

На IO 2018 показывали такую картинку:

Для проверки и тестирования есть утилита bundletool. Она позволяет генерировать apk из бандла и манипулировать ими.

Dynamic Features

Ещё один способ уменьшения веса, который вытекает из предыдущего. Если возможно генерировать отдельные apk для различных конфигураций, то естественным образом возникает вопрос: «Можно ли насильно вынести отдельные куски приложения в отдельные apk?» Такая возможность есть и это Dynamic Features. Динамические модули — это отдельные модули приложения, которые специальным образом сконфигурированы. Создаётся такой модуль так же, как и другие модули Меню File -> New Module -> Dynamic Feature Module. Настройка несложная, подробней можно прочесть в документации. Также рекомендую посмотреть пример приложения от гугла.

В итоге получается, что есть основное приложение и дополнительные модули, которые могут быть загружены по требованию. Например в приложении с рецептами большинство пользователей только просматривает рецепты и лишь небольшой процент добавляет свои — возможность добавления рецептов можно вынести в dynamic module. Это уменьшит вес основного приложение на размер дополнительных модулей.

На февраль 2019 данная функциональность находится в стадии бэта версии. Если загрузить в Play Store бандл с динамическими модулями, появится предупреждение об этом и предложение заполнить анкету на участие в тестировании. После заполнения анкеты появится обещание рассмотреть заявку в течении пары недель. После некоторого ожидания в консоли появится сообщение о том, что можно загружать бандлы с динамическими модулями.

Удаление метаданных kotlin из сборки

Ещё один простой способ, подсмотренный в этой статье. В META-INF записываются дополнительные данные для рефлексии. Если в приложении не используется полноценная kotlin рефлексия(пакет kotlin-reflect), то эти файлы можно не включать в приложение. Для этого в gradle конфиге уровня приложения в блоке android нужно прописать следующее:

packagingOptions {
    exclude("META-INF/*.kotlin_module")
    exclude("**.kotlin_builtins")
    exclude("**.kotlin_metadata")
}

В моём случае вес приложения уменьшился ещё на 412 килобайт. Здесь показатель уменьшения веса напрямую зависит от количества кода в приложении. После сборки приложения стоит убедиться, что ничего не поломалось, так как рефлексия может использоваться в подключенных библиотеках. Также можно исключать отдельные файлы, чтобы оставить нужные для рефлексии.

И это ещё не всё

Размер приложения также можно уменьшить за счёт оптимизации различных ресурсов — графики, видео, звуков. Но это отдельная большая тема, достойная отельной статьи.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *