Эффективный бэкграунд: организация стабильной фоновой работы в связке двух мобильных медтех приложений
Всем привет! Меня зовут Антон, я ведущий разработчик в одной из команд мобильной разработки в компании DD Planet. В этой статье поделюсь опытом нашей команды по организации стабильной фоновой работы в мобильном медтех приложении, предназначенном для работы с медицинским оборудованием.
Введение
К нам в компанию поступил запрос на разработку мобильного приложения для взаимодействия с медицинским устройством, которое в режиме реального времени записывает данные с датчиков, расположенных на теле пациента, и может передавать эти данные сторонним устройствам посредством Bluetooth. Приложение должно обеспечивать расчет по исходным данным и отображение интерактивной кардиограммы и графика активности пациента (датчики акселерометра) в режиме реального времени и в режиме архива за выбранный период времени.
После завершения разработки заказчик планировал получить государственный патент на приложение и из-за этого с самого начала у нас появилось требование – в дальнейшем менять что-либо в его исходном коде будет нельзя. При этом после получения патента от нас требовалось обеспечить дальнейшее развитие проекта, планировалась разработка дополнительных функций, которые не могли быть реализованы изначально из-за сжатых сроков и планов заказчика.
Нашим решением стала разработка двух приложений. Первое приложение было предназначено для выполнения всех основных функций, а именно получения данных от медицинского устройства, их обработки и визуализации и после разработки не требовало обновлений.
Второе приложение, которое мы назвали приложением-компаньоном, должно было забирать данные у первого приложения и передавать их на бэкенд. В нем была предусмотрена авторизация пользователя, а передача данных на бэкенд должна была происходить в фоновом режиме без его явного участия. Наличие второго приложения позволило нам обойти ограничения, накладываемые патентом, и обеспечить дальнейшее развитие функционала проекта.
Приложение-компаньон было оптимизировано для периодической работы в фоновом режиме и минимального потребления ресурсов, чтобы ОС Android не заглушала его при повышенной нагрузке на систему. В статье речь пойдет о тех приемах оптимизации, которые позволили нам организовать стабильную работу в фоне и выполнить поставленные бизнес-задачи. Все примеры кода были упрощены для понимания, чтобы передать их суть и уместить в рамках статьи.
Выбор технологического стека
В требованиях заказчика изначально был выбран основной технологический стек – фреймворк разработки мобильных приложений Xamarin на базе .NET. Этот выбор был обусловлен наличием экспертизы и уже написанных ранее приложений на Xamarin и .NET у команды заказчика.
Несмотря на то, что с 1 мая 2024 года “классический” Xamarin больше не поддерживается и не обновляется корпорацией Майкрософт, мобильная разработка на платформе .NET по-прежнему доступна. В настоящий момент Xamarin окончательно интегрировался в платформу .NET и по факту превратился в .NET for iOS и .NET for Android. Также развивается кроссплатформенная разработка .NET MAUI – эволюция Xamarin.Forms.
К моменту начала разработки наша команда уже имела опыт создания приложений на Xamarin.iOS и Xamarin.Android, а с недавних пор мы также перешли на последние версии .NET. В мобильном приложении-компаньоне мы использовали версию платформы .NET 8.
Получение данных из основного приложения
Одной из основных задач, которую нужно было решить в приложении-компаньоне было получение данных из основного приложения. И в процессе разработки двух приложений мы рассматривали различные варианты ее реализации:
1. Межпроцессное взаимодействие (IPC)
- IPC (Inter Process Communication) позволяет приложениям обмениваться данными и управлять ресурсами.
2. Контент провайдер (Content Provider)
- Позволяет приложению предоставлять другим приложениям доступ к его данным через абстрактный интерфейс.
- Пример: приложение “Контакты” в Android, предоставляющее доступ к контактам пользователя другим приложениям, например, таким как банковские.
Нам нужно было асинхронное бэкграунд решение, чтобы процесс передачи данных происходил незаметно для пользователя и не требовал от него каких-либо действий в интерфейсе приложения.
Требования к реализации:
- Частое обращение к данным. Нам необходимо часто обращаться к данным.
- Скорость и асинхронность. Запрос должен быть быстрым и асинхронным.
- Объем данных. Возможность выборки из большого объема данных.
В конце концов мы пришли к варианту с использованием двумя приложениями единого источника данных – локальной базы данных SQLite.
При таком подходе базу данных нам пришлось расположить не во внутреннем хранилище основного приложения, а во внешнем, чтобы обеспечить доступ к ней из приложения-компаньона. Так как база данных доступна во внешнем хранилище, для обеспечения безопасности данных, мы решили ее зашифровать. Для этого мы использовали библиотеку Sqlite-net-sqlcipher, которая позволяет создавать подключения к зашифрованной базе данных.
Чтобы получить доступ ко внешнему хранилищу в Android, добавляем в AndroidManifest.xml следующие разрешения:

По умолчанию приложение имеет доступ только к своему внутреннему хранилищу, например, по пути «data/user/0/имя пакета». Для доступа к внешнему хранилищу нужно запросить разрешения.
На главном экране приложения переопределяем метод ViewAppeared

И добавляем проверку на наличие разрешений. Если разрешения не были предоставлены, уведомляем пользователя о необходимости их предоставить и не даем возможности пользоваться приложением в противном случае. Поскольку без предоставления разрешений основная фича приложения – фоновая передача данных на бэкенд не сможет функционировать.

StartupViewModel – это специальный экран, задача которого – запросить все необходимые доступы на платформенной части и закрыться. Происходит это незаметно для пользователя.
Переопределяем метод OnCreate, наследуемый от MvxActivity

Проверяем версию Android API и выполняем запрос разрешений на получение доступа

Следующий код подходит для версий Android API, начиная с 30

Переопределение метода OnActivityResult позволяет нам отследить, предоставлены ли разрешения и незаметно для пользователя закрыть экран StartupView

Для создания защищенного подключения используем разные перегрузки конструктора SQLiteConnectionString

Стоит отметить что одновременные обращения к базе данных SQLite из нескольких разных приложений (процессов) на чтение допустимы. Однако, во время обращения на запись база блокируется и доступна только одному обратившемуся процессу.
Перезапуск сервиса синхронизации данных в основном приложении с помощью push-уведомления в приложении-компаньоне
В нашем проекте push-уведомления выполняют две задачи:
- Информационные пуши.
- Сервисные пуши. Выполняют задачу перезапуска фонового сервиса основного приложения для синхронизации данных с медицинским устройством.
Если с информационными пушами все понятно – это видимые пользователю пуши с заголовком и текстом. То перезапуск сервиса с помощью невидимого сервисного пуша – это наше решение на случай, если ОС Android заглушит фоновую передачу данных. Если это произойдет, то бэкенд отследит отсутствие данных о синхронизации за некоторый промежуток времени и отправит пуш на устройство, тем самым перезапустит сервис.
Запуск по пушу без явных действий со стороны пользователя стороннего сервиса, реализованного и объявленного в другом Android приложении – это интересная задача, которая потребовала некоторого ресерча и попутного решения возникающих проблем. Одна из проблем заключалась в том, что на современных, актуальных версиях Android нельзя отправить Intent для запуска сервиса из фона. При попытке такого запуска возникает ошибка. Решение заключается в том, чтобы отправлять Intent из состояния Foreground, то есть из какого-либо запущенного Activity приложения. А для того, чтобы со стороны пользователя этот процесс прошел незаметно, мы запускаем полностью прозрачное активити поверх других приложений и отправляем Intent на перезапуск сервиса из его метода жизненного цикла OnResume.

Чтобы визуально запуск активити был незаметным для пользователя сделаем его прозрачным:

А в методе OnResume отправляем Intent на перезапуск (остановку и старт) фонового сервиса с указанием идентификатора (package) основного приложения “com.mainapp” и кастомного действия (action) “com.mainapp.START_FOREGROUND_SERVICE”:

Чтобы интент отправился, в AndroidManifest приложения-компаньона добавляем элемент <queries> с явным указанием идентификатора (package) основного приложения:

Иначе при попытке отправки будем получать вот такую ошибку:
W/ActivityManager( 1454): Unable to start service Intent { act=com.mainapp.START_FOREGROUND_SERVICE pkg=com.mainapp } U=0: not found
В AndroidManifest основного приложения добавляем информацию о фоновом foreground сервисе с указанием типа сервиса (connectedDevice) и кастомного действия (action), которое он будет обрабатывать:

А вот для возможности запуска активити из фона и его отображения поверх других приложений, необходимо добавить следующую логику при старте приложения на экране StartupView. С помощью специального интента Settings.ActionManageOverlayPermission пользователь будет перенаправлен на экран Настроек для выдачи необходимого разрешения:

Настройка фоновых задач
Итак, для выполнения фоновых задач, нам потребуется Android сервис. Начнем с выбора типа сервиса – их существует несколько, и для каждого из них есть свои условия работы. Подробнее можно ознакомиться в официальной документации Android.
Задача заключалась в обеспечении фоновой синхронизации данных, которая должна работать незаметно для пользователя, минимизировать нагрузку на устройство и преодолевать агрессивные ограничения на работу в фоне, которые системы некоторых вендоров Android могут применять для экономии заряда батареи смартфона.
Для этой задачи подходят несколько типов сервисов:
- Service – используется для выполнения долгосрочных фоновых задач, которые требуют постоянной работы.
- IntentService – сервис, который запускается при получении Intent. После завершения задачи он автоматически останавливается.
- JobService — сервис, для которого можно запланировать периодический запуск.
- ForegroundService – выполняется в фоновом режиме, при этом отображая постоянное уведомление. Преимущество этого сервиса в том, что он продолжает работать даже при ограничениях на фоновые задачи, введенных еще в Android 8.0 (Oreo) и выше. Начиная с Android 10, также были введены типы foreground сервисов, которые классифицируют задачи, выполняемые в фоне (например, синхронизация данных или воспроизведение мультимедиа). В основном приложении мы используем ForegroundService для обмена данными с подключенными по Bluetooth устройствами.
По нашему опыту работы с Service мы стремились обойти проблемы, связанные с ограничениями на работу в фоне, и учли последние изменения в Android 15, направленные на экономию заряда батареи, которые могли бы привести к новым проблемам при дальнейшей поддержке приложения.
В итоге мы выбрали JobService с периодическим запуском в фоновом режиме как оптимальное решение для этой задачи.
В AndroidManifest добавим информацию о сервисе:

Теперь создадим класс в проекте:

android.permission.BIND_JOB_SERVICE – необходимое разрешение для запуска сервиса через JobScheduler.
ForegroundService.TypeDataSync – данный тип указывает, что сервис выполняет задачи синхронизации данных.
Enabled: true – сервис доступен и может быть запущен системой.
Exported: false – сервис не может быть запущен другими приложениями. Например, для сервиса синхронизации данных с медицинским устройством из основного приложения параметр указан в True для того, чтобы мы могли его перезапускать из приложения-компаньона.
Переопределяем метод запуска сервиса:

Для управления состоянием задачи используем переменную _workerTask

Методы репозиториев, выполняющих логику синхронизации, вызываем в методе SyncData

Получение зависимостей на репозитории выполняем по необходимости, обернув в конструктор Lazy

При завершении работы фонового сервиса отменяем выполняемую задачу

Для добавления в планировщик фоновой задачи обращаемся к сервису JobSchedulerService

Минимальная периодичность запуска сервиса – 15 минут, однако во время тестов на устройствах нескольких вендоров мы воспроизводили периодичность перезапуска не в фиксированное время, а в интервале – 15-30 минут.
Добавление фонового сервиса в планировщик выполняем после авторизации.

Для остановки текущего фонового процесса, например, при деавторизации (Logout) подписываемся на LogoutMessage в классе фонового сервиса и отменяем текущее выполнение задачи

А также удаляем из планировщика последующие плановые перезапуски задачи

Заключение
В результате проведенной работы нам удалось успешно реализовать архитектурное решение, позволяющее обойти ограничения, связанные с патентованием основного приложения, и обеспечить дальнейшее развитие проекта. Разработанное приложение-компаньон эффективно решает задачу фоновой синхронизации данных с сервером, при этом потребляя минимальное количество системных ресурсов.
Описанные в статье приемы оптимизации фоновых процессов позволили достичь стабильной работы приложения даже в условиях повышенной нагрузки на систему Android. Настройка фоновых задач с использованием JobService обеспечила надежную синхронизацию данных и минимизировала нагрузку на устройство. Реализация сервисных push-уведомлений с помощью Firebase Cloud Messaging добавила дополнительный уровень надежности и автономности.
В процессе разработки мы столкнулись с рядом проблем, таких как ограничения Android на фоновые задачи и особенности работы с различными вендорами-производителями Android устройств. Однако, благодаря интересным решениям, мы смогли преодолеть эти вызовы и создать мобильное медтех приложение, удовлетворяющее всем требованиям заказчика.
Представленные в статье подходы к оптимизации фоновых процессов могут быть полезны разработчикам, решающим схожие задачи по созданию энергоэффективных приложений с длительной фоновой работой в условиях ограниченных системных ресурсов.