Создание подкласса OperationQueue для добавления периода сна
import Foundation
class MyOperationQueue {
static let shared = MyOperationQueue()
private var queue: OperationQueue
init() {
self.queue = OperationQueue()
queue.name = "com.myqueue.name"
queue.maxConcurrentOperationCount = 1
queue.qualityOfService = .background
}
func requestDataOperation() {
queue.addOperation {
print("START NETWORK \(Date())")
NetworkService.shared.getData()
print("END NETWORK \(Date())")
}
}
func scheduleSleep() {
queue.cancelAllOperations()
queue.addOperation {
print("SLEEP START \(Date())")
Thread.sleep(forTimeInterval: 5)
print("SLEEP END \(Date())")
}
}
func cancelAll() {
queue.cancelAllOperations()
}
}
я кладу
requestDataOperation
функция внутри таймера для каждого 10-секундного интервала. И у меня есть кнопка для звонка
scheduleSleep
вручную. Ожидалось, что я буду отклонять запрос каждые 5 секунд, когда нажимаю кнопку.
Но я получаю что-то вроде этого:
START NETWORK
END NETWORK
SLEEP START 2021-03-11 11:13:40 +0000
SLEEP END 2021-03-11 11:13:45 +0000
SLEEP START 2021-03-11 11:13:45 +0000
SLEEP END 2021-03-11 11:13:50 +0000
START NETWORK
END NETWORK
Как добавить еще 5 секунд с момента последнего нажатия и объединить их вместе, а не разбивать на две операции? Я звоню
queue.cancelAllOperations
и начать новую операцию сна, но, похоже, не работает.
Ожидайте результата:
START NETWORK
END NETWORK
SLEEP START 2021-03-11 11:13:40 +0000
// <- the second tap when 2 seconds passed away
SLEEP END 2021-03-11 11:13:47 +0000 // 2+5
START NETWORK
END NETWORK
1 ответ
Если вы хотите, чтобы какая-то операция была отложена на определенное время, я бы не создавал класс «очередь», а просто определял бы класс, которого просто не будет, пока не пройдет это время (например, пять секунд спустя). Это не только устраняет необходимость в двух отдельных «операциях сна», но и полностью исключает их.
Например,
class DelayedOperation: Operation {
@Atomic private var enoughTimePassed = false
private var timer: DispatchSourceTimer?
private var block: (() -> Void)?
override var isReady: Bool { enoughTimePassed && super.isReady } // this operation won't run until (a) enough time has passed; and (b) any dependencies or the like are satisfied
init(timeInterval: TimeInterval = 5, block: @escaping () -> Void) {
self.block = block
super.init()
resetTimer(for: timeInterval)
}
override func main() {
block?()
block = nil
}
func resetTimer(for timeInterval: TimeInterval = 5) {
timer = DispatchSource.makeTimerSource() // create GCD timer (eliminating reference to any prior timer will cancel that one)
timer?.setEventHandler { [weak self] in
guard let self = self else { return }
self.willChangeValue(forKey: #keyPath(isReady)) // make sure to do necessary `isReady` KVO notification
self.enoughTimePassed = true
self.didChangeValue(forKey: #keyPath(isReady))
}
timer?.schedule(deadline: .now() + timeInterval)
timer?.resume()
}
}
Я синхронизирую свое взаимодействие с
enoughTimePassed
со следующей оболочкой свойств, но вы можете использовать любой механизм синхронизации, который захотите:
@propertyWrapper
struct Atomic<Value> {
private var value: Value
private var lock = NSLock()
init(wrappedValue: Value) {
value = wrappedValue
}
var wrappedValue: Value {
get { synchronized { value } }
set { synchronized { value = newValue } }
}
private func synchronized<T>(block: () throws -> T) rethrows -> T {
lock.lock()
defer { lock.unlock() }
return try block()
}
}
Просто убедитесь, что это потокобезопасный.
Во всяком случае, определив, что
DelayedOperation
, тогда вы можете сделать что-то вроде
logger.debug("creating operation")
let operation = DelayedOperation {
logger.debug("some task")
}
queue.addOperation(operation)
И это задержит выполнение этой задачи (в данном случае просто войдите в систему с сообщением «какая-то задача») на пять секунд. Если вы хотите сбросить таймер, просто вызовите этот метод в подклассе операции:
operation.resetTimer()
Например, здесь я создал задачу, добавил ее в очередь, сбросил ее три раза с интервалом в две секунды, и блок фактически запускается через пять секунд после последнего сброса:
2021-09-30 01:13:12.727038-0700 MyApp[7882:228747] [ViewController] creating operation
2021-09-30 01:13:14.728953-0700 MyApp[7882:228747] [ViewController] delaying operation
2021-09-30 01:13:16.728942-0700 MyApp[7882:228747] [ViewController] delaying operation
2021-09-30 01:13:18.729079-0700 MyApp[7882:228747] [ViewController] delaying operation
2021-09-30 01:13:23.731010-0700 MyApp[7882:228829] [ViewController] some task
Теперь, если вы используете операции для сетевых запросов, то вы, вероятно, уже реализовали свой собственный асинхронный подкласс, который выполняет необходимые KVO для
isFinished
,
isExecuting
и т. д., так что вы можете жениться на вышеупомянутом
isReady
логика с существующим
Operation
подкласс.
Но идея состоит в том, чтобы полностью потерять операцию «сна» (которая блокирует один из очень ограниченного числа рабочих потоков) с помощью асинхронного шаблона.
С учетом всего вышесказанного, если бы я лично хотел отклонить сетевой запрос, я бы не стал интегрировать это в операцию или очередь операций. Я бы просто сделал это отбой в то время, когда я начал запрос:
weak var timer: Timer?
func debouncedRequest(in timeInterval: TimeInterval = 5) {
timer?.invalidate()
timer = .scheduledTimer(withTimeInterval: timeInterval, repeats: false) { _ in
// initiate request here
}
}