问题
Overview
- There is an asynchronous operation subclass
- Added this operation to the queue.
- I cancelled this operation before it starts.
Runtime Error / Warning:
SomeOperation went isFinished=YES without being started by the queue it is in
Question:
- Is this something that can be ignored or it is something serious ?
- How to resolve this ?
- Is the workaround / solution provided at the end valid ?
Code:
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
}
}
Adding to Queue and cancelling:
//someOperation is a property in a class
if let someOperation = someOperation {
queue.addOperation(someOperation)
}
//Based on some condition cancelling it
someOperation?.cancel()
Is this valid a solution ?
public override func cancel() {
isExecuting = true //Just in case the operation was cancelled before starting
super.cancel()
markAsCompleted()
}
Note:
markAsCompleted
setsisExecuting = false
andisFinished = true
isExecuting
,isFinished
are properties which are synchronisedKVO
回答1:
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:
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 theDispatchWorkItem
it won't finish the operation, I therefore need to make sure thatcancel
will explicitly finish the operation itself.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 canceldataTask
(which will call its completion handler even if you cancel the request) and call thesuper
implementation.The third scenario is where you have some operation that is periodically checking
isCancelled
state. In that case, you don't have to implementcancel
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 updateisCancelled
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 }
}
}
来源:https://stackoverflow.com/questions/48137896/operation-went-isfinished-yes-without-being-started-by-the-queue-it-is-in