操作完成了 isFinished=YES 没有被它所在的队列启动

Operation went isFinished=YES without being started by the queue it is in

概览

运行时错误/警告:

SomeOperation went isFinished=YES without being started by the queue it is in

问题:

  1. 这是可以忽略的事情还是很严重的事情?
  2. 如何解决这个问题?
  3. 最后提供的解决方法/解决方案有效吗?

代码:

public class SomeOperation : AsyncOperation {

    //MARK: Start

    public override func start() {

        isExecuting = true

        guard !isCancelled else {
            markAsCompleted() //isExecuting = false, isFinished = true
            return
        }

        doSomethingAsynchronously { [weak self] in

            self?.markAsCompleted() //isExecuting = false, isFinished = true
        }
    }

    //MARK: Cancel

    public override func cancel() {

        super.cancel()
        markAsCompleted() //isExecuting = false, isFinished = true
    }
}

加入队列和取消:

//someOperation is a property in a class
if let someOperation = someOperation {
    queue.addOperation(someOperation)
}

//Based on some condition cancelling it
someOperation?.cancel()

这是有效的解决方案吗?

public override func cancel() {

    isExecuting = true //Just in case the operation was cancelled before starting

    super.cancel()
    markAsCompleted()
}

注:

关键问题是你的markAsCompleted在操作不是isExecuting的时候触发了isFinished。我建议您将 markAsCompleted 修改为仅在 isExecuting 为真时执行此操作。这减轻了子类进行任何复杂状态测试以确定它们是否需要转换到 isFinished 的负担。

话虽如此,我在编写可取消异步操作时看到了三种基本模式:

  1. 如果我正在处理某种模式,其中取消任务会阻止它把正在执行的操作转换到 isFinished 状态。

    在那种情况下,我必须 cancel 实现手动完成执行操作。例如:

    class FiveSecondOperation: AsynchronousOperation {
        var block: DispatchWorkItem?
    
        override func main() {
            block = DispatchWorkItem { [weak self] in
                self?.finish()
                self?.block = nil
            }
    
            DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: block!)
        }
    
        override func cancel() {
            super.cancel()
    
            if isExecuting {
                block?.cancel()
                finish()
            }
        }
    }
    

    关注 cancel 实现,因为如果我取消 DispatchWorkItem 它不会完成操作,因此我需要确保 cancel 将显式完成操作本身。

  2. 有时,当您取消一些异步任务时,它会自动为您调用其完成处理程序,在这种情况下 cancel 除了取消该任务外不需要做任何事情并打电话给超级。例如:

    class GetOperation: AsynchronousOperation {
        var url: URL
        weak var task: URLSessionTask?
    
        init(url: URL) {
            self.url = url
            super.init()
        }
    
        override func main() {
            let task = URLSession.shared.dataTask(with: url) { data, _, error in
                defer { self.finish() }  // make sure to finish the operation
    
                // process `data` & `error` here
            }
            task.resume()
            self.task = task
        }
    
        override func cancel() {
            super.cancel()
            task?.cancel()
        }
    }
    

    同样,关注cancel,在这种情况下我们不触及"finished"状态,而只是取消dataTask(即使你取消也会调用它的完成处理程序请求)并调用 super 实现。

  3. 第三种情况是您有一些定期检查 isCancelled 状态的操作。在这种情况下,您根本不必实施 cancel,因为默认行为就足够了。例如:

    class DisplayLinkOperation: AsynchronousOperation {
        private weak var displayLink: CADisplayLink?
        private var startTime: CFTimeInterval!
        private let duration: CFTimeInterval = 2
    
        override func main() {
            startTime = CACurrentMediaTime()
            let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
            displayLink.add(to: .main, forMode: .commonModes)
            self.displayLink = displayLink
        }
    
        @objc func handleDisplayLink(_ displayLink: CADisplayLink) {
            let percentComplete = (CACurrentMediaTime() - startTime) / duration
    
            if percentComplete >= 1.0 || isCancelled {
                displayLink.invalidate()
                finish()
            }
    
            // now do some UI update based upon `elapsed`
        }
    }
    

    在这种情况下,我在一个操作中包装了显示 link 以便我可以管理依赖项 and/or 将显示 link 封装在一个方便的对象中,我不根本不必实施 cancel,因为默认实施会为我更新 isCancelled,我可以检查一下。

这是我通常看到的三种基本 cancel 模式。话虽如此,将 markAsCompleted 更新为仅在 isExecuting 时触发 isFinished 是一个很好的安全检查,以确保您永远不会遇到您描述的问题。


顺便说一句,我在上面的例子中使用的AsynchronousOperation如下,改编自。顺便说一句,你所谓的 markAsCompleted 被称为 finish,听起来你是通过不同的机制触发 isFinishedisExecuting KVO,但这个想法基本上是相同的。在触发 isFinished KVO:

之前检查当前状态
open class AsynchronousOperation: Operation {

    /// State for this operation.

    @objc private enum OperationState: Int {
        case ready
        case executing
        case finished
    }

    /// Concurrent queue for synchronizing access to `state`.

    private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent)

    /// Private backing stored property for `state`.

    private var rawState: OperationState = .ready

    /// The state of the operation

    @objc private dynamic var state: OperationState {
        get { return stateQueue.sync { rawState } }
        set { stateQueue.sync(flags: .barrier) { rawState = newValue } }
    }

    // MARK: - Various `Operation` properties

    open         override var isReady:        Bool { return state == .ready && super.isReady }
    public final override var isExecuting:    Bool { return state == .executing }
    public final override var isFinished:     Bool { return state == .finished }
    public final override var isAsynchronous: Bool { return true }

    // MARK: - KVN for dependent properties

    open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        if ["isReady", "isFinished", "isExecuting"].contains(key) {
            return [#keyPath(state)]
        }

        return super.keyPathsForValuesAffectingValue(forKey: key)
    }

    // MARK: - Foundation.Operation

    public final override func start() {
        if isCancelled {
            state = .finished
            return
        }

        state = .executing

        main()
    }

    /// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception.

    open override func main() {
        fatalError("Subclasses must implement `main`.")
    }

    /// Call this function to finish an operation that is currently executing

    public final func finish() {
        if isExecuting { state = .finished }
    }
}