paint-brush
Как использовать RunLoop в приложениях iOSк@alekseimarinin
4,338 чтения
4,338 чтения

Как использовать RunLoop в приложениях iOS

к amarinin5m2024/03/04
Read on Terminal Reader

Слишком долго; Читать

Runloop — это цикл, который координирует получение и обработку входящих событий в определенном потоке. По умолчанию в приложении всегда работает основной RunLoop; он обрабатывает сообщения от системы и передает их приложению. Вспомогательные потоки требуют самоопределения необходимости RunLoop, и вам придется самостоятельно его настраивать и запускать.
featured image - Как использовать RunLoop в приложениях iOS
amarinin HackerNoon profile picture

Что такое Runloop?

RunLoop — это цикл, который координирует получение и обработку входящих событий в определенном потоке.


RunLoop присутствует в каждом потоке, но по умолчанию он находится в режиме ожидания и не выполняет никакой работы.


Разработчик может запустить его при необходимости, но автоматически он работать не будет. Для этого вам нужно будет написать код.

Какую проблему решает Runloop?

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


Наиболее заметно это при работе с пользовательским интерфейсом, например, при использовании UIScrollView.


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


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


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

Как это работает?

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


Итак, RunLoop может находиться в следующих режимах:

  1. Default — режим по умолчанию, поток бесплатный, в нем можно безопасно выполнять крупные операции.


  2. Tracking . Поток занят важной работой. На этом этапе лучше не запускать никаких задач или хотя бы запускать какие-то небольшие задачи.


  3. Initialization — этот режим выполняется один раз во время инициализации потока.


  4. EventReceive — это внутренний режим получения системных событий, который обычно не используется.

  5. Common — это режим-заполнитель, не имеющий практического значения.


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


Управление циклом выполнения в других RunLoop не является полностью автоматическим. Вам необходимо написать код для потока, который начнет цикл выполнения в подходящее время. Кроме того, вам необходимо соответствующим образом реагировать на события и использовать бесконечные циклы, чтобы гарантировать, что цикл выполнения не остановится.


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


Выполним задачу обычным способом:


 DispatchQueue.main.async { sleep(2) self.tableView.refreshControl?.endRefreshing() }


Но результат будет очень плохим. Пользователь заметит значительные задержки в работе приложения.

Этот негативный эффект возникает из-за того, что мы запускаем задачу в основном потоке, не обращая никакого внимания на то, что происходит в нем в данный момент.


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


Этого можно избежать, используя механизм RunLoop. Давайте реализуем ту же логику, используя это:


 CFRunLoopPerformBlock(CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue) { sleep(2) self.tableView.refreshControl?.endRefreshing() }


Позвольте мне объяснить, что здесь происходит. Функция CFRunLoopPerformBlock добавляет код для выполнения через RunLoop. Помимо самого блока кода, эта функция имеет 2 важных параметра.


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


Второй отвечает за режим, в котором будет выполнено задание.


Всего возможны три режима:

  • Отслеживание — когда пользователь взаимодействует с интерфейсом, например прокручивает UIScrollView.


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


  • Общий — сочетает в себе режим по умолчанию и режим отслеживания.

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

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

Таймеры

Помимо пользовательского интерфейса, цикл также тесно связан с работой таймеров.


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


 Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in // makeSomething }


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


 let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in // makeSomething } RunLoop.main.add(timer, forMode: .common)


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


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


 let thread = Thread { let timer = Timer(timeInterval: 1.0, repeats: true) { timer in // makeSomething } RunLoop.current.add(timer, forMode: .default) RunLoop.current.run() } thread.start()


Результат

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


Он особенно полезен при работе с пользовательским интерфейсом (UI) и таймерами, поскольку имеет возможность выполнять задачи в нужное время, что позволяет избежать «зависания» интерфейса и обеспечить корректную работу таймеров.


Несмотря на то, что работа с Run Loop требует дополнительного написания кода, это стоящая инвестиция, которая повышает эффективность и стабильность вашего приложения.