[NSBlockOperation addExecutionBlock:]: blocks cannot be added after the operation has started executing or finished

瘦欲@ 提交于 2019-12-01 09:21:47

Let me outline a series of alternatives. The first is just addressing the immediate tactical problem in your question, and the latter two being further refinements, of increasing complexity.

  1. You cannot call addExecutionBlock once an operation has started. So just create a new Operation.

    For example:

    class ViewController: UIViewController {
    
        @IBOutlet weak var imageView1: UIImageView!
    
        weak var downloadOperation: Operation?    // make this weak
    
        var queue = OperationQueue()
    
        let imageURLs: [String] = ...
    
        @IBAction func didClickOnStart(_ sender: Any) {
            downloadOperation?.cancel()             // you might want to stop the previous one if you restart this
    
            let operation = BlockOperation {
                for (index, imageURL) in self.imageURLs.enumerate() {
                    guard let cancelled = self.downloadOperation?.cancelled where !cancelled else  { return }
    
                    let img1 = Downloader.downloadImageWithURL(imageURL)
                    OperationQueue.main.addOperation {
                        self.imageView1.image = img1
    
                        print("index \(index)")
                    }
                }
            }
            queue.addOperation(operation)
    
            downloadOperation = operation
        }
    
        @IBAction func didClickOnCancel(_ sender: Any) {
            downloadOperation?.cancel()
        }
    
    }
    
  2. It's worth noting that this is going to be unnecessarily slow, loading the images consecutively. You could load them concurrently with something like:

    class ViewController: UIViewController {
    
        @IBOutlet weak var imageView1: UIImageView!
    
        var queue: OperationQueue = {
            let _queue = OperationQueue()
            _queue.maxConcurrentOperationCount = 4
            return _queue
        }()
    
        let imageURLs: [String] = ...
    
        @IBAction func didClickOnStart(_ sender: Any) {
            queue.cancelAllOperations()
    
            let completionOperation = BlockOperation {
                print("all done")
            }
    
            for (index, imageURL) in self.imageURLs.enumerate() {
                let operation = BlockOperation {
                    let img1 = Downloader.downloadImageWithURL(imageURL)
                    OperationQueue.main.addOperation {
                        self.imageView1.image = img1
    
                        print("index \(index)")
                    }
                }
                completionOperation.addDependency(operation)
                queue.addOperation(operation)
            }
    
            OperationQueue.main.addOperation(completionOperation)
        }
    
        @IBAction func didClickOnCancel(_ sender: Any) {
            queue.cancelAllOperations()
        }
    }
    
  3. Even that has issues. The other problem is that when you “cancel”, it may well continue trying to download the resource currently being downloaded because you're not using a cancelable network request.

    An even better approach would be to wrap the download (which is to be performed via URLSession) in its own asynchronous Operation subclass, and make it cancelable, e.g.:

    class ViewController: UIViewController {
        var queue: OperationQueue = {
            let _queue = OperationQueue()
            _queue.maxConcurrentOperationCount = 4
            return _queue
        }()
    
        let imageURLs: [URL] = ...
    
        @IBAction func didClickOnStart(_ sender: Any) {
            queue.cancelAllOperations()
    
            let completion = BlockOperation {
                print("done")
            }
    
            for url in imageURLs {
                let operation = ImageDownloadOperation(url: url) { result in
                    switch result {
                    case .failure(let error): 
                        print(url.lastPathComponent, error)
    
                    case .success(let image): 
                        OperationQueue.main.addOperation {
                            self.imageView1.image = img1
    
                            print("index \(index)")
                        }
                    }
                }
                completion.addDependency(operation)
                queue.addOperation(operation)
            }
    
            OperationQueue.main.addOperation(completion)
        }
    
        @IBAction func didClickOnCancel(_ sender: AnyObject) {
            queue.cancelAllOperations()
        }
    }
    

    Where

    /// Simple image network operation
    
    class ImageDownloadOperation: DataOperation {
        init(url: URL, session: URLSession = .shared, networkCompletionHandler: @escaping (Result<UIImage, Error>) -> Void) {
            super.init(url: url, session: session) { result in
                switch result {
                case .failure(let error):
                    networkCompletionHandler(.failure(error))
    
                case .success(let data):
                    guard let image = UIImage(data: data) else {
                        networkCompletionHandler(.failure(DownloadError.notImage))
                        return
                    }
    
                    networkCompletionHandler(.success(image))
                }
            }
        }
    }
    
    /// Simple network data operation
    ///
    /// This can be subclassed for image-specific operations, JSON-specific operations, etc.
    
    class DataOperation: AsynchronousOperation {
        var downloadTask: URLSessionTask?
    
        init(url: URL, session: URLSession = .shared, networkCompletionHandler: @escaping (Result<Data, Error>) -> Void) {
            super.init()
    
            downloadTask = session.dataTask(with: url) { data, response, error in
                defer { self.complete() }
    
                guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
                    networkCompletionHandler(.failure(error!))
                    return
                }
    
                guard 200..<300 ~= response.statusCode else {
                    networkCompletionHandler(.failure(DownloadError.invalidStatusCode(response)))
                    return
                }
    
                networkCompletionHandler(.success(data))
            }
        }
    
        override func main() {
            downloadTask?.resume()
        }
    
        override func cancel() {
            super.cancel()
    
            downloadTask?.cancel()
        }
    }
    
    /// 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 stateLock = NSLock()
    
        private var _executing: Bool = false
        override private(set) public var isExecuting: Bool {
            get {
                return stateLock.withCriticalScope { _executing }
            }
            set {
                willChangeValue(forKey: "isExecuting")
                stateLock.withCriticalScope { _executing = newValue }
                willChangeValue(forKey: "isExecuting")
            }
        }
    
        private var _finished: Bool = false
        override private(set) public var isFinished: Bool {
            get {
                return stateLock.withCriticalScope { _finished }
            }
            set {
                willChangeValue(forKey: "isFinished")
                stateLock.withCriticalScope { _finished = newValue }
                didChangeValue(forKey: "isFinished")
            }
        }
    
        /// Complete the operation
        ///
        /// This will result in the appropriate KVN of isFinished and isExecuting
    
        public func complete() {
            if isExecuting {
                isExecuting = false
            }
    
            if !isFinished {
                isFinished = true
            }
        }
    
        override public func start() {
            if isCancelled {
                isFinished = true
                return
            }
    
            isExecuting = true
    
            main()
        }
    
        override public func main() {
            fatalError("subclasses must override `main`")
        }
    }
    
    extension NSLock {
    
        /// Perform closure within lock.
        ///
        /// An extension to `NSLock` to simplify executing critical code.
        ///
        /// - parameter block: The closure to be performed.
    
        func withCriticalScope<T>(block: () throws -> T) rethrows -> T {
            lock()
            defer { unlock() }
            return try block()
        }
    }
    
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!