Сжимаем APK, стараясь сохранить его работоспособность


/ PxHere / PD

Оптимизация веса APK — это нетривиальная, но очень актуальная во времена Instant App, задача. Включение proguard избавит вас от ненужного кода, если ваши зависимости можно определить на этапе компиляции, но в APK есть ещё несколько видов файлов, которые можно исключить из сборки.

Под катом о том, как сделать зависимости — определяемыми на этапе компиляции, какие файлы можно исключить из сборки и как это сделать, а так же, разберём, как исключить из сборки неиспользуемые компоненты, если у вас несколько приложений с общей кодовой базой.


Перед прочтением

  • Прежде, чем применять советы из статьи, оптимизируйте APK по гайду от Google. Эта статья для тех, кому недостаточно стандартных оптимизаций.
  • Под словом «proguard» я подразумеваю оптимизирующий компилятор с минификацией.
  • Под компонентом я имею в виду некую фичу продукта с точки зрения бизнеса. В нашем случае, это просто набор файлов в неком пакете. Gradle-модуль у нас один на всё приложение.

Вес нашего APK, оптимизированного по гайду от Google составлял 4.4 мб.

Лишние файлы

Начнём с простого. Если вы не используете kotlin-reflect, то можете исключить из сборки мета-информацию о kotlin-классах. Сделать это можно следующим образом:
В build.gradle (Module: app)

android {
packagingOptions {
exclude(«META-INF/*.kotlin_module»)
exclude(«**.kotlin_builtins»)
exclude(«**.kotlin_metadata»)
}
}

Для Java-рефлексии не нужны файлы *.kotlin_module, *.kotlin_builtins и *.kotlin_metadata. Определить, какую рефлексию вы используете, очень просто. Если вы пишите obj::class.<method>, то вы используете kotlin-рефлекцию, если же obj::class.java.<method>, то java-рефлексию.

Итог оптимизации для нас: -602.1 кб

Зависимости

Иногда библиотеки тянут за собой зависимости для случаев, которые никогда не произойдут в вашем приложении. Например, ktor-client тянет вместе с собой kotlin-reflect (0.5 мб!).
Я боролся с такими случаями следующим образом: собирал APK с minifyEnabled = true, закидывал его в анализатор Android Studio, загружал mapping.txt и искал пакеты, которые, по идее, не должны присутствовать в сборке. Например, kotlin.reflect. После запускал ./gradlew app:dependencies в папке проекта для поиска зависимостей (не забудьте увеличить длину истории в терминале. Дерево зависимостей может быть большим!). По этому дереву легко понять, что ссылается на лишние зависимости и исключить их. В build.gradle вашего модуля:

dependencies {
implementation(«io.ktor:ktor-client-core:$ktorVersion») {
exclude(group: «org.jetbrains.kotlin», module: «kotlin-reflect»)
}
implementation(«io.ktor:ktor-client-okhttp:$ktorVersion») {
exclude(group: «org.jetbrains.kotlin», module: «kotlin-reflect»)
}
}

Этот код убирает зависимость библиотеки ktor-client на kotlin-reflect. Если хотите исключить что-то другое — подставьте свои значения.

!!! Очень осторожно пользуйтесь этим советом! Перед исключением зависимостей, убедитесь, что вам они не нужны. Если вы этого не сделаете, то приложение может начать падать в продакшене !!!

Итог оптимизации для нас: -500.3 кб

Проверьте ваши XML

К сожалению, proguard не удаляет лишние файлы разметки на языке XML из папки layout. Неиспользуемые XML могут использовать «тяжёлые» виджеты и proguard не сможет исключить из сборки их тоже! Чтобы избежать такого, удалите неиспользуемые ресурсы с помощью Refactor -> Remove unused resources…

Проверьте ваш DI

Если вы, как и мы, используете runtime DI, то проверьте, нет ли у вас provider’ов для тех зависимостей, которые вы не используете. Proguard не может исключить их из сборки потому что они не являются неиспользуемыми с точки зрения компилятора. Вы используете их при построении графа зависимостей.

Исключите отладочные зависимости из релизной сборки

Инструменты отладки могут занимать неожиданно много места. Например, stetho весит около 0.2 мб после сжатия! В любом случае, лучше исключить из релизной сборки всю отладочную инфраструктуру, чтобы никто не смог узнать о вашем приложении слишком много, просто скачав его из Google Play.

Вы можете сделать так, чтобы для дебага и для релиза использовались разные версии одних и тех же файлов. Для этого в папке src, рядом с main, создайте папки debug и release. Теперь вы можете написать функцию initStetho, которая инициализирует Stetho в файле src/debug/java/your/pkg/Stetho.kt и функцию initStetho, которая не делает ничего, в файле src/release/java/your/pkg/Stetho.kt.

На всякий случай, сделайте так, чтобы эта зависимость включалась только в дебаговые сборки. Сделать это можно, заменив implementation на debugImplementation в build.gradle. Чаще всего, proguard исключает ненужные файлы даже без этого шага, но не всегда. Ответ на вопрос «почему?» ниже в тексте статьи.

Платформы

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


/ PxHere / PD

Наш опыт

Мы разрабатываем конструктор мобильных приложений E-SHOP. У нас несколько десятков клиентов и у каждого свой индивидуальный набор компонентов. Некоторые компоненты используются всеми клиентами, некоторые — только частью. Наша задача — включить в сборку клиента только те компоненты, которые ему нужны.

Исключение компонента по флагу

Для каждого клиента мы создаём отдельный productFlavor. Это удобно, потому что легко сделать разные ресурсы для разных клиентов, IDE предоставляет графический интерфейс для переключения между flavor’ами, и хорошо работают кэши. А ещё можно генерировать для каждого клиента свой BuildConfig.java. Значения полей этого класса известны на этапе компиляции. Это то, что нам нужно! Создаём поле типа boolean для каждого компонента.

android {
productFlavors {
client1 {
buildConfigField(«boolean», «IS_CATALOG_ENABLED», «true»)
}
client2 {
buildConfigField(«boolean», «IS_CATALOG_ENABLED», «false»)
}
}
}

Это — упрощённая версия конфигурации. Настоящая сложна из-за интеграции с нашим CI.

Теперь известно, активен ли компонент, на этапе компиляции, и proguard может исключить его из сборки!

Снова XML

Теперь проблема с неиспользуемыми XML-layouts приобретает новый масштаб! Нельзя просто взять и удалить разметку какого-нибудь компонента просто потому, что некоторым клиентам он не нужен.

В нашем приложении в XML одного из редкоиспользуемых компонентов, использовался виджет, который ссылался на библиотеку распознавания изображений firebase.ml.vision. Она весит около 0.2 мб, что немало. Было принято решение добавлять этот виджет кодом вместо того, чтобы объявлять его в разметке. После этого proguard смог исключить vision из сборки для клиентов, которым он не нужен.

Итог оптимизации для нас: -222.3 кб для среднего APK

Аннотация @Keep

Есть 2 способа сказать proguard, что ваш класс нельзя минифицировать: написать правило в файле proguard-rules.pro или поставить аннотацию @Keep. В библиотеке play-services-vision на корневом классе стоит именно эта аннотация. Поэтому 0.2 мб висело мёртвым грузом даже в тех приложениях клиентов, которым не нужно распознавание изображений.

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

К счастью, библиотека firebase.ml.vision, которая является более новой версией play-services-vision, не использует эту аннотацию и мы решили проблему, перейдя на неё.

И вновь DI

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

Итог оптимизации для нас: -20.1 кб для среднего APK

Выводы

  • Вес среднего APK уменьшился с 4.4 мб до 3.1 мб, а минимального — до 2.5 мб!
  • Код приложения не пострадал, а улучшился. Теперь с DI проще работать

Все оптимизации, представленные в статье — это «низковисящие фрукты». Их довольно легко внедрить и быстро получить результат. До -43% для уже оптимизированного APK в нашем случае. Надеюсь, я сэкономил ваше время тем, что перечислил всё в одном месте.

Всем спасибо!

Оставить комментарий