Как стать автором
Обновить
327.93
Ozon Tech
Команда разработки ведущего e‑com в России

Многопоточность в мобильных приложениях: руководство для QA-инженеров

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров2.8K

Всем привет! Меня зовут Ира, я руковожу отделом тестирования мобильной платформы в Ozon Tech. Наш отдел разрабатывает инструменты для автоматизации тестирования мобильных приложений Ozon и тестирует внутренние библиотеки.

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

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

Как работает мобильное приложение

Для начала разберёмся, как вообще работает мобильное приложение. Чтобы не путаться в понятиях, которыми мы будем оперировать, вот небольшая справка.

  • Процесс — это экземпляр программы во время выполнения. Каждое приложение запускается в своём собственном процессе, что обеспечивает изоляцию и безопасность.

  • Поток — это способ выполнения процесса. Набор инструкций, которые выполняются последовательно.

  • Многопоточность — это способность процессора обеспечивать одновременное выполнение нескольких потоков в рамках одного процесса.

Когда мы нажимаем на иконку приложения, операционная система создаёт новый процесс для приложения, который будет выполнять код приложения. Внутри созданного процесса запускается главный поток (он также называется main thread, UI thread или UI-поток). Позже расскажу, почему он главный и чем отличается от других, пока важно запомнить, что при старте приложения создаются и процесс, и поток внутри процесса.

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

Рисунок 1. Схематическое изображение процессов и потоков
Рисунок 1. Схематическое изображение процессов и потоков

Теперь вернёмся к главному UI-потоку. UI-поток отвечает за:

  1. отрисовку и изменения UI (перерисовка экрана, обновление данных в UI);

  2. обработку касаний/жестов/свайпов.

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

Кроме потоков и процессов ещё важно понимать процесс отрисовки графики. Современные устройства имеют разную частоту обновления экрана (герцовку). Например, на большинстве смартфонов это 60 Гц, но на некоторых моделях может быть 90 Гц, 120 Гц или даже выше. Это означает, что время кадра варьируется: для 60 Гц это 16.6 мс, для 120 Гц — всего 8.3 мс. Если UI-поток занят выполнением тяжёлых задач, таких как обработка данных или сложные вычисления, он может не уложиться в это время. В результате система пропускает отрисовку, и пользователь видит фризы (задержки изображения). Это особенно критично для игр и приложений с анимацией, где плавность напрямую влияет на пользовательский опыт.

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

Как следствие, мы можем оказаться, например, в ситуации:

  1. Когда жмём кнопки, а приложение не реагирует или увидим бесконечный лоадер.

  2. Можно также встретить ошибку ANR (Application Not Responding) — она возникает, когда приложение не отвечает и не может обрабатывать действия пользователя дольше 5 секунд.

  3. В итоге открывается диалоговое окно, предлагающее пользователю подождать или закрыть приложение.

  4. Когда в результате выполнения запроса видишь не то, что ожидал.

    На Рисунке 2 приведены скриншоты возможных ошибок (ANR и бесконечный loader):

Рисунок 2. Бесконечный лоадер и ошибка ANR

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

Теперь посмотрим, какие могут быть варианты работы приложения с потоками. Первый вариант — выполнять всё в одном UI-потоке, получится как на Рисунке 3:

pastedGraphic_2.png
Рисунок 3

Если в главном потоке выполнять запросы в сеть, обращения к БД и сложные вычисления, то в это время приложение не сможет обрабатывать пользовательский ввод.

Альтернативный подход — это синхронное выполнение задачи в другом потоке:

pastedGraphic_3.png
Рисунок 4

Такой подход нужно использовать, если для выполнения задачи нужно дождаться выполнения «медленной не UI-задачи». Например, мы зашли в Корзину, нажали кнопку «Оплатить». Нам нужно дождаться, когда заказ на сервере оформится и оплата пройдёт, и только тогда пользователю покажется следующий экран. В случае реализации как на рисунке 4 — в то время, когда у нас на сервере создаётся заказ и проходит оплата, мы не можем нажимать что-то в приложении, потому что для нас важно дождаться завершения операции по созданию и оплате заказа, чтобы пойти дальше.

Третий вариант работы приложения с потоками — асинхронное выполнение задачи в другом потоке — схематично изображён на Рисунке 5:

pastedGraphic_4.png
Рисунок 5

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

В общем виде выполнение различных задач в приложении может выглядеть так:

pastedGraphic_5.png
Рисунок 6

Проблемы многопоточности

В рамках этой статьи рассмотрим только популярные проблемы, которые обнаружить проще всего.

  1. Проблемы с UI/UX

    1. Неотзывчивый интерфейс

    2. Бесконечный loader

    3. ANR (Application Not Responding)

  2. Проблемы, связанные с тем, что разные задачи хотят получить доступ к одним и тем же ресурсам (Race Condition, Deadlock, Starvation)

Проблема 1. Проблемы с UI/UX

Посмотрим на две gif’ки, которые ниже (честно взятые из статьи https://habr.com/ru/articles/739212/).

Слева видим, что какая-то задача блокирует UI-поток, а когда пользователь пытается что-то кликнуть, то ничего не происходит. Через несколько секунд получаем сообщение с ошибкой ANR (Application Not Responding). Эта ошибка означает, что приложение не может обрабатывать действия пользователя более 5 секунд. Операционная система предлагает или подождать, или завершить работу приложения.

Справа более правильная реализация, когда есть loader, что-то в фоновом потоке подгружается, но UI-поток у нас не занят и пользовательские нажатия обрабатываются. Для пользователя интуитивно понятнее будет именно такое поведение приложения.

pastedGraphic_6.png
pastedGraphic_6.png

Проблема 2. Race Сondition

Race Сondition возникает, когда несколько потоков одновременно обращаются к одним и тем же данным или ресурсу, и результат зависит от порядка выполнения потоков.

Основные последствия:

  • непредсказуемое поведение приложения;

  • потеря / дублирование данных;

  • лазейки для злоумышленников.

pastedGraphic_7.png

Приведу пример тест-кейса для проверки ситуации с Race Condition.

Шаги:

  1. Пополнить баланс Ozon Карты на 500 руб.

  2. Накидать в Корзину товаров на 400 руб.

  3. Перейти в Корзину.

  4. Нажать на кнопку «Оплатить» несколько раз подряд.

Ожидаемое поведение: спишется 400 руб., на счету останется 100 руб.

Race Condition: 400 руб. спишется несколько раз, итоговый баланс будет меньше 0 руб. или будет 100 руб. Мы точно не знаем. Мы только знаем, что такая ошибка воспроизводится непостоянно и результат может быть каждый раз разным.

Проблема 3. Deadlock

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

Основные последствия:

  • зависание приложения;

  • потеря данных: данные могут сохраниться частично или исказиться.

  • утечка ресурсов;

    • RAM/соединение с БД;

  • падение приложения;

Картинка для наглядности (лучше любых примеров):

pastedGraphic_8.png

Проблема 4. Starvation

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

Последствия Starvation:

  1. Снижение производительности: фоновые задачи (кэширование, синхронизация) не завершаются, что ухудшает пользовательский опыт.

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

  3. Краши приложения: если starving-поток отвечает за критически важную логику (например, освобождение памяти).

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

pastedGraphic_9.png

Рекомендации при тестировании мобильных приложений

Общий список рекомендаций по тестированию может выглядеть следующим образом:

  1. запуск тяжёлых операций (загрузка данных, обработка изображений) одновременно со взаимодействием пользователя (прокрутка, нажатия);

  2. тестирование на слабых девайсах: устаревшие или бюджетные устройства быстрее проявляют фризы из-за нехватки ресурсов;

  3. тестирование в условиях слабого интернета: долгие запросы и прерывания запросов должны корректно обрабатываться;

  4. сценарии с прерываниями (звонок/смс/пуш/будильник/изменение ориентации экрана/сворачивание приложения и т. д.);

  5. имитация «хаотичного» поведения пользователя: быстрые клики, многократные нажатия на одну и ту же кнопку;

  6. параллельное редактирование (если приложение поддерживает многопользовательский доступ).

И самое важное — это не игнорировать единичные ошибки. Проблемы с многопоточностью воспроизводятся часто нестабильно.

Заключение

Ошибки, связанные с многопоточностью, часто трудно воспроизвести, так как они зависят от времени и порядка выполнения потоков. Это усложняет поиск и исправление багов. Но несмотря на все описанные сложности, многопоточность остаётся неотъемлемой частью разработки современных мобильных приложений. И теперь, когда мы ещё больше знаем про то, как работает приложение, что такое UI-поток и многопоточность, узнали, как выглядят проблемы с многопоточностью и как их искать, мы можем составлять стратегию тестирования с учётом полученной информации. Если же при тестировании не включать проверки работы с потоками, то есть риск пропустить критичные ошибки, связанные с потерей данных, неправильными списаниями, зависаниями приложения и возникновением ANR-ошибки. 

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

Теги:
Хабы:
+40
Комментарии5

Публикации

Информация

Сайт
ozon.tech
Дата регистрации
Дата основания
Численность
5 001–10 000 человек
Местоположение
Россия