Если вы хотите добиться UI
отзывчивости вашего iOS
приложения, выполняя такие затратные по времени куски кода, как загрузка данных из сети или обработка изображений, то вам нужно использовать продвинутые паттерны, связанные с многопоточностью (сoncurrency
), иначе работа вашего пользовательского интерфейса (U
I) начнет сильно замедляться и даже может привести к полной его «заморозке». Вам нужно убрать ресурсо-затратные задачи с main thread
(главного потока), который отвечает за выполнение кода, отображающего ваш пользовательский интерфейс (UI
).
В текущей версии Swift 3
и ближайшей Swift 4
(осень 2017) это можно сделать двумя способами, которые пока не связаны с встроенными языковыми конструкциями Swift
, начало реализации которых будет только в Swift 5
(конец 2018).
Один из них использует GCD (Grand Central Dispatch)
и ему посвящена предыдущая статья. В этой статье мы покажем, как достичь отзывчивости UI
в iOS
приложениях с помощью таких абстрактных понятий, как операция Operation
и очередь операций OperationQueue
. Мы также покажем в чем различие этих двух подходов и какой из них в каких ситуациях лучше использовать.
Код для этой статьи можно посмотреть на Github.
Что такое Operation
? Хорошее определение Operation
дано в NSHipster:
Operation
представляет собой законченную задачу и является абстрактным классом, который предоставляет вам потока-безопасную структуру для моделирования состояния операции, ее приоритета, зависимостей (dependencies
) от другихOperations
и управления этой операцией.
Основные понятия. Operation
Простейшая операция Operation
может быть представлена обычным замыканием, которое также может выполняться и на DispatchQueue
. Но эту форму операции можно применять только при условии, что вы будете добавлять ее на OperationQueue
с помощью метода addOperation
:
Полноценная операция Operation
может быть сконструирована с помощью BlockOperation
инициализатора. Она запускается на выполнение с помощью своего собственного метода start ()
:
Если вы хотите получить что-то повторно используемое типа асинхронной версии синхронной функции, вам необходимо создать пользовательский subclass
класса Operation
и получить его экземпляр:
Операция FilterOperation
получения размытого изображения с помощью соответствующего фильтра определена как пользовательский subclass
класса Operation
. Вы видите, что у пользовательского класса могут быть как входные, так и выходные свойства, а также другие вспомогательные функции. Для размещения функциональной части операции мы переопределили (override
) метод main ()
.
Класс Operation
позволяет вам создать некоторую задачу, которую вы в будущем можете запустить на очереди операций OperationQueue
, а пока она может ожидать выполнения других Operations
.
У Operation
есть машина состояний (state mashine
), которая представляет собой «жизненный цикл» операции Operation
:
Возможные состояния операции Operation
: pending
(отложенная), ready
(готова к выполнению), executing
(выполняется), finished
(закончена) и cancelled
(уничтожена).
Когда вы создаете операцию Operation
и размещаете ее на OperationQueue
, то устанавливаете операцию в состояние pending
(отложенная). Спустя некоторое время она принимает состояние ready
(готова к выполнению), и в любой момент может быть отправлена на OperationQueue
для выполнение, перейдя в состояние executing
(выполняется), которое может длится от миллисекунд до нескольких минут или дольше. После завершения операция Operation
переходит в финальное состояние finished
(закончена). В любой точке этого простого «жизненного» цикла операция Operation
может быть уничтожена и перейдет в состояние cancelled
(уничтожена).
API
класса Operation
отражает этот «жизненный цикл» операции и представлен ниже:
Мы можем запустить операцию Operation
на выполнение с помощью метода start()
, но чаще всего мы будем добавлять операцию на очередь операций OperationQueue
, и эта очередь автоматически будет запускать операцию. При этом надо помнить, что отдельная операция Operation
, запущенная с помощью start()
, выполняется СИНХРОННО на текущем потоке. Для того, чтобы ее запустить за пределами текущего потока нужно воспользоваться либо OperationQueue
, либо DispatchQueue
.
Текущее состояние операции Operation
в любой точке приложения можно отслеживать с помощью булевских свойств : isReady
, isExecuting
, isFinished
, isCancelled
c помощью механизмов KVO (key-value observation
), так как сама операция может выполняться на любом потоке, а информация может нам понадобиться скорее всего на главном потоке (main thread
) или на любом другом потоке, отличным от того, на котором выполняется сама операция.
Если мы хотим добавить функциональности операции Operation
, мы должны создать subclass
Operation
. В простейшем случае в этом subclass
нам нужно переопределить метод main()
класса Operation
. Сам класс Operation
автоматически управляет изменением состояния операции, но в более сложных случаях, представленных ниже, нам придется это делать вручную.
Мы можем снабдить операцию завершающим замыкание completionBlock
, которое выполняется после завершения операции, а также «качеством обслуживания» qualityOfService
, которое влияет на приоритет выполнения операции на OperationQueue
.
Как мы видим, класс Operation
имеет метод cancel()
, однако использование этого метода только устанавливает свойство isCancelled
в true
, а что семантически означает «удаление» операции можно определить только при создании subclass
Operation
. Например, в случае загрузки данных из сети можно определить cancel()
как отключение операции от сетевого взаимодействия.
Основные понятия. OperationQueue
Вместо того, чтобы самостоятельно запускать операции, мы будем управлять ими с помощью очереди операций OperationQueue
. Очередь операций OperationQueue
можно рассматривать как высоко-приоритетную «обертку» DispatchQueue
, наделенную дополнительной функциональностью: возможностью уничтожения выполняемых операций, выполнения зависимых операций и т.д.
Давайте посмотрим на API
класса OperationQueue
:
Здесь мы видим простейший инициализатор очереди операций OperationQueue ()
и два свойства класса: current
и main
, задающие текущую очередь операций OperationQueue.current
и main queue
- OperationQueue.main
, которую используют для обновления пользовательского интерфейса (UI
) аналогично DispatchQueue.main
в GCD
. Очень важное свойство maxConcurrentOperationCount
задает количество одновременно выполняемых операции на этой очереди и, задавая его равным 1, мы устанавливаем последовательную (serial
) очередь операций.
По умолчанию значение свойства maxConcurrentOperationCount
устанавливается равным Default
, что означает максимально возможное число одновременно выполняемых операций:
Вы можете непосредственно добавить на очередь OperationQueue
операцию Operation
(или любой ее subclass
), замыкание или целый массив операций с возможностью блокировки текущего потока до момента полного завершения всего массива операций.
Очередь операций OperationQueue
выполняет размещенные на ней операции согласно их приоритету qualityOfService
, «готовности» (свойство isReady
установлено в true
) и зависимостям ( dependencies
) от других операций. Если все эти характеристики равны, то операции отправляются на «выполнение» в том порядке, в котором они были поставлены в очередь. Если какая-то операция размещена в какой-то очереди операций, то она не может быть поставлена еще раз в любую из этих очередей. Если операция была выполнена, она не может быть выполнена повторно ни на какой из очередей операции, операция — одноразовая вещь, поэтому имеет смысл создавать subclasses
класса Operation
и использовать их, если необходимо, для повторного получения экземпляра этой операции.
Вы можете послать сообщение cancel()
всем находящимся в очереди операциям с помощью метода cancellAllOperations ()
, например, если приложение «уходит» в фоновый (background
) режим. С помощью метода waitUntilAllOperationsAreFinished()
вы можете блокировать текущий поток до тех пор, пока не будут завершены все операции на этой очереди операций. Но НИКОГДА НЕ делайте этого на main queue
. Если вам действительно нужно что-то сделать только после завершения всех операций, то создайте private
последовательную очередь операций (serial queue
) и ожидайте там завершения ваших операций.
Очередь операций OperationQueue
ведет себя как DispatchGroup
. Вы можете добавлять на OperationQueue
операции с различными qualityOfService
, и они будут запускаться в соответствии с их приоритетом. Вы можете также установить qualityOfService
на более высоком уровне — для очереди операций в целом, но это значение будет передопределено значением qualityOfService
для отдельной операции.
По умолчанию qualityOfService
для OperationQueue
-это .background
.
Вы можете также остановить выполнение операций на OperationQueue
путем задания свойства isSuspended
в true
. Выполняемые операции на этой очереди будут продолжаться, но вновь добавляемые не будут отправляться на выполнение до тех пор, пока вы не измените значение свойства isSuspended
на false
. По умолчанию значение свойства isSuspended
- false
.
Давайте проведем некоторые эксперименты с операциями Operation
и очередью операций OperationQueue
на Playground
.
Эксперимент 1. Создание OperationQueue
и добавление замыканий
Код можно посмотреть на OperationQueue.playground
на Github.
Создаем пустую очередь printerQueue
:
Добавляем операции в виде замыканий на очередь printerQueue
:
Операции стартуют асинхронно по отношению к текущему потоку, как только мы добавили их на printerQueue
, и они находятся в состоянии ready
. Время выполнения всех операций оцениваем с помощью метода waitUntilAllOperationsAreFinished()
, который «синхронно» по отношению к текущему потоку ждет окончания операций. На main queue
в приложении лучше этого не делать, но на нашей Playground
ничего не происходит с U
I и мы можем себе это позволить. Общее время выполнения всех 7 операций составляет чуть больше 2 секунд и соответствует времени выполнения оператора sleep(2)
, следовательно, printerQueue
запускает все 7 операции одновременно на многих потоках.
Давайте изменим свойство очереди printerQueue
, и установим свойство maxConcurrentOperationCount
в 2:
Мы видим, что требуется очень короткое время, чтобы поместить все операции на printerQueue
, и чуть больше 8 секунд для выполнения всех операций, так как они стартуют парами и последняя 7-ая операция стартует одна в четвертой «паре».
Теперь добавим еще одну операцию concatenationOperation
с повышенным qualityOfService
, равным .userInitiated
:
Мы видим, что при первой же возможности операция с более высоким приоритетом выполняется раньше остальных, при этом общее время выполнения всех операций практически не меняется — чуть более 8 секунд.
Давайте превратим очередь printerQueue
в последовательную (serial
), установив свойство maxConcurrentOperationCount
в 1:
Мы видим, что в этом случае операции выполняются последовательно, одна за другой, приоритеты не работают и общее время выполнения всех операций возрастает до 16 секунд.
Рассмотрим более сложный случай, когда для получения массива отфильтрованных исходных изображений
применяется пользовательская операция фильтрации FilterOperation
, знакомая нам из предыдущего раздела:
Создадим очередь filterQueue
для выполнения операций фильтрации и последовательную (serial
) очередь appendQueue
для потоко-безопасного добавления отфильтрованного изображения к массиву. Дело в том, что множество операций фильтрации будут одновременно обращаться к разделяемому (shared
) ресурсу — массиву filteredImages
- для добавления своего элемента. Помните? Мы использовали последовательную (serial
) DispatchQueue
очередь для изменения разделяемых (shared
) ресурсов? Здесь то же самое, но будет выполняться для Operation
. Конечно, последовательная (serial
) DispatchQueue
очередь будет более эффективна, но мы покажем создание и использование последовательной (serial
) OperatioQueue
очереди.
Swift
массивы являются value type
и копируются при записи (copy on write
), поэтому вам не стоит беспокоиться о многопоточных изменениях, но в форумах есть сообщения о том, есть с этим проблемы, особенно, если массив является свойством объекта типа class. Поэтому мы показываем способ сохранить многопоточную безопасность (thread safe
) массива при добавлении в него новых элементов в разных потоках.
Затем создаем операцию фильтрации для каждого изображения и добавляем ее на filterQueue
для асинхронного выполнения. После того, как фильтрация выполнена, мы добавляем полученное изображение к массиву filteredImages
и делаем это в completionBlock
, где используем другую очередь операций — appendQueue
. У completionBlock
нет входных параметров и он ничего не возвращает.
Дожидаемся выполнения всех операций фильтрации и проверяем отфильтрованный массив изображений:
Время выполнения всех операций 1.19 секунды сопоставимо со временем выполнения одной операции (см. предыдущий раздел), то есть имеет место многопоточное выполнение операций на очереди filterQueue
. На Playground
мы видим массив отфильтрованных изображений, но они очень маленькие, чтобы увидеть эффект фильтрации. Мы можем кликнуть на кнопку быстрого просмотра и увидеть эффект фильтрации:
Асинхронная операция
Код можно посмотреть на AsyncOperations.playground
на Github.
До сих пор мы использовали операции для СИНХРОННЫХ заданий, то есть функций, которые используют текущий поток и не возвращаются до тех пор, пока не выполнят свое задание целиком. АСИНХРОННЫЕ задания (функции) ведут себя совершенно по-другому: они немедленно возвращают управление на текущем потоке, а выполняют свое задание на другом потоке, и дают вам знать, что задание выполнено, вызывая замыкание completionHandler
на другом потоке. Классическим примером является URLSession
:
Мы можем «завернуть» функциональность URLSession
в операцию Operation
, но нам придется вручную управлять состояниями операции.
Для создания пользовательских операций для СИНХРОННЫХ функций нам нужно было только переопределить метод операции main()
. Если мы точно также поступим с АСИНХРОННОЙ функцией, то в main()
она немедленно вернет управление текущему потоку и «уйдет» работать на другой поток, а мы окажемся в конце метода main()
и OperationQueue
тут же «выкинет» нашу операцию из очереди, так и не завершив нашу АСИНХРОННУЮ функцию. Такова логика работы OperationQueue
.
У АСИНХРОННОЙ ОПЕРАЦИИ совсем другая логика работы .
Если операция «готова» ( isReady = true
), операционная очередь OperationQueue
вызывает метод start()
, в котором мы должны установить операцию в состояние «выполняется» (isExecuting = true
) и вызвать метод main()
, который в свою очередь вызовет АСИНХРОННУЮ функцию. АСИНХРОННАЯ функция что-то выполняет на ДРУГОМ потоке, но свойство isExecuting
должно оставаться равным true
, даже если она ничего не выполняет на ТЕКУЩЕМ потоке, а только представляет задание, которое выполняется на другом потоке. Когда АСИНХРОННАЯ функция вызывает completionHandler
, что свидетельствует об окончании АСИНХРОННОЙ функции, мы должны установить в completionHandler
свойство isFinished
в true
, а свойство isExecuting
- в false
.
Следовательно, для АСИНХРОННОЙ операции нам придется переопределить (override
) больше, чем просто метод main()
. Нам нужно переопределить следующие методы и свойства:
Давайте создадим абстрактный пользовательский класс AsyncOperaton
, наследуемый от Operation
и пригодный для выполнения любой АСИНХРОННОЙ операции. Его абстрактность заключается в том, что у него не будет метода main()
для выполнения асинхронной операции. Вот его схема:
Если вы будете использовать АСИНХРОННУЮ операцию самостоятельно, без OperationQueue
, то вам нужно переопределить свойство isAsynchronous
и вернуть true
. Нам нужно переопределить метод start()
, чтобы реально стартовать АСИНХРОННУЮ функцию и сохранить свойство isExecuting
равным true
. Нам нужно также научиться управлять свойствами, определяющими состояние операции: isReady
, isExecuting
, isFinished
. Эти свойства использует очередь операций OperationQueue
для отслеживания состояний операций и организации выполнения зависимых операций.
Когда вы определяете зависимости (dependencies
) между операциями, то это означает, что одна операция должна закончиться прежде, чем другая операция начнется, поэтому очереди операций OperationQueue
очень важно знать, когда операция заканчивается. Для СИНХРОННОЙ операции это не является проблемой, потому что СИНХРОННАЯ операция заканчивается, когда заканчивается СИНХРОННАЯ функция. Но АСИНХРОННАЯ функция заканчивается за пределами текущего потока, поэтому нам нужен какой-то способ, чтобы сказать очереди операций OperationQueue
о действительном окончании АСИНХРОННОЙ функции. Если вспомним GCD
, то при добавлении АСИНХРОННОЙ функции в группу, мы четко обозначали с помощью методов enter()
и leave()
начало и конец АСИНХРОННОЙ функции. Но в случае операции Operation
ситуация гораздо сложнее, так как у операции есть состояния: isReady
, isExecuting
, isFinished
, isCancelled
и т.д.. При добавлении АСИНХРОННОЙ функции в операцию Operation
, мы должны управлять этими состояниями вручную. Для того, чтобы облегчить эту работу, мы и создали специальный абстрактный пользовательский subclass
класса Operation
с имеем AsyncOperaton
, главной задачей которого является управление изменением состояния операции. Для своей собственной АСИНХРОННОЙ функции вы создадите subclass
класса AsyncOperaton
, определив там только main ()
, из которого вызовите свою АСИНХРОННОЙ функции. И уже эту новую операцию будете добавлять на OperatonQueue
.
Но проблема заключается в том, что я не могу написать, например, isExecuting = true
, потому что все свойства, связанные с состоянием операции: isReady
, isExecuting
, isFinished
, являются readonly
({get}
), и мы не можем устанавливать их на прямую. Мы можем только сделать что-то, что заставит свойство isExecuting
вернуть true
, сообщив при этом системе, что состояние АСИНХРОННОЙ операции AsyncOperaton
изменилось.
Класс Operation
использует KVO (key-value observation
) механизм и методы willChangeValueForKe
и didChangeValueForKey
для уведомления об изменении свойств состояния типа isReady
, isExecuting
, isFinished
.
Поэтому для удобства управления состояниями операции мы создадим новый тип данных — собственное перечисление enum State
для представления состояний АСИНХРОННОЙ операции собственными вариантами: ready
, executing
, finished
.
Перечисление State
содержит также fileprivate
свойство с именем keyPath
, которое мы будем использовать в качестве переключателя для KVO уведомлений. Свойство keyPath
, является вычисляемым и складывается из строки "is"
, соединенной с rawValue
, которым является наименование элемента перечисления State
с заглавной буквы.
Затем мы определяем в абстрактном классе AsyncOperaton
переменную var state
типа State
для представления текущего состояния операции, по умолчанию это значение равно ready
. При каждом изменении переменной state
нам необходимо переключать KVO уведомления. Мы будем это делать с помощью Наблюдателей willSet {}
и didSet {}
свойства state
:
Перед тем, как переключить состояние операции state
, например, с executing
на finished
, нам нужно послать KVO уведомления willChangeValue
о предстоящем изменении обоих операций: нового состояния newValue
(finished
) и текущего состояния state
(executing
). После того, как переключения состояния state
произошло, мы посылаем KVO уведомления didChangeValue
для keyPath
обоих состояний: oldValue
( executing
) и state
(finished
).
Это заставит систему прочитать новые значения «родных» переменных состояния операции isReady
, isExecuting
, isFinished
. Поэтому в АСИНХРОННОЙ операции AsyncOperation
нам нужно переопределить «родные» переменные состояния операции isReady
, isExecuting
, isFinished
с использованием нового свойства state
как вычисляемые переменные, возвращающие правильные значения. Мы сделаем это в расширении extension
класса AsyncOperation
:
В некоторый момент времени свойство операцииisReady
становится равным true
, и мы должны использовать свойствоisReady
для superclass
, которое «воспринимает» зависимости (dependencies
) от других операций. Комбинируя нашу собственную логику со свойством isReady
для superclass
, мы можем быть уверены, что операция действительно «готова». Заметьте, что если переменная state
равна .finished
, то свойство superclass
isFinished
равно true
, а свойство isExecuting
— false
. Мы переопределим также свойство isAsynchronous
, вернув true
, и два метода: start()
и cancell()
.
В методе start()
мы проверяем, уничтожена ли операция, то есть свойство isCancelled
равно true
. Если это так, то мы должны установить новое значение для переменной state
— .finished
. Если операция не уничтожена, мы вызываем main()
. Помните, что в main()
находится АСИНХРОННАЯ функция, и она немедленно возвращает управление, так что нам следует вручную установить состояние операции stat
равное .executing
. Когда АСИНХРОННАЯ функция вернет completionHanller
, в нем мы должны установить состояние операции .finished
. Очень важно помнить, что в случае АСИНХРОННАЯ функции мы не можем использовать в методе start()
вызов аналогичного метода для superclass
— super.start()
, так как это будет означать синхронный запуск функции main()
, а нам нужно совершенно противоположное.
В методе cancell()
мы также должны установить состояние операции .finished
.
В результате мы получили абстрактный класс AsyncOperation
, и можем использовать его для своих собственных АСИНХРОННЫХ операций. Для этого необходимо осуществить следующий порядок действий:
В качестве примера возьмем функцию асинхронного медленного (внутри есть sleep (1)
) сложения двух чисел:
Используя AsyncOperation
в качестве superclass
, мы должны переопределить main ()
и не забыть установить состояние операции state
в .finished
в callback
:
callback
возвращает результат result
, который мы присваиваем свойству операции self.result
и устанавливаем состояние операции state
в .finished
, что информирует очередь операций о завершении асинхронного сложения чисел и о том, что больше не нужно работать с этой операцией.
Давайте используем нашу SumOperation
для получения массива суммы пар чисел:
Для каждой пары чисел мы создаем операцию SumOperation
и размещаем ее в очереди операций additionQueue
обычным способом, Мы видим, что порядок выполнения асинхронных операций немного отличается от порядка пар чисел в массиве. Это говорит о многопоточном выполнении наших асинхронных операций.
Второй пример связан с асинхронной загрузкой изображения по заданному URL
с помощью URLSession
:
Создаем АСИНХРОННУЮ операция ImageLoadOperation
, которая, как и в прошлый раз, является subclass класса AsyncOperation
. Для операции ImageLoadOperation
, как и в прошлый раз, переопределяем main ()
и не забываем установить состояние операции state
в .finished
в completion
:
Создаем операцию operationLoad
, получаем изображение operationLoad.outputImage
и отображаем на view
:
Код можно посмотреть на AsyncOperations.playground
на Github.
Зависимости (dependencies
)
Код можно посмотреть на LoadAndFilter.playground на Github.
В этом разделе мы рассмотрим как результат одной операции передать в другую операцию, при этом заставив вторую операцию начаться только тогда, когда закончится первая.
На рисунке вы видите две операции: первая загружает изображение из сети, а вторая осуществляет «затуманивание» верхней и нижней частей изображения — фильтрацию.
Обе эти операции очень хорошо работают друг с другом: «загрузчик» изображения передает напрямую свои данные «фильтру».
Мы могли бы создать одну операцию, которая включает обе эти операции, но это не очень гибкий дизайн.
Желательно иметь некоторую модульность операций при работе с изображениями, чтобы использовать их в любом порядке в различных местах приложения.
Более гибкое решение связано с выполнением «цепочки» операций с передачей изображения из первой операции во вторую. Мы можем определить, что «фильтр» зависит от «загрузчика», и тогда очередь операций OperationQueue
будет знать, что «фильтр» устанавливается в состояние ready
только после окончания работы «загрузчика». Это действительно замечательная «способность» очереди операций OperationQueue
. Вы можете создать очень сложный граф «зависимостей» и тем самым заставить OperationQueue
осуществлять автоматически запуск операций так, как вам нужно. API
класса Operation
, поддерживающего работу с «зависимостями» (dependencies
) - очень простое, но обнаруживает при работе фантастическую мощность.
Вы можете добавлять и убирать «зависимость» (dependency
), а также получать список «зависимостей» dependencies
, добавленных для этой операции. Ниже мы будем использовать список dependencies
для получения входного изображения зависимой операции фильтрации.
Когда вы создаете зависимости, то есть большая вероятность получить deadlock (взаимную блокировку):
Появление замкнутых циклов в графе «зависимостей» ведет к возникновению deadlock и нет универсального способа их устранения, кроме как выявления их путем визуального анализа.
Следующая проблема, связанная с «зависимостями» операций состоит в том, как передавать данные по «цепочке зависимостей»? Например, в рассмотренном выше примере, когда вначале работает «загрузчик», а затем «фильтр», для которого входным изображением является выходное изображение «загрузчика»:
Как нам этого добиться? Создаем протокол ImagePass
, который будет поставлять нам нужные данные, в нашем случае UIImage?
:
Загрузчик" — уже знакомый нам класс ImageLoadOperation
операции загрузки изображения из сети. На входе у него задается URL
изображения в виде строки urlString
, а на выходе — само изображение outputImage
.
Класс ImageLoadOperation
«подтверждает» протокол ImagePass
и возвращает в качестве свойства протокола image
выходное загруженное изображение outputImage
.
В свою очередь операция «фильтрации» — класс FilterOperation
- в случае отсутствия входное изображение _inputImage
анализирует свои «зависимости» dependencies
и интересуется только теми, которые «подтвердили» протокол ImagePass
. Он выбирает первую же такую зависимую операцию и извлекает оттуда свой inputImage
:
У «фильтра» на входе — входное изображение inputImage
, а на выходе — отфильтрованное с помощью функции filterImage (image:)
изображение outputImage
. Это обычная синхронная операция, поэтому нам нужно только переопределись main()
.
Мы хотим заставить эти две операции работать вместе, так, чтобы «фильтр» использовал в качестве входного изображение inputImage
выходное изображение операции «загрузка». Для этого мы инициализируем операцию фильтрации filter
со значение nil
:
Код находится на LoadAndFilter.playground
на Github.
Уничтожение операций на OperationQueue
Код находится на Cancellation.playground
на Github.
Мы рассмотрим еще одну замечательную возможность очереди OperationQueue
— возможность уничтожения операций.
После того, как вы разместили свою операцию Operation
в очереди OperationQueue
, у вас нет способа влить на ее выполнение, так как у очереди операций свой план запуска операций и она полностью управляет вашей операцией. Но у вас есть возможность уничтожить Operation
, используя метод cancel()
.
Вы можете подумать, что вызов метода cancel()
мгновенно приведет к остановке операции, но это не так. Метод cancel()
только устанавливает свойство isCancelled
операции в true
. Если операция еще не стартовала, то по умолчанию метод start()
не позволит операции выполняться и установит ее свойство isFinished
в true
. Если вы переопределяете (override
) метод start()
, то вы должны сохранить способность вашего start()
предотвращать запуск операции, если свойство операции isCancelled
установлено в true
. И если вы посмотрите на абстрактный класс AsyncOperation
, то увидите, что мы именно так и сделали.
Далее в методе main()
операции, особенно перед тем, как выполнять что-то медленное или ресурсов-затратное, нужно тестировать свойство isCancelled
на предмет того, а не уничтожена ли уже операция. И если операция уничтожена и это показывает значение true
свойства isCancelled
, то вы должны САМИ провести необходимые действия по остановке операции. Если операция Operation
выполняется локально, например, преобразование изображения, то вы можете остановить операцию. Если операция связана с обращением в сеть, как например, загрузка изображения с сервера, то вы не можете остановить такую операцию до тех пор, пока сервер не вернет вам результат.
Вам нужно добавить «логику» между различными «шагами» операции по проверке того, а стоит ли продолжать выполнять эту операцию или установить операцию в состояние isFinished
равно true
.
API
класса Operation
, предназначенное для удаления операции очень простое и состоит всего из двух позиций:
Вы вызываете метод cancel()
— он устанавливает свойство isCancelled
операции в true
. Важно отметить, что когда вызывается метод cancel()
, то свойства isExecuting
и isFinished
также изменяются на false
и true
соответственно.
Для операции совершенно нормально быть уничтоженной (isCancelled = true
) и не закончится (isFinished = true
). Свойство isCancelled
сообщает операции, что она должна остановиться, а свойство isFinished
сообщает системе, что операция уже остановлена.
Наша абстрактная АСИНХРОННАЯ операция AsyncOperation
переопределяет метод cancel()
таким образом, что устанавливает состояние операции state
в .finished
, а такое изменение приводит к изменению свойств операции isFinished
и isExecuting
:
Очередь OperationQueue
может уничтожить все операции:
На примере некоторых пользовательских операций давайте посмотрим, как можно добиться правильной реакции операции на вызов метода cancel()
.
Код находится на Cancellation.playground
на Github.
Операция ArraySumOperation
имеет на входе массив inputArray
кортежей, состоящих из пары целых чисел и формирует массив outputArray
их сумм на выходе:
Для каждой пары чисел мы используем функцию «медленного» сложения slowAdd
, представленную в папке Source
на Cancellation.playground, и добавляем в выходной массив outputArray
.
Задаем входной массив чисел, формируем операцию sumOperation
, добавляем ее в очередь операций queue
и запускаем таймер, который позволит нам в дальнейшем регулировать время, спустя которое мы сможем проверить реакцию операции sumOperation
на вызов метода cancel()
. Кроме того, у операции есть completionBlock
, в котором мы останавливаем таймер, показываем на Playground
outputArray
и завершаем работу на Playground
:
Итак, на выполнение операции sumOperation
уходит чуть более 5 секунд. Теперь попытаем уничтожить эту операцию, спустя 2 секунды после начала, вызвав метод cancel ()
:
Мы получили неожиданный результат — операция sumOperation
выполнилась полностью, никакого уничтожения операции не произошло. В чем же дело? А дело в том, что метод cancel ()
только устанавливает свойство isCancelled
в true
, а действия, необходимые для удаления операции ложатся на самого разработчика операции. Мы должны отреагировать на то, что свойство isCancelled
установилось в true
. Мы будем в цикле перед каждым добавлением суммы в выходной массив проверять, а не уничтожена ли операция. И если уничтожена, то мы прерываем цикл:
Давайте повторно запустим Playground
:
Мы остановились немного позже, чем через 2 секунды и успели получить 2 суммы, а когда собирались получить третью сумму, то получили сигнал об уничтожении операции и остановили дальнейшее получение сумм. На этом примере наглядно видно, как получить реакцию пользовательской операции на команду cancel ()
.
Давайте рассмотрим еще одну операцию AnotherArraySumOperation
, которая отличается тем, что используется другая функция slowAddArray
для получения выходного массива цикл по массиву кортежей:
Отличие от предыдущего случая заключается в том, что цикл по элементам массива кортежей находится не в методе main()
операции, а в другой функции и нам затруднительно прервать цикл, если операция будет уничтожена. Но такая возможность есть, хотя и очень изощренная:
На входе функции slowAddArray
массив input
пар целых чисел, кроме того у нее есть аргумент progress
, представляющий собой Optional
функцию, забирающую в качестве аргумента изменяющуюся глубину обработки массива
Double(results.count) / Double(input.count
и возвращающую Bool
. Этот Bool
и определяет продолжение обработки массива.
В методе main()
операции AnotherArraySumOperation
(предыдущий рисунок) мы передали функции slowAddArray
массив inputArray
, а аргумент progress
оформили в виде «хвостового» замыкания, в котором использовали свойство progress
для печати. Свойство progress
является Double
, так что мы умножили его на 100 и получили % окончания обработки массива и завершения операции. Затем мы возвращаем реакцию на уничтожение операции, что является сигналом на продолжение или прерывание обработки массива. Реакция представляет собой инверсию свойства isCancelled
.
Заменим предыдущую операцию SumOperation
на новую операцию AnotherArraySumOperation
:
Прервав операцию через 2 секунды, мы получили тот же результат — нам удалось обработать только 2 элемент массива из 5-ти, то есть 40%, перед тем, как операция была уничтожена.
Установим задержку прерывания операции 4 секунды:
Обработано 4 элемента массива из 5-ти, то есть 80%, перед тем, как операция была уничтожена.
Очень важно убедиться, что индивидуальные операции реагируют на свойство isCancelled
и, следовательно, могут быть уничтожены.
Но в дополнение к уничтожению отдельных операций с помощью метода cancel ()
, вы можете уничтожить все стартовавшие операции на очереди операций OprationQueue
с помощью метода cancellAllOperations
. Это особенно полезно, если у вас есть набор операций, работающих на единую цель. Эта цель может заключаться в параллельном запуске множества независимые операции или представлять собой граф зависимых операций, исполняемых одна за другой. Рассмотрим оба этих случая.
Паттерн 1. Работа с группой независимых операций
Код находится на CancellationGroup.playground
на Github.
Поставим задачу достигнуть того же результата, что и операция ArraySumOperation
, представленная в предыдущем разделе. Эта операция берет массив кортежей (Int, Int)
, и, используя функцию медленного сложения slowAdd()
, создает массив сумм чисел, составляющих кортеж. Цикл по составляющим входного массива скрыт внутри ArraySumOperation
. Давайте создадим группу отдельных очень простых операций типа SumOperation
. Операция SumOperation
складывает два числа из входной пары inputPair
с помощью функции медленного сложения slowAdd()
и возвращает результат output
:
Создадим самый обычный класс GroupAdd
, который управляет private
очередью операций queue
и множеством операций SumOperation
для того, чтобы посчитать сумму всех пар во входном массиве и разместить в выходном массиве outputArray
кортежи (Int, Int, Int )
, состоящие из исходных данных и результата:
При инициализации экземпляра класса GroupAdd
задается входный массив input
пар чисел, из которых формируются операции типа SumOperation
. В completionBlock
каждой операции производится добавление результата в выходной массив outputArray
, которое выполняется на отдельной private
последовательной очереди операций appendOperation
, чтобы избежать race condition.
Класс имеет все присущие операции Operation
методы: start()
, cancel ()
, wait ()
, поэтому мы вправе рассматривать его как «комплексную операцию».
Создаем экземпляр класса GroupAdd
, подавая на вход массив пар чисел:
Стартуем groupAdd
, ожидаем 1 секунду и используем метод cancel ()
для удаления всех операций суммирования из очереди операций. В результате после завершения всех операций (используем wait()
, который НЕЛЬЗЯ использовать на main queue
, но можно на Playground
), получаем укороченный выходной массив:
Результат можно посмотреть на Playground
CancelletionGroup.playground
на Github.
Паттерн 2. Работаем с группой зависимых операций
Код находится на CancellationFourImages.playground
на Github.
В качестве группы зависимых операций рассмотрим уже знакомую нам группу взаимосвязанных операций по загрузке изображения из сети, его фильтрации и модификации UI
. Попробуем оформить эту последовательность в отдельный класс ImageProvider
, который будет управлять этими операциями на OperationQueue
с помощью методов start ()
, wait ()
и cancel ()
.
У нас будет две абстрактные операции (то есть те, у которых нет реализации метода main()
). Одна — это уже знакомая нам АСИНХРОННАЯ операция AsyncOperation
, а другая — операция ImageTakeOperation
извлечения входное изображение inputImage
из зависимостей dependecies
.
На основе AsyncOperation
создадим операцию загрузки изображения из «сети» по заданному URL адресу:
Эта операция подтверждает протокол ImagePass
для передачи полученного изображения outputImage далее по цепочке операций.
Абстрактная операция ImageTakeOperation
извлекает входное изображение inputImage
из зависимостей dependecies
, если оно не задано при инициализации этой операции, и позволяет «забрать» выходное изображение с помощью уже знакомого нам протокола ImagePass
, используемого для передачи изображений в цепочке последовательных операций:
Абстрактный класс ImageTakeOperation
очень удобно использовать в качестве superclass
для создания операции, участвующей в цепочке зависимых операций. Например, для операции фильтрации Filter
:
Или для операции «состаривания» изображения в стиле «хипстер» PostProcessImageOperation
:
Или для операции «выбрасывания» входного изображения во внешнюю среду с помощью замыкания ImageOutputOperation
:
Теперь займемся классом ImageProvider
. Создадим самый обычный класс ImageProvider
, который управляет private
очередью операций operationQueue
и последовательностью операций dataLoad
, filter
и output
для того, чтобы загрузить изображение по заданному URL, отфильтровать его и передать в замыкание completion
:
Класс ImageProvider
имеет все присущие операции Operation
методы: start()
, cancel ()
, wait ()
, поэтому мы вправе рассматривать его как «комплексную операцию».
Создаем 4 экземпляра класса ImageProvider
:
Стартуем загрузку изображений:
Ждем завершения операций и получаем 4 изображения:
Длительность всех операций чуть больше 10 секунд.
Стартуем загрузку изображений, ожидаем 6 секунду и используем метод cancel ()
для удаления всех операций. В результате получаем загрузку лишь трех изображений — 1-го, 3-го и 4-го:
Результат можно посмотреть на Playground CancelletionFourImages.playground
на Github.
Паттерн 3. Работаем с TableViewController и CollectionViewController
Код проекта находится в папке OperationTableViewController
на Github.
Очень часто таблицы в iOS приложениях содержат изображения, получение которых требует обращения к серверу, а иногда и дополнительных действий с полученным изображением типа «фильтрации», о котором упоминалось в предыдущем разделе. Все это занимает значительное время и для гладкого прокручивания таблицы все манипуляции с изображениями должны выполняться асинхронно за пределами main queue
. Давайте рассмотрим применение представленного в предыдущем разделе класса ImageProvider
, который выполняет уже знакомую нам группу взаимосвязанных операций по загрузке изображения из сети, его фильтрации и модификации UI
.
Рассмотрим в качестве примера очень простое приложение, состоящее всего из одного Image Table View Controller
, у которого ячейки таблицы содержат только изображения, загружаемые из интернета и индикатор активности, показывающий процесс загрузки:
Вот как выглядит класс ImageTableViewController
, обслуживающий экранный фрагмент Image Table View Controller
:
Моделью для класса ImageTableViewController
является массив из 8 URLs:
- Эйфелева башня
- Венеция - загружается и фильтруется значительно дольше остальных
- Шотландский замок
- Арктика -02
- Эйфелева башня
- Арктика -16
- Арктика -15
- Арктика -12
Класс ImageTableViewCell
для ячейки таблицы, в которую загружается изображение, имеет вид:
Public API
этого класса — строка imageURLString
, содержащая URL адрес изображения. Но если мы зададим imageURLString
не равным nil
, то загрузки изображения не будет, начнет работать только индикатор в виде «вращающего колесика». Но если у нас уже есть каким-то образом загруженное и обработанное изображение image
, то вызывая метод updateImageViewWithImage
, мы покажем его в этой ячейке на экране с помощью легкой анимации. В этом классе есть индикатор активности spinner
, который стартует, если присвоить imageURLString
значение в методе tableView( _ : cellForRowAt:)
.
Загрузку изображения будем производить в методах делегата UITableViewDelegate
, отвечающих за взаимодействие ячейки UITableViewCell
с таблицей UITableView
:
- таблица запрашивает ячейку для показа на экране —
tableView( _ : cellForRowAt:)
, - таблица готова показать ячейку на экране —
tableView( _ : willDisplay:forRowAt:)
, - таблица убрала ячейку с экрана —
tableView( _ : didEndDisplaying:forRowAt:)
,
Запрос на асинхронную загрузку изображения с помощью класса ImageProvider
будет выведен за пределы метода tableView( _ : cellForRowAt:)
, чтобы максимально «облегчить» этот метод. Он разместится в методе tableView( _ : willDisplay:forRowAt:)
делегата, который готовит ячейку к тому чтобы стать видимой. Другой метод tableView( _ : didEndDisplaying:forRowAt:)
делегата будет использован для того, чтобы уничтожить любой запрос на загрузку изображения, который он не будет завершен к тому моменту, когда ячейка покинет экран. Это достаточно общий подход и может быть использован в любом приложении, работающим с TableView
. Это улучшит производительность прокрутки таблицы.
Но сначала вернемся к классу ImageProvider
, который будет использоваться в этом приложении. В отличие от варианта класса ImageProvider
, который был использован в предыдущем разделе на Playground
, будем использовать его упрощенную форму. А именно, нам нет необходимости при инициализации экземпляра класса ImageProvider
останавливать (isSuspended = true
) очередь операций, а затем специально стартовать экземпляра класса ImageProvider
с помощью метода start()
— мы сразу запускаем цепь зависимых операций при инициализации и задаем waitUntilFinished
равным false
, так как это не Playground
, а приложение, и мы не можем использовать синхронный метод wait()
:
Таким образом, в классе ImageProvider
у нас есть инициализатор, на вход которого мы должны подать строку imageURLString
с URL адресом изображения и замыканием completion
, которое generic
способом возвращает изображение типа UIImage?
тому, кто создал этот экземпляр класса ImageProvider
, вместо того, чтобы использовать вычисляемое свойство image
. Входное замыкание completion
имеет сигнатуру (UIImage?) -> ()
, то есть берет изображение UIImage?
и ничего не возвращает. Оно может быть использовано для возвращение в UITableViewController
.
Кроме того, мы должны разрешить уничтожать экземпляр класса ImageProvider
, который приведет к уничтожению всех стартовавших операций, если ячейка таблицы покинет экран до того, как все операции закончатся. Поэтому у нас есть метод cancel()
в классе ImageProvider
.
Итак, класс ImageProvider
обеспечивает асинхронное выполнение группы зависимых операций по загрузке изображения с сервера, фильтрации его и доставке в метод UITableViewDelegate
. В случае необходимости, мы можем удалить экземпляр этого класса.
Возвращаемся к ImageTableViewController
.
Вместо того, чтобы загружать изображение в методе tableView( _ tableView:, cellForRowAt indexPath:)
, мы будем это делать в другом методе делегата — tableView( _ tableView: , willDisplay cell:, forRowAt indexPath:)
, а затем мы удалим изображения в методе tableView( _ tableView: , didEndDisplaying cell:, forRowAt indexPath:)
.
Давайте создадим расширение extension
для этих двух методов. Начнем с метода tableView( _ tableView: , willDisplay cell:, forRowAt indexPath:)
.
Также как и в методе tableView( _ tableView:, cellForRowAt indexPath:)
, у нас будет ячейка cell
и indexPath
. Сначала выполняем стандартную процедуру проверки того, что ячейка имеет тип ImageTableViewCell
. Затем создаем imageProvider
со строкой imageURLs [ (indexPath as NSIndexPath).row ]
в качестве URL адреса изображения и замыканием, которое оформлено как «хвостовое» замыкание. В замыкании мы получаем изображение image
, которое необходимо показать в этой ячейки таблицы. Это UI
, и мы должны использовать для его обновления main queue
, потому что если вы попытаетесь сделать обновление UI
на фоновой очереди (background queue
), то это не будет работать, а вы будете удивляться, почему это не появляется изображение. У нас есть свойство main
класса OperationQueue
для main queue
, и все, что нам нужно сделать, — это вызвать метод updateImageViewWithImage( image )
на main queue
, который обновит нужную нам ячейку UITableViewCell
.
Теперь нам надо подумать об возможном удалении операции. Для этого нам нужно не потерять ссылку на созданный imageProvider
, иначе мы потом не сможем найти его и удалить связанные с ним операции.
Идем в самое начало класса ImageTableViewController
и добавляем новое свойство c именем imageProviders
:
Свойство imageProviders
представляет собой множество объектов типа imageProvider
, которое вначале пусто.
Давайте посмотрим на нижнюю часть файла ImageProvider.swift. Вы увидите там уже существующее расширение extension
класса ImageProvider
, которое подтверждает протокол Hashable
, необходимый для множеств Set
:
Мы получаем вычисляемое свойство hashValue
и оператор сравнения на равенство ==
. И теперь можем устанавливать и сравнивать экземпляры объектов ImageProvider
. Возвращаемся к ImageTableViewController
. Теперь мы можем отслеживать экземпляры объектов ImageProvider
и добавлять их в множество imageProviders
, которые задействованы на данный момент времени:
Давайте пройдем по этому коду, чтобы посмотреть шаг за шагом, что происходит. Это метод делегата tableView
, который вызывается непосредственно перед тем, как ячейка появится на экране. В этот момент мы создаем ImageProvider
, который асинхронно загружает, фильтрует изображение и возвращает результирующее изображение image
в completionHandler
. Мы используем image
для обновления UIImageView
на main queue
. Затем мы запоминаем ImageProvider
в множестве imageProviders
для того, чтобы мы могли уничтожить его позже. Мы будем это делать в следующем методе делегата tableView
с именем tableView( _ tableView:, didEndDisplaying cell:, forRowAt indexPath:)
, который вызывается сразу же после того, как ячейка уйдет с экрана. Именно здесь нам нужно уничтожить все операции этого ImageProvider
:
Для этого мы находим ImageProvider
для этой ячейки cell
и используем метод cancel ()
этого ImageProvider
, который удаляет все операции этого провайдера, а затем удаляем и сам провайдер из моего множества imageProviders
. Как всегда мы сначала выполняем стандартную процедуру проверки того, что ячейка имеет тип ImageTableViewCell
, а затем находим все провайдеры, у которых та же самая строка ImageURLString
, что и для данной ячейки. Проходим по всем этим провайдерам и удаляем их, а затем убираем их из множества imageProviders
. Это все, что нам необходимо сделать.
Давайте запустим приложение.
Вы видите, что работают индикаторы активности во время загрузки изображений и прокрутка теперь работает очень быстро без всякой задержки. Изображения пустые до тех пор, пока они не загрузятся, а затем происходит их анимация. Прекрасно.
Код проекта находится в папке OperationTableViewController
на Github.
Сравнение операции Operation
и очереди операций OperationQueue
с GCD
GCD
и Operations
имеют очень много сходных возможностей, но в таблице представлены их отличия.
DispatchGroup
и OperationQueue
могут обрабатывать событие, связанное с полным завершение всех заданий, но вы должны быть очень внимательны при запуске метода waitUntilAllOperationsAreFinished
для очереди OperationQueue
, которая ни в коем случае НЕ ДОЛЖНА быть main queue
.
Что касается зависимостей (dependecies
), то все, что вы можете сделать на GCD
, это реализовать цепочку заданий на private
последовательной (serial
) DispatchQueue
. Зато это самая сильная сторона OperationQueue
. Зависимости ( dependecies
) на OperationQueue
могут быть более сложными, чем просто цепочки, и операции могут выполняться на разных очередях OperationQueue
.
Вы можете использовать барьеры в GCD
для решения проблемы «писателей» и «читателей», если последовательная (serial
) очередь DispatchQueue
не подходит. Соответствующее решение этой проблемы с помощью OperationQueue
очень запутанное и требует flags
и очень специальных зависимостей.
В GCD
вы можете удалять только DispatchWorkItems
. Операции Operations
можно удалять с помощью их собственного метода cancel()
или все операции сразу на OperationQueue
. Можно удалять замыкания в BlockOperation
.
Как GCD
, так и Operations
могут выполнить СИНХРОННУЮ функцию АСИНХРОННО. При этом операция Operation
снабжает нас объектно-ориентированной моделью для инкапсуляции всех данных для этой повторно используемой функции, включая реализацию subclasses
Operation
. Но часто для более простых задач, не обремененных сложными зависимостями, удобнее использовать более «легкие» методы GCD
, чем создавать операцию Operation
. Кроме того Dispatch
блоки в GCD
требуют меньше времени для выполнения: от наносекунд до миллисекунд, а операция Operation
обычно требует от нескольких миллисекунд до минут.
Заключение
В этой статье мы рассмотрели следующие вопросы, касающиеся операции и Operation
и очереди операций OperationQueue
:
1. Операция Operation
может инкапсулировать задачу и данные в одном объекте, который имеет «жизненный цикл» и свойства, отображающие его состояния.
2. BlockOperation
представляет собой объектно-ориентированную «обертку» вокруг DispatchQueue.global()
, которая позволяет вам отслеживать выполнение группы замыканий вместо потери контроля над этой группой на DispatchQueue.global()
. BlockOperation
удобно использовать для простых операций как альтернативу GCD
, если вы в своем приложении уже используете Operations
и не хотите мешать их с DispatchQueue
.
3. Основной свой потенциал операции Operation
раскрывают, если они запускаются на OperationQueue
. Как только вы подготовили операцию Operation
, вы передаете ее на OperationQueue
, которая управляет порядком выполнения всех операций, по существу являясь очень простой моделью, которая скрывая сложность многопоточного программирования. OperationQueue
похожа на DispatchGroup
, для которой вы можете перемешивать операции с различным уровнем qualityOfService
и ожидать, пока все операции закончатся. Но вы должны быть очень внимательны, когда вызываете этот sync
метод.
4. Для включения АСИНХРОННЫХ функций, в операцию Operation
, мы должны сделать что-то специальное, чтобы точно фиксировать ее завершение. Мы должны управлять АСИНХРОННОЙ операцией AsyncOperation
вручную с помощью KVO.
5. Непревзойденной возможностью операций Operation
является то, что вы можете их комбинировать в цепочки операций для получения сложного графа зависимостей (dependencies
). Это означает, что вы можете очень легко определить операцию Operation
, которая не может стартовать до тех пор, пока одна или несколько других операций Operation
не завершатся. В статье показано, как можно использовать протокол protocol
для передачи данных между операциями Operation
для графа зависимостей (dependencies
). Но вы должны исследовать свой граф зависимостей с целью избежания циклов, которые могут вызвать deadLock
(неразрешимую взаимную блокировку), особенно, если есть зависимости между операциями на различных OperationQueue
.
6. Как только вы передали операцию Operation
на очередь операций OperationQueue
, вы потеряли контроль над этой операцией, ибо теперь очередь OperationQueue
сама составляет расписание запуска операций на выполнение и управляет их выполнением. Тем не менее, вы можете использовать метод cancel ()
для того, чтобы предотвратить запуск операции. В статье показано, как нужно учитывать свойство isCancelled
при конструировании операции Operation
и как можно удалить абсолютно все операции на очереди операций OperationQueue
.
7. В заключении показана разработка приложения, отражающего реальный сценарий, когда нужно прокручивать таблицу с изображениями, полученными из интернета и требующими дополнительных действий типа «фильтрации» или «состаривания». Все это занимает значительное время и для гладкого прокручивания таблицы все манипуляции с изображениями должны выполняться асинхронно за пределами main queue
. В этом приложении мы использовали широкий спектр приемов работы с операцией Operation
, в частности, с АСИНХРОННОЙ ОПЕРАЦИЕЙ AsyncOperation
и ее зависимостями (dependencies
), которые позволили нам добиться значительного улучшения нашего UI
.
Эта статья вместе с предыдущей дают вам полное представление о многопоточной обработке в Swift 3
и 4
на iOS, которая существует в настоящее время. Теперь вы можете принять полноправное участие в обсуждении будущих возможностей многопоточной обработки в Swift
, которая закладывается именно сейчас, когда для версии Swift 5
приоритетным направлением помимо ABI
стабильности объявлена многопоточность (concurrency
). Версия Swift 5
предполагает лишь начало работы над полностью новой моделью многопоточности, реализация которой будет продолжаться в последующих версиях. Уже поступают предложения о будущей модели многопоточности в Swift 5. Так что «включайте моторы!» и вперед.
За эволюцией Swift можно смотреть теперь здесь.
Ссылки
→ WWDC 2015.Advanced NSOperations (session 226).
→ Having fun with NSOperations in iOS
→ NSOperation and NSOperationQueue Tutorial in Swift
→ iOS Concurrency with GCD and Operations
→ CONCURRENCY IN IOS
→ Concurrency in Swift: One possible approach
Автор: WildGreyPlus