Всем привет!
Меня зовут Ира и я руковожу отделом тестирования мобильной платформы: наш отдел занимается разработкой инструментов для автоматизации тестирования мобильных приложений Ozon и тестированием внутренних библиотек, которые используются в наших приложениях. Около года назад мы пытались понять, почему у одной из команд джоба с автотестами отваливается по тайм-ауту. К слову, это был проект мобильного приложения для продавцов, и на нем у нас для автоматизации тестирования используются нативные фреймворки: Kaspresso + Kotlin для Android и XCTest + Swift для iOS.
Одна из гипотез заключалась в том, что в приложении могут быть утечки памяти и что-то зависает. Спойлер: дело было не в этом. В общем, около года назад я проверяла, что к чему там у нас с памятью приложения, а сейчас поняла, что полученными знаниями можно и поделиться.
Эта статья будет полезна тем, кто только начинает изучать, что происходит со стабильностью мобильного приложения. Внутри статьи разберёмся с тем, как приложение работает с оперативной памятью; что такое утечки памяти и когда они возникают; как утечки влияют на стабильность работы приложения и как их находить.
Как исправлять найденные проблемы в своей статье я не описываю.
Введение
Представьте, что запускаете мобильное приложение, в котором много полезных фич и интересных возможностей. Но вместо того, чтобы наслаждаться его работой, сталкиваетесь с внезапным завершением работы приложения, с зависаниями приложения и медленным откликом после нажатия на элементы. Одна из возможных причин такого поведения — это утечки памяти.
Утечка памяти (memory leak) — это состояние, когда приложение использует оперативную память устройства и не освобождает её после завершения своей работы. Такое состояние может наступать по разным причинам, включая ошибки в коде приложения, некорректное управление жизненным циклом объектов, утечки ресурсов или неправильное использование сторонних библиотек.
Использование оперативной памяти во время работы с мобильным приложением
Прежде чем перейти к поиску и анализу проблем, вызываемых утечками памяти, разберёмся, какая память есть в смартфоне и что происходит с памятью, когда пользователь работает с мобильным приложением.
В смартфонах обычно используется несколько видов памяти:
-
Внутренняя память (ROM). Это основная память, используемая для хранения операционной системы, приложений и данных пользователя. В эту память происходит запись при скачивании и установке приложения.
-
Оперативная память (RAM). Данный вид памяти обеспечивает работу всех выполняемых в устройстве процессов. Она используется для временного хранения данных и программ во время их выполнения. Также RAM требует непрерывного источника питания: если питание отключается, память очищается.
-
Карта памяти (microSD). Некоторые смартфоны поддерживают расширение памяти с помощью съёмных карт microSD. Обычно такие карты могут добавить до нескольких сотен гигабайт дополнительного пространства для хранения данных.
-
Временное хранилище (кэш). Смартфоны также используют кэш-память для временного хранения данных приложений и веб-страниц, чтобы ускорить доступ к ним.
-
Облачное хранилище. Многие смартфоны интегрируют возможность работы с облачными сервисами для хранения данных, такими как Google Drive, iCloud, Dropbox и другими. Это позволяет сохранять данные в облаке и получать к ним доступ с разных устройств.
В этой статье нас интересует только оперативная память (RAM), далее — память. Когда запускается мобильное приложение, для него выделяется место в памяти. После инициализации приложение начинает загрузку данных с сервера, обработку пользовательского ввода, выполнение алгоритмов и т. д. Каждая операция требует временного использования памяти для хранения промежуточных результатов и переменных.
Также оперативная память активно используется операционной системой для управления переходами между экранами и обеспечения плавной работы мобильного приложения. Операционная система отслеживает жизненный цикл каждого экрана (Activity или Fragment в Android, ViewController или View в iOS) и использует оперативную память для сохранения состояния экрана в случае необходимости. Например, если пользователь переходит на другой экран и затем возвращается, состояние экрана может быть восстановлено из памяти.
При необходимости операционная система может освобождать память, занимаемую предыдущим экраном, который больше не отображается на экране. Это может включать в себя удаление из памяти временных объектов и освобождение ресурсов, чтобы освободить место для нового экрана. Некоторые данные могут быть сохранены в памяти на время перехода между экранами, чтобы обеспечить плавный переход. Например, если пользователь вводил данные в форме на одном экране, эти данные могут временно сохраняться в памяти, чтобы не потеряться при переходе к следующему экрану.
Когда приложение завершает свою работу, связанный с ним процесс также завершается. Это означает, что все выделенные для этого процесса ресурсы, включая оперативную память, освобождаются и возвращаются в систему.
Допустим, у нас есть два экрана: «Товары» со списком товаров и «Настройки» с настройками приложения. Попробую нарисовать схематично, что происходит, когда запускается приложение и происходит переход по экранам.
На картинке выше зелёным цветом отмечена область в памяти, которая всегда должна быть выделена для корректной работы приложения в любой момент времени. Жёлтым цветом отмечены те области, в которых есть данные, некритичные для работы всего приложения (скорее всего, пользователю не нужна информация с экрана Товаров, когда он находится в Настройках приложения, и данные Товаров можно удалить). Если выделенная изначально под наше приложение область в памяти будет заполнена, приложение может запросить у операционной системы дополнительный ресурс. Однако этот ресурс ограничен техническими характеристиками смартфона. Поэтому важно своевременно удалять неиспользуемые данные из памяти. Это можно делать из кода приложения: разработчик может управлять тем, что и когда размещать в оперативной памяти и когда удалять из неё ненужные объекты. И как раз в этом месте могут возникать ошибки. Самые распространённые из них — это утечки памяти.
Типичные баги, связанные с утечками памяти
-
Вылеты приложения
Постоянное увеличение использования памяти может привести к исчерпанию ресурсов и в конечном итоге к вылету приложения из-за нехватки памяти. Приложение может закрываться неожиданно без какого-либо предупреждения или сообщения об ошибке из-за исчерпания памяти. -
Замедление работы приложения
Постепенное увеличение использования памяти может привести к уменьшению производительности приложения. Оно может стать медленным и отвечать с задержками, особенно на старых или слабых устройствах. -
Потеря данных
В некоторых случаях утечки памяти могут привести к потере данных, если приложение не успевает сохранить данные перед выходом из-за нехватки памяти. -
Неотзывчивый интерфейс
Утечки могут привести к неотзывчивости пользовательского интерфейса: кнопки могут перестать реагировать на нажатия, прокрутка может замедлиться или остановиться. -
Утечки памяти в фоновых процессах
Также они могут происходить и в фоновых процессах приложения, что может привести к излишнему использованию ресурсов устройства даже тогда, когда приложение неактивно.
Ниже приведу примеры возможных тест-кейсов для поиска проблем с утечками памяти, но сначала познакомимся с инструментами, которые помогут их обнаружить. Самый простой путь — это поискать ошибку OutOfMemoryError в логах и приложить найденные логи к тикету. Или можно посмотреть производительность приложения самостоятельно в режиме реального времени. Для этого используются профилировщики.
Профилировщик — это инструмент разработки программного обеспечения, который позволяет анализировать производительность приложения и выявлять узкие места в его работе. Он используется для сбора информации о времени выполнения различных частей программы, использовании памяти, вызовах функций и других аспектах работы приложения.
Для Android-приложений есть профилировщик внутри Android Studio или можно подключить к проекту библиотеку LeakCanary. Для iOS обычно используется Xcode Instruments.
Про то, как пользоваться профилировщиками, написано уже много статей, поэтому не буду их пересказывать здесь, добавлю просто ссылки в конце своей статьи (в следующем разделе будут скриншоты того, что можно увидеть в профилировщике).
Наконец разберёмся, что можно делать, чтобы воспроизвести баг с утечкой памяти в мобильном приложении. Важно отметить, что утечки начинают влиять на приложение, когда пользователь долго сидит в приложении, и это даёт возможность накопиться большому количеству утечек, что может вызвать проблемы в совершенно любом месте приложения. То есть возможна ситуация, когда утечка происходит на одном экране (например, экран со списком Товаров), и в фоне память утекает; потом мы переходим на другой экран (например, экран Настроек приложения) и происходит вылет. Поэтому такие баги сложно поймать и локализовать: обычно нет чёткого сценария воспроизведения.
Выполнение действий в фоновых процессах
Шаги:
Запустить выполнение каких-либо длительных операций или загрузку данных на одном из экранов.
Перейти на другой экран или свернуть приложение в фоновый режим.
Подождать некоторое время.
Ожидаемый результат:
память не должна значительно увеличиваться в фоновом режиме и должна быть корректно освобождена после возвращения в приложение.
Использование функций поиска и фильтрации
Шаги:
Перейти на экран с поиском или фильтрацией данных.
Ввести поисковый запрос или установить фильтр.
Произвести несколько операций поиска или фильтрации.
Перейти на другой экран.
Ожидаемый результат:
память не должна значительно увеличиваться при использовании функций поиска или фильтрации и должна быть корректно освобождена после перехода на другой экран.
Использование различных типов контента
Шаги:
Просмотреть контент различных типов на разных экранах (текст, изображения, видео и т. д.).
Ожидаемый результат:
память не должна расти с каждым просмотром нового контента и должна быть освобождена после завершения просмотра.
Использование кэширования данных
Шаги:
Просмотреть контент, который подлежит кэшированию (например, изображения или новости).
Перезапустить приложение или очистить кэш.
Просмотреть тот же контент снова.
Ожидаемый результат:
память не должна увеличиваться из-за кэшированных данных, и кэшированные данные должны быть правильно освобождены после перезапуска приложения или очистки кэша.
Использование памяти при работе в фоновом режиме
Шаги:
Перейти на главный экран и свернуть приложение.
Продолжить использовать другие приложения или просто дождаться продолжительного периода неактивности приложения.
Вернуться в приложение.
Ожидаемый результат:
память не должна значительно увеличиваться при переключении между приложениями и должна быть корректно освобождена при закрытии тестируемого приложения
В «ожидаемом результате» указываю результат, который можно увидеть в профилировщике.
Смотрим утечки памяти на Android
Тут стоит отметить, что данные актуальны на апрель 2023, а не на текущий момент.
Android Profiler
Это встроенный инструмент Android Studio для разработки приложений под Android. Он предоставляет информацию о производительности приложения, включая использование памяти, CPU и сетевых ресурсов.
Запускаю debug-сборку на девайсе Poco X3 Pro. На скриншотах ниже можно увидеть общую информацию по использованию памяти приложением и список Activity и Fragment’ов, где потенциально могут быть проблемы в работе с памятью.
LeakCanary
Это библиотека для обнаружения утечек памяти в приложениях на платформе Android. Она автоматически анализирует утечки памяти и отправляет уведомления в случае обнаружения утечек, а также предоставляет подробные отчёты об утечках памяти с указанием места возникновения проблемы в коде.
Запускаю приложение с внедрённой библиотекой LeakCanary (и открываю полученные дампы в Android Studio):
Здесь интересно, что одна из утечек возникала в LeakActivity — а это Activity из библиотеки LeakCanary :)
Заключение
При тестировании приложения мы тщательно проверяем UI/UX, логику, обработку ошибок, которые приходят с сервера, загрузку картинок и много ещё всего, что пользователь может легко увидеть глазами, но не всегда думаем про стабильность нашего приложения. При этом, допуская наличие утечек памяти в приложении, мы рискуем вызвать сильный негатив у пользователя во время использования приложения, несмотря на полезный функционал и красивый интерфейс. Надеюсь, эта статья помогла лучше понять, какие проблемы со стабильностью могут возникать в мобильном приложении и как их отловить.
Что ещё можно почитать
-
Про профилирование в Android
-
Про leak canary
-
Про инструменты iOS
Автор: quakke