dispatch_after в Swift: как GCD выполняет отложенные задачи под капотом
Представьте: вы пишете приложение на Swift, и вам нужно выполнить кусок кода не сразу, а, скажем, через 3 секунды. Первое, что приходит в голову — dispatch_after. Но если вы заглянете в старые версии Swift или на Stack Overflow, то увидите, что этот метод изменился. А главное — мало кто понимает, что на самом деле происходит внутри Grand Central Dispatch (GCD), когда мы говорим: «Сделай это через 2 секунды».
В этой статье мы не просто покажем, как писать dispatch_after в современном Swift. Мы залезем под капот GCD, разберём очереди, потоки и таймеры, чтобы вы могли писать надёжный и эффективный асинхронный код. Поехали!
Что такое dispatch_after и почему он стал asyncAfter?
В старых версиях Swift (1.x, 2.x) вы могли встретить такой код:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(2 * NSEC_PER_SEC)), dispatch_get_main_queue()) {
print("Прошло 2 секунды")
}
Этот код работал, но выглядел громоздко. Начиная с Swift 3, Apple провела рефакторинг всего GCD API, сделав его более «свифтовым». Теперь тот же самый функционал выглядит так:
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
print("Прошло 2 секунды")
}
Обратите внимание: asyncAfter — это метод очереди (DispatchQueue), а не отдельная функция. Это ключевое изменение: теперь вы явно указываете, на какой очереди выполнять задачу.
Как GCD работает под капотом: очереди, потоки и таймеры
Чтобы понять dispatch_after (или asyncAfter), нужно разобраться в трёх сущностях:
- DispatchQueue — очередь задач. Может быть последовательной (serial) или параллельной (concurrent).
- DispatchWorkItem — задача, которую вы ставите в очередь. В нашем случае это замыкание.
- DispatchTime — момент времени, когда задача должна запуститься. GCD использует системный таймер для отслеживания дедлайнов.
Когда вы вызываете asyncAfter, GCD не создаёт новый поток. Вместо этого он:
- Создаёт таймер, который сработает в указанное время.
- Помещает задачу в специальную очередь отложенных задач (часто её называют dispatch timer).
- Когда таймер срабатывает, GCD перемещает задачу в обычную очередь
DispatchQueue, где она ждёт своего потока.
Если очередь, в которую вы ставите задачу, занята другими задачами, ваша отложенная задача может выполниться позже указанного дедлайна. Это важный момент: dispatch_after гарантирует, что задача не выполнится раньше дедлайна, но может выполниться позже.
Практический пример: dispatch_after для обновления UI
Самый частый сценарий — обновление интерфейса через некоторое время после действия пользователя. Допустим, вы показываете сообщение «Сохранено», а через 2 секунды убираете его:
func showTemporaryMessage() {
let messageLabel = UILabel()
messageLabel.text = "Сохранено!"
messageLabel.alpha = 1.0
view.addSubview(messageLabel)
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
UIView.animate(withDuration: 0.5) {
messageLabel.alpha = 0.0
} completion: { _ in
messageLabel.removeFromSuperview()
}
}
}
Обратите внимание: мы используем DispatchQueue.main, потому что все операции с UI должны выполняться на главном потоке. Если бы мы вызвали asyncAfter на фоновой очереди, а потом попытались изменить UI, приложение бы упало.
Подводные камни dispatch_after: утечки памяти и захват self
Одна из самых частых ошибок — сильная ссылка на self внутри замыкания. Представьте, что вы пишете:
class ViewController: UIViewController {
func scheduleTask() {
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.updateUI() // self захвачен сильно
}
}
}
Если пользователь уйдёт с экрана до того, как пройдёт 5 секунд, self (ViewController) не будет освобождён, потому что GCD держит на него сильную ссылку. Это приводит к утечке памяти.
Решение — использовать слабый захват:
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in
guard let self = self else { return }
self.updateUI()
}
Или, если вы используете Swift 5.5+ и iOS 13+, можно написать ещё короче:
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in
self?.updateUI()
}
Отмена отложенной задачи: DispatchWorkItem
Иногда нужно отменить задачу, которая ещё не выполнилась. Например, пользователь нажал кнопку «Отмена». Для этого используйте DispatchWorkItem:
var pendingTask: DispatchWorkItem?
func scheduleCancellableTask() {
let task = DispatchWorkItem { [weak self] in
self?.doSomething()
}
pendingTask = task
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: task)
}
func cancelTask() {
pendingTask?.cancel()
pendingTask = nil
}
Важно: cancel() не останавливает задачу, если она уже выполняется. Он только устанавливает флаг isCancelled. Внутри задачи вы должны сами проверять этот флаг:
let task = DispatchWorkItem {
guard !Task.current.isCancelled else { return }
// выполняем работу
}
dispatch_after и точность времени: когда «через 2 секунды» — это не ровно 2 секунды
GCD использует системный таймер, который имеет ограниченную точность. Обычно это около 1 миллисекунды, но если система сильно загружена, задержка может быть больше. Есть два типа дедлайнов:
- DispatchTime — абсолютное время. Задача выполнится как можно ближе к этому моменту.
- DispatchWallTime — привязано к настенным часам. Используется, когда важна точность относительно реального времени (например, будильник).
// DispatchTime — стандартный подход
let deadline = DispatchTime.now() + 2.0
// DispatchWallTime — точное время с учётом сна устройства
let wallDeadline = DispatchWallTime.now() + 2.0
DispatchQueue.global().asyncAfter(wallDeadline: wallDeadline) {
print("Это сработает через 2 секунды даже после сна")
}
Разница: если устройство уходит в сон, DispatchTime «заморозится» вместе с ним, а