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

孤人 提交于 2019-12-01 11:42:28

The key problem is that your markAsCompleted is triggering isFinished when the operation is not isExecuting. I'd suggest you just fix that markAsCompleted to only do this if isExecuting is true. This reduces the burden on subclasses doing any complicated state tests to figure out whether they need to transition to isFinished or not.

That having been said, I see three basic patterns when writing cancelable asynchronous operations:

  1. If I'm dealing with some pattern where the canceling of the task will prevent it from transitioning executing operations to a isFinished state.

    In that case, I must have cancel implementation manually finish the executing operations. For example:

    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()
            }
        }
    }
    

    Focusing on the cancel implementation, because if I cancel the DispatchWorkItem it won't finish the operation, I therefore need to make sure that cancel will explicitly finish the operation itself.

  2. Sometimes, when you cancel some asynchronous task, it will call its completion handler automatically for you, in which case cancel doesn't need to do anything other than cancel the that task and call super. For example:

    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()
        }
    }
    

    Again, focusing on cancel, in this case we don't touch the "finished" state, but just cancel dataTask (which will call its completion handler even if you cancel the request) and call the super implementation.

  3. The third scenario is where you have some operation that is periodically checking isCancelled state. In that case, you don't have to implement cancel at all, as the default behavior is sufficient. For example:

    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`
        }
    }
    

    In this case, where I've wrapped a display link in an operation so I can manage dependencies and/or encapsulate the display link in a convenient object, I don't have to implement cancel at all, because the default implementation will update isCancelled for me, and I can just check for that.

Those are three basic cancel patterns I generally see. That having been said, updating markAsCompleted to only trigger isFinished if isExecuting is a good safety check to make sure you can never get the problem you described.


By the way, the AsynchronousOperation that I used for the above examples is as follows, adapted from Trying to Understand Asynchronous Operation Subclass. BTW, what you called markAsCompleted is called finish, and it sounds like you're triggering the isFinished and isExecuting KVO via a different mechanism, but the idea is basically the same. Just check the current state before you trigger 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 }
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!