Сбер вакансии Backend
Сбер вакансии Backend
Сбер вакансии Backend
Написать пост

Уменьшаем размер приложения на Android с помощью Dynamic delivery

Отредактировано

Рассказали об особенностях принципах работы с Dynamic delivery и разобрали, как с её помощью перенести фичи приложения в динамические модули.

2К открытий4К показов
Уменьшаем размер приложения на Android с помощью Dynamic delivery

В мобильной разработке не первый год актуальна концепция мультифункционального приложения (Super app). Она имеет много преимуществ, но зачастую пользователя интересует лишь часть функционала. А остальные фичи остаются невостребованными и занимают место на устройстве. Создание единого большого приложения ведёт ещё и к увеличению объёма, что негативно отражается на количестве скачиваний.

Уменьшить размер приложения и дать пользователю возможность самостоятельно выбрать подходящий ему набор фич — важные задачи, которые помогает решить Dynamic delivery. В сети есть много инструкций, как создать динамическую фичу с нуля. Но как превратить написанный код в динамическую фичу?

В этой статье я хочу раскрыть вопрос миграции фич в динамические модули на примере нашего флагманского приложения на Android. Расскажу о влиянии Dynamic delivery на архитектуру, о возможных сценариях миграции существующих фич, о сложностях, с которыми я столкнулся и о результатах, которые мы получили.

  1. Коротко о Dynamic feature delivery
  2. Влияние Dynamic delivery на архитектуру нашего приложения
  3. Миграция существующих фич
  4. Результаты
  5. Заключение
  6. Дополнительные материалы

Коротко о Dynamic feature delivery

Dynamic delivery — технология от Google, которая позволяет выделить из монолитного приложения фичи, и устанавливать и удалять их прямо во время выполнения программы.

Если вы только начали знакомство с Dynamic delivery, то рекомендую начать с ссылок, которые я собрал в конце статьи.

После ознакомления с этим материалом вы сможете лучше понять принцип работы Dynamic delivery, ознакомиться с предоставляемым API, найти пример с использованием архитектуры RIBs. Однако, не во всех проектах используется эта архитектура. Поэтому я дополню подборку источников и расскажу, как у нас в мобильном штабе «Лаборатории Касперского» происходила миграция существующего кода в динамический модуль.

Влияние Dynamic delivery на архитектуру нашего приложения

Как была построена многомодульность

Начну с короткого описания нашей архитектуры (более подробное можно найти по ссылкам в конце статьи). В нашем проекте Kaspersky Internet Security для Android есть module-injector, который содержит базовые интерфейсы:

BaseDependencies используется для перечисления объектов, которые требуются фиче на вход (зависимости фичи). BaseApi — для перечисления объектов, которые фича предоставляет наружу (внешний интерфейс фичи). ComponentHolder нужен для связки BaseDependencies. BaseApi — позволяет получить реализацию BaseApi конкретной фичи, передав все необходимые зависимости.

Для лучшего понимания рассмотрим эти три интерфейса на примере фичи Security news. Эта фича позволяет получать актуальные новости безопасности. В качестве входных зависимостей у неё будет логин в формате строки. А наружу она предоставит интерактор с методом проверки новостей:

Связующим звеном выступает SecurityNewsFeatureComponentHolder:

Таким образом, чтобы получить инстанс интерактора в основном модуле, необходимо в метод init объекта SecurityNewsFeatureComponentHolder передать зависимости фичи.

В проекте присутствует и альтернативный вариант базового ComponentHolder — LazyComponentHolder, который создаёт инстанс API фичи только при первом обращении. Реализацию его в статье приводить не буду, чтоб не усложнять материал.

Если в проекте используется Dagger, то в теле метода createApi будет обращение к dagger-компоненту фичи. Однако, такая тройка интерфейсов не обязывает использовать тот или иной DI-инструмент.

В приведённом выше коде SecurityNewsComponent расширяет интерфейс SecurityNewsFeatureApi презентором SecNewsPresenter. Он объявлен в интерфейсе SecurityNewsComponent, а не в SecurityNewsFeatureApi, потому что используется в коде внутри модуля фичи, а не внутри модуля App. Чтобы внутри фичи можно было получить SecNewsPresenter, изменим код SecurityNewsFeatureComponentHolder:

Как можно заметить, теперь мы держим ссылку типа SecurityNewsComponent, а не SecurityNewsFeatureApi. Появился метод getSecurityNewsComponent, который поможет получить SecNewsPresenter внутри кода фичи. В итоге внутри модуля фичи происходит обращение к методу getSecurityNewsComponent, а вне его — к методу get.

Влияние Dynamic delivery на архитектуру приложения

В случае обычных приложений главный модуль App знает обо всех фича-модулях. В случае Dynamic delivery динамическая фича зависит от модуля App, которыйничего не знает о модуле динамической фичи и работает с ней через ReflectionAPI. Подробнее можно почитать в документации.

Уменьшаем размер приложения на Android с помощью Dynamic delivery 1
Cхема зависимости модулей друг от друга

Описанные выше ограничения Dynamic delivery подталкивают нас изменить код внутри модуля фичи. Начнём с разделения модуля на API и реализацию.

Уменьшаем размер приложения на Android с помощью Dynamic delivery 2
Схема зависимости модулей с учётом Dynamic feature delivery

Такое разделение следует сделать для создания общего для App и Dynamic feature impl-модуля, куда можно положить классы и интерфейсы, на которые ссылаются в коде оба модуля.

В Dynamic feature API выносим SecurityNewsFeatureDependencies, SecurityNewsFeatureApi и все интерфейсы и классы, которые используются в них (в нашем случае — SecurityNewsInteractor). Однако, мы не можем вынести SecurityNewsFeatureComponentHolder — он связан с сущностями фичи, которые должны остаться в том же модуле. Если обращений к SecurityNewsFeatureComponentHolder будет несколько, то увеличится количество использования ReflectionAPI в коде (например, через ReflectionAPI нужно будет вызвать метод init, чтобы проинициализировать фичу, а затем — get, чтобы получить внешний интерфейс фичи).

Для упрощения кода можно уменьшить количество доступных только через Reflection API методов до одного (оставить только метод инициализации init). Для этого в module-injector создаём абстрактный класс ApiHolder:

В модуле FeatureSecurityNewsApi создаём объект SecurityNewsApiHolder:

В модуле FeatureSecurityNewsImpl меняем код в SecurityNewsFeatureComponentHolder:

Таким образом, сперва через ReflectionAPI нужно вызвать метод инициализации у SecurityNewsFeatureComponentHolder, в результате чего будет создан SecurityNewsComponent, а ссылка на компонент сохранится в поле объекта FeatureSecurityNewsApi. После инициализации внутри модуля фичи будет происходить обращение к методу getSecurityNewsComponent объекта SecurityNewsFeatureComponentHolder, а вне модуля — к методу getApi объекта SecurityNewsApiHolder.23ц

Миграция существующих фич

Мы успешно адаптировали архитектуру многомодульного приложения под Dynamic delivery. Следующим шагом применим один из нескольких сценариев включения фичи в состав приложения. Исходя из официального описания, существует 3 сценария:

  • при установке (install-time delivery);
  • при установке с определёнными фильтрами (conditional delivery);
  • по запросу (on demand delivery).

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

Рассмотрим сценарий, когда фича Security news была частью приложения, а теперь мы хотим её сделать динамически подгружаемой по требованию. Если обычный gradle-модуль сделать сразу динамически подгружаемым, то фича в следующем релизе исчезнет у пользователя, который получит обновление. Это может иметь негативные последствия как поведенческого характера, так и юридического. Если это не критично, можно явно пользователя попросить скачать фичу снова. Если по каким-то причинам важно сохранить фичу на девайсах тех, к кому она уже попала (например, если за неё уже заплатили), то нужно проработать процесс миграции. Рассмотрим возможные сценарии миграции от простого к сложному.

1 вариант

Выше мы рассмотрели 3 сценария включения фичи в состав приложения. Чтобы сохранить фичу у пользователя, можно сперва gradle-модуль превратить в динамическую фичу, доступную в момент установки (сценарий install time), а после этого сделать её подгружаемой по требованию (сценарий on-demand delivery). В этом варианте получаем 3 релиза приложения:

  • релиз A содержит фичу как обычный gradle модуль;
  • релиз B содержит фичу как Dynamic module с dist-параметром install-time;
  • релиз C содержит фичу как Dynamic module с dist-параметром on-demand.

В этом случае, при миграции с релиза B на релиз C фича не пропадёт с устройства пользователя. Однако, такой вариант миграции всё равно сохраняет риск потери фичи: если пользователь получит релиз A, не получит релиз B, а затем получит релиз C.

2 вариант

Очевидно, что проблему с неполучением промежуточного релиза можно решить увеличением количества этих промежуточных релизов. То есть между релизами A и C из первого варианта выпустить промежуточные: B1, B2, B3… Если пользователь не получил релиз B1, он сможет получить B2 или B3.

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

3 вариант

В этом варианте миграции можно запустить скачивание фичи в фоне при её отсутствии после получения обновления. Загрузка модуля не по запросу пользователя может завершиться обрабатываемой ошибкой:

exception com.google.android.play.core.splitinstall.SplitInstallException: Split Install Error(-7): Download not permitted under current device circumstances (e.g. in background). (https://developer.android.com/reference/com/google/android/play/core/splitinstall/model/SplitInstallErrorCode.html#ACCESS_DENIED).

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

По личному опыту, я смог больше 10 раз поставить фичу в фоне, прежде чем получить ошибку (10 раз провести последовательность действий: ставить apk, скачивать в фоне фичу, удалять apk). Только после получения ошибки пришлось зайти в приложение и явно подтверждать скачивание. Вдобавок расходовать трафик на скачивание фичи без его ведома может быть плохим решением.

Этот вариант тоже не даёт гарантию. Потребуется явное согласие пользователя: нужно будет попросить его подтвердить скачивание. Это 4-й вариант.

4 вариант

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

5 вариант

Google предоставляет возможность отложенной установки (deferred install). Такой вариант не даёт контроля над установкой. В официальной документации можно найти такую фразу: best-effort when the app is in the background. Практика показывает, что скачивание происходит при получении и установке следующего обновления. Таким образом, пользователь может пробыть без фичи какое-то время до получения обновления.

6 вариант

Если важно сохранить какой-то функционал у всех пользователей, можно ядро фичи оставить в приложении, а опциональную часть и тяжёлые ресурсы вынести в динамическую фичу, подгружаемую по требованию. В случае фичи Security news в состав ядра можно включить интерактор, который останется в приложении и продолжит мониторить и получать свежие новости. А всю логику, связанную с UI, — загружать отдельно по требованию.

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

Результаты

Выделение кода существующих фич в динамические модули может быть трудозатратным и требовать использования одного из предложенных выше сценариев миграции. Безусловно, новые фичи разрабатывать, как динамические модули, зачастую бывает проще. Однако, выделение уже существующих фич может существенно уменьшить объём приложения приложения.

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

Дальнейшее выделение динамических фич из приложения поможет больше сократить размер. Приложение можно распаковать и оценить ожидаемый результат. Можно оптимизировать размеры:

  • dex-файлов вынесением части кода в динамические модули (в том числе вынесением больших используемых библиотек, которые нужны только этой фиче);
  • папок res и assets вынесением картинок и других файлов в динамические модули;
  • папки lib (например, тяжёлые нативные .so-файлы);
  • бинарных ресурсов вынесением большого количества строк и идентификаторов в динамические модули.

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

Следует помнить, что вынесение определённых фич может негативно отразиться на конверсиях в покупки, подписки и так далее.

Заключение

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

Описанная в статье многомодульная архитектура может упростить вам работу с динамическими модулями, а предложенные сценарии миграции — минимизировать возможные негативные риски.

Надеюсь, статья была полезна для вас. Делитесь в комментариях своими результатами и мнениями об этой возможности от Google.

Дополнительные материалы

Следите за новыми постами
Следите за новыми постами по любимым темам
2К открытий4К показов