一个问题是您的操作只是启动请求,但由于请求是异步执行的,操作是立即完成的,而不是真正等待请求完成。在异步请求完成之前,您不想完成操作。
如果您想对操作队列执行此操作,诀窍是您必须继承 Operation 并为 isExecuting 和 isFinished 执行必要的 KVO。然后,您在启动请求时更改 isExecuting,在完成请求时更改 isFinished,并为两者关联 KVO。这一切都在Concurrency Programming Guide: Defining a Custom Operation Object 中进行了概述,特别是在Configuring Operations for Concurrent Execution 部分。 (注意,本指南有点过时(它指的是isConcurrent 属性,已被替换为isAsynchronous;它专注于Objective-C 等),但它向您介绍了问题。
无论如何,这是一个抽象类,我用它来封装所有这些愚蠢的异步操作:
/// Asynchronous Operation base class
///
/// This class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
/// necessary and then ensuring that `completeOperation()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `completeOperation()` is called.
public class AsynchronousOperation : Operation {
override public var isAsynchronous: Bool { return true }
private let lock = NSLock()
private var _executing: Bool = false
override private(set) public var isExecuting: Bool {
get {
return lock.synchronize { _executing }
}
set {
willChangeValue(forKey: "isExecuting")
lock.synchronize { _executing = newValue }
didChangeValue(forKey: "isExecuting")
}
}
private var _finished: Bool = false
override private(set) public var isFinished: Bool {
get {
return lock.synchronize { _finished }
}
set {
willChangeValue(forKey: "isFinished")
lock.synchronize { _finished = newValue }
didChangeValue(forKey: "isFinished")
}
}
/// Complete the operation
///
/// This will result in the appropriate KVN of isFinished and isExecuting
public func completeOperation() {
if isExecuting {
isExecuting = false
isFinished = true
}
}
override public func start() {
if isCancelled {
isFinished = true
return
}
isExecuting = true
main()
}
}
我使用这个 Apple 扩展 NSLocking 来确保我同步上面的状态更改(他们的扩展名为 withCriticalSection 上 NSLock,但这是一个稍微更通用的演绎,适用于任何事情符合NSLocking 并处理抛出错误的闭包):
extension NSLocking {
/// Perform closure within lock.
///
/// An extension to `NSLocking` to simplify executing critical code.
///
/// - parameter block: The closure to be performed.
func synchronize<T>(block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}
然后,我可以创建一个使用它的NetworkOperation:
class NetworkOperation: AsynchronousOperation {
var task: URLSessionTask!
init(session: URLSession, url: URL, requestCompletionHandler: @escaping (Data?, URLResponse?, Error?) -> ()) {
super.init()
task = session.dataTask(with: url) { data, response, error in
requestCompletionHandler(data, response, error)
self.completeOperation()
}
}
override func main() {
task.resume()
}
override func cancel() {
task.cancel()
super.cancel()
}
}
无论如何,完成之后,我现在可以为网络请求创建操作,例如:
let queue = OperationQueue()
queue.name = "com.domain.app.network"
let url = URL(string: "http://...")!
let operation = NetworkOperation(session: .shared, url: url) { data, response, error in
guard let data = data, error == nil else {
print("\(error)")
return
}
let string = String(data: data, encoding: .utf8)
print("\(string)")
// do something with `data` here
}
let operation2 = BlockOperation {
print("done")
}
operation2.addDependency(operation)
queue.addOperations([operation, operation2], waitUntilFinished: false) // if you're using command line app, you'd might use `true` for `waitUntilFinished`, but with standard Cocoa apps, you generally would not
注意,在上面的例子中,我添加了第二个操作,它只是打印了一些东西,使它依赖于第一个操作,以说明第一个操作直到网络请求完成后才完成。
显然,您通常不会使用原始示例的waitUntilAllOperationsAreFinished,也不会使用我的示例中的addOperations 的waitUntilFinished 选项。但是因为您正在处理一个在完成这些请求之前不想退出的命令行应用程序,所以这种模式是可以接受的。 (我只是为了让未来的读者对随意使用waitUntilFinished 感到惊讶,这通常是不可取的。)