Реальные примеры использования GCD в сложных сценариях
Grand Central Dispatch (GCD) — это мощный инструмент, но его настоящий потенциал раскрывается при решении сложных задач многопоточности. В этой статье мы рассмотрим практические примеры использования GCD в реальных проектах и сценариях, с которыми сталкиваются iOS-разработчики в повседневной работе.
Содержание:
Композиция изображений в реальном времени
Одна из распространенных задач — обработка и композиция изображений для фото-приложений. Вот пример реализации функциональности, подобной фильтрам в Instagram:
class PhotoProcessor {
private let processingQueue = DispatchQueue(label: "com.photoapp.processing", qos: .userInitiated, attributes: .concurrent)
private let renderingQueue = DispatchQueue(label: "com.photoapp.rendering", qos: .userInteractive)
func applyFilters(to image: UIImage, filters: [ImageFilter], completion: @escaping (UIImage?) -> Void) {
// Создаем группу для отслеживания завершения всех операций фильтрации
let group = DispatchGroup()
// Массив для хранения промежуточных результатов
var filteredImages = [Int: UIImage]()
let lockQueue = DispatchQueue(label: "com.photoapp.filterlock")
// Применяем каждый фильтр на processingQueue
for (index, filter) in filters.enumerated() {
group.enter()
processingQueue.async {
// Применяем фильтр
guard let filteredImage = filter.apply(to: image) else {
group.leave()
return
}
// Сохраняем результат с синхронизацией доступа
lockQueue.sync {
filteredImages[index] = filteredImage
}
group.leave()
}
}
// После завершения всех фильтров, компонуем их вместе
group.notify(queue: renderingQueue) {
// Проверяем, что все фильтры успешно применены
guard filteredImages.count == filters.count else {
completion(nil)
return
}
// Компонуем слои в правильном порядке
var compositedImage = image
for i in 0..<filters.count {
guard let layerImage = filteredImages[i] else { continue }
compositedImage = self.compositeImages(base: compositedImage, overlay: layerImage)
}
// Возвращаем результат в главный поток
DispatchQueue.main.async {
completion(compositedImage)
}
}
}
private func compositeImages(base: UIImage, overlay: UIImage) -> UIImage {
// Реализация наложения изображений
// ...
return base
}
}
Ключевые аспекты реализации:
- Использование отдельной очереди с высоким приоритетом для обработки
- Применение DispatchGroup для координации параллельных задач
- Защита доступа к общему состоянию с помощью отдельной lockQueue
- Компоновка результатов на высокоприоритетной очереди рендеринга
- Возврат готового результата в главный поток
Кэширование и асинхронная загрузка изображений
Реализация эффективного менеджера изображений с кэшированием — еще один распространенный сценарий:
class ImageManager {
static let shared = ImageManager()
private let downloadQueue = DispatchQueue(label: "com.imagemanager.download", qos: .utility, attributes: .concurrent)
private let processingQueue = DispatchQueue(label: "com.imagemanager.processing", qos: .userInitiated, attributes: .concurrent)
private let cachingQueue = DispatchQueue(label: "com.imagemanager.caching", qos: .background)
// Кэш загруженных изображений
private let imageCache = NSCache<NSString, UIImage>()
// Словарь для отслеживания активных загрузок, чтобы не дублировать запросы
private var activeDownloads = [URL: [DownloadCompletion]]()
private let downloadsLock = NSLock()
typealias DownloadCompletion = (UIImage?) -> Void
func downloadImage(from url: URL, completion: @escaping DownloadCompletion) {
// Проверяем кэш
if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) {
DispatchQueue.main.async {
completion(cachedImage)
}
return
}
// Проверяем, не загружается ли уже изображение
downloadsLock.lock()
if var callbacks = activeDownloads[url] {
// Добавляем новый completion в список и выходим
callbacks.append(completion)
activeDownloads[url] = callbacks
downloadsLock.unlock()
return
} else {
// Начинаем новую загрузку
activeDownloads[url] = [completion]
downloadsLock.unlock()
}
// Выполняем загрузку в фоновой очереди
downloadQueue.async {
do {
let data = try Data(contentsOf: url)
// Обработка изображения в очереди обработки
self.processingQueue.async {
guard let image = UIImage(data: data) else {
self.completeDownload(for: url, with: nil)
return
}
// Оптимизация изображения для отображения
let optimizedImage = self.optimizeForDisplay(image)
// Сохраняем в кэш
self.cachingQueue.async {
self.imageCache.setObject(optimizedImage, forKey: url.absoluteString as NSString)
}
// Уведомляем всех ожидающих
self.completeDownload(for: url, with: optimizedImage)
}
} catch {
self.completeDownload(for: url, with: nil)
}
}
}
private func completeDownload(for url: URL, with image: UIImage?) {
// Получаем список ожидающих колбэков
downloadsLock.lock()
guard let callbacks = activeDownloads[url] else {
downloadsLock.unlock()
return
}
// Удаляем из активных загрузок
activeDownloads.removeValue(forKey: url)
downloadsLock.unlock()
// Уведомляем всех в главном потоке
DispatchQueue.main.async {
for callback in callbacks {
callback(image)
}
}
}
private func optimizeForDisplay(_ image: UIImage) -> UIImage {
// Реализация оптимизации изображения
// Изменение размера, формата и т.д.
return image
}
}
Особенности этой реализации:
- Разделение задач по отдельным очередям с разными QoS
- Предотвращение дублирования загрузок одного и того же URL
- Многоуровневая обработка: загрузка → оптимизация → кэширование
- Синхронизация доступа к состоянию с помощью NSLock
Периодическое обновление данных с адаптивным интервалом
Бизнес-приложения часто требуют фонового обновления данных с различной периодичностью:
class DataSynchronizer {
private var timer: DispatchSourceTimer?
private let syncQueue = DispatchQueue(label: "com.app.datasync", qos: .utility)
private let activeSessionsSemaphore = DispatchSemaphore(value: 3) // Ограничиваем до 3 одновременных сессий
// Адаптивный интервал, который изменяется в зависимости от активности пользователя
private var syncInterval: TimeInterval = 300 // 5 минут по умолчанию
func startSyncProcess() {
stopSyncProcess() // Останавливаем, если уже запущен
timer = DispatchSource.makeTimerSource(queue: syncQueue)
// Настраиваем таймер с небольшим интервалом погрешности для экономии энергии
timer?.schedule(deadline: .now(), repeating: syncInterval, leeway: .seconds(5))
timer?.setEventHandler { [weak self] in
guard let self = self else { return }
// Проверяем, активно ли приложение для регулировки интервала
let isActive = self.isAppInForeground()
self.syncInterval = isActive ? 60 : 300 // 1 минута при активном использовании, иначе 5 минут
// Ожидаем доступности слота для синхронизации
self.activeSessionsSemaphore.wait()
// Выполняем синхронизацию
self.performSync { success in
// Освобождаем слот, независимо от результата
self.activeSessionsSemaphore.signal()
if !success {
// В случае ошибки, увеличиваем интервал с экспоненциальной задержкой
self.handleSyncFailure()
} else {
// Сбрасываем счетчик ошибок после успешной синхронизации
self.resetErrorCounter()
}
}
}
timer?.resume()
}
func stopSyncProcess() {
timer?.cancel()
timer = nil
}
private var failureCount = 0
private func handleSyncFailure() {
failureCount += 1
// Экспоненциальное увеличение интервала (но не более 30 минут)
let maxInterval: TimeInterval = 30 * 60
syncInterval = min(maxInterval, syncInterval * pow(2, Double(min(failureCount, 5))))
}
private func resetErrorCounter() {
failureCount = 0
}
private func performSync(completion: @escaping (Bool) -> Void) {
// Реализация фактической синхронизации данных
// ...
// Для примера:
DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(Int.random(in: 1...3))) {
let success = Bool.random()
completion(success)
}
}
private func isAppInForeground() -> Bool {
// Проверка активности приложения
return UIApplication.shared.applicationState == .active
}
}
Ключевые аспекты реализации:
- Использование DispatchSourceTimer для точного управления интервалами
- Семафор для ограничения количества одновременных сессий
- Адаптивное изменение интервала в зависимости от состояния приложения
- Экспоненциальная задержка при ошибках (backoff strategy)
Отложенный поиск при вводе текста
Реализация поиска с задержкой для предотвращения частых запросов при быстром вводе:
class DebouncedSearchController {
private let searchQueue = DispatchQueue(label: "com.app.search", qos: .userInitiated)
private var pendingWorkItem: DispatchWorkItem?
private var lastSearchTerm: String?
typealias SearchCompletion = ([SearchResult]) -> Void
func search(term: String, debounceTime: TimeInterval = 0.3, completion: @escaping SearchCompletion) {
// Отменяем предыдущую задачу, если она еще не началась
pendingWorkItem?.cancel()
// Если новый запрос такой же, как последний выполненный, пропускаем
if term == lastSearchTerm {
return
}
// Создаем новую задачу поиска
let workItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
// Запоминаем термин поиска
self.lastSearchTerm = term
// Выполняем поиск (в реальном приложении это может быть запрос к API или базе данных)
self.performSearch(term) { results in
// Возвращаем результаты в главном потоке
DispatchQueue.main.async {
completion(results)
}
}
}
// Сохраняем ссылку на задачу
pendingWorkItem = workItem
// Откладываем выполнение задачи
searchQueue.asyncAfter(deadline: .now() + debounceTime, execute: workItem)
}
private func performSearch(_ term: String, completion: @escaping SearchCompletion) {
// Имитация поискового запроса
// В реальном приложении здесь был бы запрос к API или базе данных
// Для демонстрации используем задержку
Thread.sleep(forTimeInterval: 0.5)
let results: [SearchResult] = [
SearchResult(title: "Результат 1 для (term)"),
SearchResult(title: "Результат 2 для (term)")
]
completion(results)
}
}
struct SearchResult {
let title: String
// Другие поля результата поиска
}
Использование в UISearchController:
class SearchViewController: UIViewController, UISearchBarDelegate {
private let searchController = UISearchController(searchResultsController: nil)
private let debouncedSearch = DebouncedSearchController()
private var searchResults: [SearchResult] = []
override func viewDidLoad() {
super.viewDidLoad()
setupSearchController()
}
private func setupSearchController() {
searchController.searchBar.delegate = self
navigationItem.searchController = searchController
}
// MARK: - UISearchBarDelegate
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
debouncedSearch.search(term: searchText) { [weak self] results in
guard let self = self else { return }
self.searchResults = results
self.tableView.reloadData()
}
}
}
Сложная цепочка зависимых задач с обработкой ошибок
Многие бизнес-процессы требуют последовательного выполнения зависимых операций с возможностью обработки ошибок на каждом этапе:
class ComplexWorkflow {
enum WorkflowError: Error {
case dataFetchFailed
case validationFailed
case processingFailed
case persistenceFailed
}
typealias WorkflowCompletion = (Result<WorkflowResult, Error>) -> Void
// Зависимые очереди для разных этапов процесса
private let fetchQueue = DispatchQueue(label: "com.workflow.fetch", qos: .userInitiated)
private let processingQueue = DispatchQueue(label: "com.workflow.processing", qos: .utility)
private let persistenceQueue = DispatchQueue(label: "com.workflow.persistence", qos: .background)
func executeWorkflow(input: WorkflowInput, completion: @escaping WorkflowCompletion) {
// Шаг 1: Загрузка данных
fetchQueue.async { [weak self] in
guard let self = self else { return }
do {
let data = try self.fetchData(input: input)
// Шаг 2: Валидация и обработка данных
self.processingQueue.async {
do {
let validatedData = try self.validateData(data)
let processedData = try self.processData(validatedData)
// Шаг 3: Сохранение результатов
self.persistenceQueue.async {
do {
let result = try self.persistResults(processedData)
// Успешное завершение всей цепочки
DispatchQueue.main.async {
completion(.success(result))
}
} catch {
// Ошибка на этапе сохранения
DispatchQueue.main.async {
completion(.failure(WorkflowError.persistenceFailed))
}
}
}
} catch let validationError as WorkflowError {
// Ошибка на этапе валидации
DispatchQueue.main.async {
completion(.failure(validationError))
}
} catch {
// Ошибка на этапе обработки
DispatchQueue.main.async {
completion(.failure(WorkflowError.processingFailed))
}
}
}
} catch {
// Ошибка на этапе загрузки данных
DispatchQueue.main.async {
completion(.failure(WorkflowError.dataFetchFailed))
}
}
}
}
// Реализация отдельных этапов рабочего процесса
private func fetchData(input: WorkflowInput) throws -> RawData {
// Симуляция загрузки данных
return RawData()
}
private func validateData(_ data: RawData) throws -> ValidatedData {
// Проверка данных
return ValidatedData()
}
private func processData(_ data: ValidatedData) throws -> ProcessedData {
// Обработка данных
return ProcessedData()
}
private func persistResults(_ data: ProcessedData) throws -> WorkflowResult {
// Сохранение результатов
return WorkflowResult()
}
}
// Вспомогательные структуры для примера
struct WorkflowInput {}
struct RawData {}
struct ValidatedData {}
struct ProcessedData {}
struct WorkflowResult {}
Улучшенная версия с использованием DispatchGroup:
func executeWorkflowImproved(input: WorkflowInput, completion: @escaping WorkflowCompletion) {
let workflowGroup = DispatchGroup()
var currentResult: Any? = input
var workflowError: Error?
// Шаг 1: Загрузка данных
workflowGroup.enter()
fetchQueue.async { [weak self] in
guard let self = self else {
workflowGroup.leave()
return
}
do {
let data = try self.fetchData(input: input)
currentResult = data
} catch {
workflowError = error
}
workflowGroup.leave()
}
// Шаг 2: Обработка данных
workflowGroup.notify(queue: processingQueue) { [weak self] in
guard let self = self, workflowError == nil, let data = currentResult as? RawData else {
workflowGroup.leave()
return
}
workflowGroup.enter()
do {
let validated = try self.validateData(data)
let processed = try self.processData(validated)
currentResult = processed
} catch {
workflowError = error
}
workflowGroup.leave()
}
// Шаг 3: Сохранение результатов
workflowGroup.notify(queue: persistenceQueue) { [weak self] in
guard let self = self, workflowError == nil, let data = currentResult as? ProcessedData else {
workflowGroup.leave()
return
}
workflowGroup.enter()
do {
let result = try self.persistResults(data)
currentResult = result
} catch {
workflowError = error
}
workflowGroup.leave()
}
// Финальное уведомление
workflowGroup.notify(queue: .main) {
if let error = workflowError {
completion(.failure(error))
} else if let result = currentResult as? WorkflowResult {
completion(.success(result))
} else {
completion(.failure(WorkflowError.processingFailed))
}
}
}
Не забудьте загрузить приложение iJun в AppStore и подписаться на мой YouTube канал для получения дополнительных материалов по iOS-разработке.