По исследованиям Google - каждые 10 МБ веса приложения снижают шанс загрузки приложения пользователем на 6%. Это значит, что чем меньше весит ваше приложения, то вероятность, что его скачают, выше. Также важно не забывать, что у вашего приложения могут быть легковесные конкуренты, что даёт им преимущество. Вы можете сказать, что у вас нет проблем с интернетом и он дешевый, так что скачать любой объем данных не проблема. Важно помнить, что вы разрабатываете приложения не только для того места, где вы находитесь. Даже в пределах небольшого района города интернет может сильно отличаться, уже не говорю об отдаленных уголках страны. В некоторых странах нет быстрого интернета вовсе, а его стоимость тарифицируется помегабайтно. Часть пользователей используют устройства с небольшим объемом памяти: 16, а то и вовсе 8 ГБ, что также заставляет их выбирать приложения по их размеру. Поэтому важно учитывать особенности всех устройств и регионов для достижения максимальной пользовательской базы.
Это статья - расшифровка нового видео на канале, в котором я рассказываю подходы, которые вы можете использовать для уменьшения размера вашего приложения. Это поможет упростить жизнь пользователей и скачиваться приложение будет быстрее. Скорость отладки приложения также ускорится, ведь билды будут быстрее доставляться на Android устройство и эмулятор.
Чтобы лучше понять это видео, я рекомендую посмотреть вам предыдущее, где я рассказал про то, что находится внутри APK и как Google Play оптимизирует доставку приложения, скачивая только необходимые части на устройства пользователя.
Если вам интересно следить за самыми последними новостями Android разработки и получать подборку интересных статей по этой тематике, тогда вам стоит подписаться на Телеграм-канал Android Broadcast и мой YouTube канал "Android Broadcast"
Какие размеры связаны с APK
Android приложение имеет несколько связанных с собой размеров
Вес самого APK файла
Размер начальной загрузки из магазина
Размер приложения на устройстве, который можно увидеть в настройках системы в информации о приложении
Размер скачиваемого обновления
В статье я буду разбирать, как оптимизировать размер APK (первый пункт списка). Чтобы повлиять на размер начальной загрузки и обновлений вам нужно полагаться либо на Google Play с его стандартными оптимизациями, либо заняться организацией многомодульной архитектуры приложения (подробнее тут), которая позволит вам выделять фичи вашего приложения и скачивать их по необходимости (Play Feature Delivery)
Ресурсы
Графика
Растровая графика
Любое приложение содержит множество картинок или загружает их с сервера. Давайте начнем с того, что мы оптимизируем графику, которая у нас в ресурсах Android приложений. Любые добавляемые картинки стоит хранить в оптимизированном формате. На момент записи этого видео - это WebP, формат для хранения изображений, разработанный компанией Google. Он поддерживается, начиная с Android 4.2. Вы можете конвертировать любые растровые изображения в него прямо в Android Studio c помощью встроенной утилиты. При конвертации вам нужно будет выбрать компрессию, что сокращает размер файлов, но может сказаться на сохраняемых деталях. Обычно уровень компрессии выбирают в 80%, но вы можете экспериментировать с настройками. Важно понимать, что если вы можете видеть различия на большом экране с высокой плотностью, то часть артефактов ваши пользователи могут не увидеть на своих смартфонах вовсе.
В Android 12 (API level 30) появилась поддержка более современного формат AVIF на основе кодека AV1. Из-за фрагментации Android ждать, когда minSdk станет 30, нам еще долго, хотя может вам повезло или вы читаете статью в далёком будущем, когда в Android все цветёт и пахнет.
Оптимизаторы картинок
Также вы можете использовать оптимизаторы растровых картинок, которые позволяют без потери качества уменьшить вес приложения. Я лично на одном из своих проектов с помощью оптимизатора ImageOptim добился сокращения размера с 9 Мб до 6.5 Мб автоматическим прогоном всех картинок, т.е. уменьшением размера графики на 25%! Утилита это делает за счет сокращения количества хранимых уникальных цветов и других опций, которые позволяют делать различные форматы картинок.
Этот трюк сработает для PNG, JPG, BMP и статических GIF. WebP уже хорошо оптимизированный по размеру формат и при конвертации картинки в него вы скорее получите размер файла ниже, чем у оптимизированной PNG/JPG картинки.
После оптимизации в build.gradle вам надо отключить опцию crunchPngs, которая отвечает за оптимизацию PNG, так как для уже оптимизированных файлов может привести к увеличению их размера. Опция по умолчанию выключена для всех дебажных сборок, а поэтому правим ее только для релизных сборок. Возможно еще и скорость сборки незначительно улучшите.
android {
...
buildTypes {
debug {
crunchPngs false
}
release {
crunchPngs false
}
}
}
Не стоит оптимизировать 9-patch изображения. После оптимизации с помощью ImageOptim 9-patch изображения переставали корректно работать (Если знаете как это исправить - пишите в комментариях). По-хорошему вы и так должны добавлять 9-patch изображения в минимальном размере в пикселях для каждой плотности.
Векторная графика
Следующий шаг - это максимально использовать векторные форматы для изображений. Это не только VectorDrawable, но и более старые форматы - shape, gradient и др. Они позволяют описывать изображения в XML, что позволит вам заменить несколько растровых изображений на одно векторное, которое будет показываться в хорошем качестве на всех экранах. Векторная графика в Android хоть и похожа частично на SVG, но не поддерживает множество ее возможностей и предназначается для простых иконок. Если вы видите, что XML с векторной картинкой слишком большое и сложное, то это может негативно сказаться на скорости их первой отрисовки. Во время работы приложения эти картинки превращаются в растровые изображения в памяти и последующее их использование будет быстрее.
У векторной графики есть одна проблема - экспорт их от дизайнеров содержит намного сложнее path-ы, по сравнению с оригиналом с сайта Material Design. Поэтому проверяйте, что будет оптимальнее для вас.
Шрифты
При анализе размера приложения я сталкивался с тем, что разработчики добавляют множество различных шрифтов и даже тянут за собой стандартный шрифт Android - Roboto. В этот момент я шел к дизайнерам и спрашивал: “Насколько принципиально использовать нам везде Roboto или наш системный шрифт по умолчанию, который скорее всего и является Roboto?” Практически всегда это устраивало всех дизайнеров, что позволяло удалять эти шрифты и упрощало задачу разработчикам..
Загружаемые шрифты
Важной особенностью шрифтов является то, что они уникальны, фактически имя шрифта идентифицирует его. Вряд ли в 2 разных приложениях в ресурсы положили файл с названием “Roboto”, включающим в себя 2 разных шрифта. А если таких приложений будет установлено 10 штук на телефоне? Тут с решением пришла Google. Помимо того, что в Android 8.0 добавили ресурсы шрифтов, также появилась возможность не класть их как файлы, а описывать параметры ресурса и делегировать их загрузку Google Play Services. Фича называется - Downloadable Fonts. Конечно же все это кэшируется на устройстве и несколько раз загружать один и тот же шрифт сервис не станет. К сожалению, с момента анонса так и не появилось новых публичных поставщиков шрифтов кроме Google Fonts, но там вы найдете открытые и самые популярные шрифты.
Чтобы воспользоваться этой фичей, вам надо сгенерировать сертификат. Удобнее всего это сделать с помощью Android Studio через визуальный редактор UI. Затем надо описать необходимый шрифт в ресурсах через XML и после этого вы можете ссылаться также, как бы делали это с файлом шрифта, что позволяет провести миграцию на загружаемые шрифты очень просто.
По умолчанию вам надо будет запрашивать загрузку каждого шрифта в коде.
val request = FontRequest(
"com.example.fontprovider.authority",
"com.example.fontprovider",
"my font",
certs
)
scope.launch {
try {
val typeface: Typeface = awaitFonts(context, request, handler, null, callback)
// Шрифт загружен, можно начать использовать
} catch (e: TypefaceRequestException) {
val reason: Int = e.reason
// Ошибка загрузки
}
}
Также вы можете добавить список шрифтов, которые надо скачать на устройство при установке или обновлении приложения из Google Play, что гарантирует их наличие на устройстве при первом запуске и вам не придется делать дополнительных шагов.
<!-- font/font1 -->
<font-family
android:fontProviderAuthority="com.example.fontprovider.authority"
android:fontProviderPackage="com.example.fontprovider"
android:fontProviderQuery="example font"
android:fontProviderCerts="@array/certs"
/>
<!-- res/values/arrays.xml -->
<resources>
<array name="preloaded_fonts">
<item>@font/font1</item>
<item>@font/font2</item>
</array>
</resources>
<!-- AndroidManifest.xml -->
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts"
/>
Лишние ресурсы
Общие ресурсы
В больших проектах, где много отдельных фичей команд, может происходить добавление одних и тех же ресурсов несколько раз. Чаще всего это происходит со шрифтами и картинками. Чтобы избегать таких ситуаций, я рекомендую заводить модуль с общими ресурсами для всего приложения: стандартные шрифты, базовые картинки и иконки и пр. ресурсы, которые не привязаны к одному модулю и могут быть использованы в любом модуле приложения. Таким модулем может выступить ваша дизайн система или любой другой модуль в вашем приложении.
Drawable Tint
Еще одну оптимизацию вы можете использовать для одинаковых по форме изображений, но разных по цвету. Название eй tint. Фактически это позволяет вам создать картинку на основе существующей, перекрасив ее в другой цвет. Это можно делать как внутри drawable ресурсов, так и многие view для атрибутов с drawable ресурсами имеют одноименный атрибут с приставкой Tint. Кстати, возможность делать tint влияет на то, что обычно все иконки в Android генерируются нейтрального цвета (серые или черные), а потом динамически перекрашиваются через tint
<!-- Перекрашивание картинок в drawable ресурсе -->
<bitmap
android:src="@drawable/ic_like"
android:tint="@color/red"
/>
<!-- Перекрашивание картинок в layout ресурсе -->
<ImageView
android:src="@drawable/ic_like"
android:tint="@color/red"
/>
Удаление неиспользуемых ресурсов
Помимо этого, не забывайте удалять ресурсы, которые вы уже не используете. Такой анализ позволит вам выполнить Android Lint, да и сама Android Studio умеет определять места, где используется ресурс. Также я рекомендую вам в Gradle для билдов включать опцию shrinkResources, которая не добавляет в сборку неиспользуемые ресурсы.
Но нужно не забывать, что в Android доступ к ресурсам можно выполнять не только через R классы, но и динамически по его имени. Такое использование анализаторов, о которых я только что рассказал, не сможет работать и вам нужно добавить в исключения ресурсы, которые должны быть сохранены всегда. В этом случае вы должны будете самостоятельно следить за их своевременным удалением, чтобы не таскать в APK лишний груз.
Только необходимые конфигурации ресурсов
Зачастую в приложениях мы не поддерживаем все возможные конфигурации, например, добавляем лишь несколько языков, если не вовсе один; отказываемся от ресурсов для ldpi экранов и прочие упрощения во блага большинства. Но вот не все разработчики библиотек поступают также. Например, Google Play библиотеки поддерживают более 30 локалей и естественно тянут их за собой в ваши приложения, когда вы поддержку этих языков не осуществляете.
Android Gradle Plugin позволяет указать, какие именно конфигурации ресурсов вам нужно добавлять в финальный билд. Делается это с помощью параметра resourceConfigurations. Плагин сам определяет, к какой категории относятся указанные конфигурации, и оставит ресурсы только в тех, которые вы прописали.
android {
defaultConfig {
…
// оставляем ресурсы только для английского и русского языков
resConfigs += listOf("en", "ru")
}
}
При использовании App Bundle Google Play будет загружать только актуальные необходимые строковые ресурсы и вроде бы оптимизация не нужна, но это не так. Ведь если в приложении некоторые строки будут содержать переводы для отдельных локалей, то вы получите часть текстов в заданном языке, а часть в языке по умолчанию (обычно это английский).
Код
Нативные библиотеки
Если у вас используются нативные библиотеки в приложении, то обычно вы сразу увидите, как их размер выделяется на фоне остальных файлов. Оптимизировать их размер можно с помощью использования Google Play Dynamic Delivery, т.е. используйте App Bundle как минимум не загружать
В процессе разработки мы за собой тягаем универсальную APK, что есть не очень хорошо, ведь это лишний вес. Тут на помощь придет специальная опция abiFilters в Android Gradle Plugin, которая позволяет указать, под какие ABI добавлять нативные либы. Я использую эту опцию во время разработки. Позволяет сократить размер dev сборки, т.е. быстрее доставлять её на устройство.
android {
defaultConfig {
ndk {
// удаляем все ABI кроме ARM
// не делайте так если вам важна поддержка x86
abiFilters = listOf(“armeabi-v7a”, “arm64-v8a”)
}
}
}
Проблема этого подхода заключается в том, что эффект этой настройки распространяется на все сборки, что может быть вам не нужным. Чтобы сделать ее только локальной на вашем компьютере, я добавлял поддержку нового property из local.properties, в котором указывал поддерживаемые архитектуры и считывал их в Gradle скрипте. Код вы можете увидеть на экране, а ссылку на проект с примером я добавлю в описание к этому видео. Файл local.properties был выбран, потому что он по умолчанию игнорируется в Git и все, что там остается, только на локальной машине.
# local.properties в корне Android проекта
android.abis=armeabe-v7a,arm64-v8a
// Считываем значения из файла local.properties
def supportedAndroidAbis() {
def localProperies = new Properties()
def reader = file("local.properties").newReader()
localProperties.loade(reader)
return localeProperties.getProperty("android.abis").split(",")
}
Вы также можете выбрать и другой способ передачи параметров только на вашей локальной сборке, например, через параметры Gradle сборки. Как это сделать, тоже можете увидеть в репо на GitHub.
ProGuard/R8
Я думаю мало кого удивлю, что для релизных сборок нужно использовать минификаторы, которые позволяют удалить неиспользуемый код, и обфускаторы кода, которые меняют название классов, методов и полей, что приводит к меньшему размеру файлов. Сейчас стандартном для этих оптимизаций кода в Android является R8 от Google, которая является и обфускатором, и минификатором.
Одной из ошибок, которые может допустить разработчик и увеличить размер приложения - это сохранять весь код в какой-то пакет, особенно весь код библиотек, а также отключать для него обфускацию. Такие рекомендации зачастую можно нагуглить при тестировании обфусцированных билдов. В первую очередь, проверяйте правила в репозитории проекта. Многие библиотеки в своих JAR/AAR уже содержат файл с правилами, которые автоматом будут использоваться.
Чтобы проверить результат обфускации вашего кода, откройте APK, код которой прогонялся через R8 при сборке, а затем посмотрите, что есть в dex файлаx внутри.
Подключаемые библиотеки
Подключаемые библиотеки легко могут раздуть размер вашего приложения и нести за собой кучу неиспользуемого кода. Поэтому точно понимайте, зачем вы подключаете библиотеку, возможно, если ради одной простой функции, то лучше самостоятельно ее реализовать и не тянуть стороннюю зависимость.
Также часть библиотек может использоваться только малой частью пользователей, например, что-то связанное с AR или платными функциями. Тогда вам тут может помочь Google Play Dynamic Feature, подробнее рассказывал о ней в предыдущем видео. Сразу скажу, чтобы реализовать этот подход, от вас потребуются навыки построения архитектуры в коде и умение правильно организовать Gradle модули.
Странные файлы в APK
Помимо этого, в ваш APK из Java библиотек может попадать всякий мусор, я даже видел, как одна библиотека добавила свои Java исходники, а другие авторы кладут лицензии. Со всеми этими файлами можно работать из кода, поэтому система сборки Android не убирает эти файлы, а кладет по тому же пути, что они были в JAR или AAR. По сути многие файлы вам не нужны. Чтобы избавиться от них, вы можете воспользоваться настройкой процесса упаковки и сделать это в Gradle с помощью опции packagingOptions. Вы можете указать, какие файлы и папки не должны попасть в выходной билд, решить конфликты (например, когда есть несколько одинаковых файлов, но в разных либах)
android {
packagingOptions {
// Для so файлов
jniLibs.pickFirsts += [“lib/**/libA.so”]
// Для всех прочих файлов
resources.excludes += ["README.md", "sources/**"]
}
}
Очень важно понимать, что часть файлов важна для работы библиотек, так, например, JodaTime в Java ресурсах тащит с собой базу часовых поясов и без нее библиотека перестанет работать. Поэтому удаляйте все постепенно и проверяйте работоспособность приложения.
Анализируем результат
После того как мы выполнили все оптимизации, наступает время проверить результат и сделать это можно в Android Studio через тот же инструмент, где мы смотрели содержимое APK. Мы можем посмотреть наш прогресс и сравнить 2 APK между собой. Все очень просто и понятно, поэтому не буду тратить время на демонстрацию моих навыков Drag & Drop и нажатия кнопок.
Помимо этого, на канале есть видео, как можно автоматизировать отслеживание изменения размера сборок на CI
Я разобрал самые основные советы, от которых вы можете начать оптимизацию размера сборки своего приложения. В комментариях призываю вас делиться способами уменьшения размера приложения, а также результатами, которые вы смогли достичь при помощи моих советов. На этом у меня всё! Всем хорошего Android! Пока-пока!
LuigiVampa
Хороший и полезный обзор приёмов, спасибо! От себя могу добавить ещё пару пунктов:
1) Выставление minSdk на 24 позволяет избавиться от части автоматически сгенерированных растровых картинок из векторов содержащих тэг gradient - этот тэг не корректно обрабатывается до апи 23 включительно и вызывает краш при обращении к ресурсу в рантайме, поэтому gradle во время сборки для всех векторных изображений содержащих тэг gradient генерирует растровые png изображения под соответствующий dpi. Также начиная с minsdk 24 немного уменьшается размер dex-файлов (скомпилированного байткода), точных причин не вспомню, но кажется связано это с более полноценной поддержкой java 8. Совет, разумеется, применим не для всех, но если есть возможность сделать minSdk 24 - то это круто.
2) Иногда вручную получается оптимизировать растровые картинки лучше чем автоматическими инструментами. Писал для интереса инструмент который рекурсивно ходил по директории с ресурсами и сначала пытается оптимизировать изображения с помощью pngquant, а затем преобразовывает их в webp формат через cwebp. Проблема в том что разные изображения содержат разные цвета и другие параметры, и одни можно сжать сильнее других без потери качества, а другие начнут видимо терять качество даже при небольшой обработке. Поэтому когда я проходился по всем изображениям в проекте вручную и где-то вручную пропорционально сжимал размер в пикселях, где-то руками удалял сохранённые с файлом метаданные, где-то больше или меньше оптимизировал png через pngquant, и с большим или меньшим значением качества прогонял преобразование в webp, контролируя визуально итоговое качество, то у меня получилось ужать размер ресурсов в конечном апк почти на 70% лучше, чем в случае применения автоматического подхода. Некоторые изображения получилось сжать в разы, и даже десятки раз, личный рекорд - более чем в 100 раз (png 1.2Мб без видимой потери качества был ужат до 11 Кб)
kirich1409 Автор
Спасибо за советы!