Разделы портала

Онлайн-тренинги

.
Утечки памяти в мобильных приложениях: руководство для QA-инженеров
10.07.2024 00:00

Оригинальная публикация

Меня зовут Ира и я руковожу отделом тестирования мобильной платформы: наш отдел занимается разработкой инструментов для автоматизации тестирования мобильных приложений Ozon и тестированием внутренних библиотек, которые используются в наших приложениях. Около года назад мы пытались понять, почему у одной из команд джоба с автотестами отваливается по тайм-ауту. К слову, это был проект мобильного приложения для продавцов, и на нем у нас для автоматизации тестирования используются нативные фреймворки: Kaspresso + Kotlin для Android и XCTest + Swift для iOS.

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

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

Как исправлять найденные проблемы в своей статье я не описываю.

Введение

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

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

Использование оперативной памяти во время работы с мобильным приложением

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

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

  1. Внутренняя память (ROM). Это основная память, используемая для хранения операционной системы, приложений и данных пользователя. В эту память происходит запись при скачивании и установке приложения.

  2. Оперативная память (RAM). Данный вид памяти обеспечивает работу всех выполняемых в устройстве процессов. Она используется для временного хранения данных и программ во время их выполнения. Также RAM требует непрерывного источника питания: если питание отключается, память очищается.

  3. Карта памяти (microSD). Некоторые смартфоны поддерживают расширение памяти с помощью съёмных карт microSD. Обычно такие карты могут добавить до нескольких сотен гигабайт дополнительного пространства для хранения данных.

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

  5. Облачное хранилище. Многие смартфоны интегрируют возможность работы с облачными сервисами для хранения данных, такими как Google Drive, iCloud, Dropbox и другими. Это позволяет сохранять данные в облаке и получать к ним доступ с разных устройств.

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

Также оперативная память активно используется операционной системой для управления переходами между экранами и обеспечения плавной работы мобильного приложения. Операционная система отслеживает жизненный цикл каждого экрана (Activity или Fragment в Android, ViewController или View в iOS) и использует оперативную память для сохранения состояния экрана в случае необходимости. Например, если пользователь переходит на другой экран и затем возвращается, состояние экрана может быть восстановлено из памяти. 

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

Когда приложение завершает свою работу, связанный с ним процесс также завершается. Это означает, что все выделенные для этого процесса ресурсы, включая оперативную память, освобождаются и возвращаются в систему.

Допустим, у нас есть два экрана: «Товары» со списком товаров и «Настройки» с настройками приложения. Попробую нарисовать схематично, что происходит, когда запускается приложение и происходит переход по экранам.

На картинке выше зелёным цветом отмечена область в памяти, которая всегда должна быть выделена для корректной работы приложения в любой момент времени. Жёлтым цветом отмечены те области, в которых есть данные, некритичные для работы всего приложения (скорее всего, пользователю не нужна информация с экрана Товаров, когда он находится в Настройках приложения, и данные Товаров можно удалить). Если выделенная изначально под наше приложение область в памяти будет заполнена, приложение может запросить у операционной системы дополнительный ресурс. Однако этот ресурс ограничен техническими характеристиками смартфона. Поэтому важно своевременно удалять неиспользуемые данные из памяти. Это можно делать из кода приложения: разработчик может управлять тем, что и когда размещать в оперативной памяти и когда удалять из неё ненужные объекты. И как раз в этом месте могут возникать ошибки. Самые распространённые из них — это утечки памяти.

Типичные баги, связанные с утечками памяти

  1. Вылеты приложения
    Постоянное увеличение использования памяти может привести к исчерпанию ресурсов и в конечном итоге к вылету приложения из-за нехватки памяти. Приложение может закрываться неожиданно без какого-либо предупреждения или сообщения об ошибке из-за исчерпания памяти.

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

  3. Потеря данных
    В некоторых случаях утечки памяти могут привести к потере данных, если приложение не успевает сохранить данные перед выходом из-за нехватки памяти.

  4. Неотзывчивый интерфейс
    Утечки могут привести к неотзывчивости пользовательского интерфейса: кнопки могут перестать реагировать на нажатия, прокрутка может замедлиться или остановиться.

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

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

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

Для Android-приложений есть профилировщик внутри Android Studio или можно подключить к проекту библиотеку LeakCanary. Для iOS обычно используется Xcode Instruments.

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

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

  1. Выполнение действий в фоновых процессах

    1. Шаги:

      1. Запустить выполнение каких-либо длительных операций или загрузку данных на одном из экранов.

      2. Перейти на другой экран или свернуть приложение в фоновый режим.

      3. Подождать некоторое время.

    2. Ожидаемый результат: 

      1. память не должна значительно увеличиваться в фоновом режиме и должна быть корректно освобождена после возвращения в приложение.

  2. Использование функций поиска и фильтрации

    1. Шаги:

      1. Перейти на экран с поиском или фильтрацией данных.

      2. Ввести поисковый запрос или установить фильтр.

      3. Произвести несколько операций поиска или фильтрации.

      4. Перейти на другой экран.

    2. Ожидаемый результат: 

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

  3. Использование различных типов контента

    1. Шаги: 

      1. Просмотреть контент различных типов на разных экранах (текст, изображения, видео и т. д.).

    2. Ожидаемый результат: 

      1. память не должна расти с каждым просмотром нового контента и должна быть освобождена после завершения просмотра.

  4. Использование кэширования данных

    1. Шаги:

      1. Просмотреть контент, который подлежит кэшированию (например, изображения или новости).

      2. Перезапустить приложение или очистить кэш.

      3. Просмотреть тот же контент снова.

    2. Ожидаемый результат: 

      1. память не должна увеличиваться из-за кэшированных данных, и кэшированные данные должны быть правильно освобождены после перезапуска приложения или очистки кэша.

  5. Использование памяти при работе в фоновом режиме

    1. Шаги: 

      1. Перейти на главный экран и свернуть приложение.

      2. Продолжить использовать другие приложения или просто дождаться продолжительного периода неактивности приложения.

      3. Вернуться в приложение.

    2. Ожидаемый результат: 

      1. память не должна значительно увеличиваться при переключении между приложениями и должна быть корректно освобождена при закрытии тестируемого приложения

В «ожидаемом результате» указываю результат, который можно увидеть в профилировщике.

Смотрим утечки памяти на Android

Тут стоит отметить, что данные актуальны на апрель 2023, а не на текущий момент.

Android Profiler

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

Запускаю debug-сборку на девайсе Poco X3 Pro. На скриншотах ниже можно увидеть общую информацию по использованию памяти приложением и список Activity и Fragment’ов, где потенциально могут быть проблемы в работе с памятью.

LeakCanary

Это библиотека для обнаружения утечек памяти в приложениях на платформе Android. Она автоматически анализирует утечки памяти и отправляет уведомления в случае обнаружения утечек, а также предоставляет подробные отчёты об утечках памяти с указанием места возникновения проблемы в коде.

Запускаю приложение с внедрённой библиотекой LeakCanary (и открываю полученные дампы в Android Studio):

Здесь интересно, что одна из утечек возникала в LeakActivity — а это Activity из библиотеки LeakCanary :)

Заключение

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

Что ещё можно почитать

  1. Про профилирование в Android

    1. https://developer.android.com/studio/profile/memory-profiler

    2. https://kmm.icerock.dev/learning/android/profiling

    3. https://developer.android.com/topic/performance/rendering/inspect-gpu-rendering

    4. https://habr.com/ru/companies/vk/articles/263413/

  2. Про leak canary

    1. https://developer.alexanderklimov.ru/android/debug/leakcanary.php

    2. https://habr.com/ru/articles/725778/

  3. Про инструменты iOS

    1. https://developer.apple.com/documentation/xcode/performance-and-metrics

    2. https://developer.apple.com/documentation/xcode/reducing-your-app-s-memory-use