After learning about Swift's capture list and how it can be used to avoid retain cycle, I can't help noticing something puzzling about OperationQueue
: it doesn't need either [weak self]
or [unowned self]
to prevent memory leak.
class SomeManager {
let queue = OperationQueue()
let cache: NSCache = { () -> NSCache<AnyObject, AnyObject> in
let cache = NSCache<AnyObject, AnyObject>()
cache.name = "huaTham.TestOperationQueueRetainCycle.someManager.cache"
cache.countLimit = 16
return cache
}()
func addTask(a: Int) {
queue.addOperation { // "[unowned self] in" not needed?
self.cache.setObject(a as AnyObject, forKey: a as AnyObject)
print("hello \(a)")
}
}
}
class ViewController: UIViewController {
var someM: SomeManager? = SomeManager()
override func viewDidLoad() {
super.viewDidLoad()
someM?.addTask(a: 1)
someM?.addTask(a: 2)
}
// This connects to a button.
@IBAction func invalidate() {
someM = nil // Perfectly fine here. No leak.
}
}
I don't see why adding an operation would not cause a retain cycle: SomeManager
strongly owns the queue
, which in turns strongly owns the added closures. Each added closure strongly refers back to SomeManager
. This should theoretically create a retain cycle leading to memory leak. Yet Instruments shows that everything is perfectly fine.
Why is this the case? In some other multi-threaded, block-based APIs, like DispatchSource
, you seem to need the capture list. See Apple's sample code ShapeEdit
for example, in ThumbnailCache.swift
:
fileprivate var flushSource: DispatchSource
...
flushSource.setEventHandler { [weak self] in // Here
guard let strongSelf = self else { return }
strongSelf.delegate?.thumbnailCache(strongSelf, didLoadThumbnailsForURLs: strongSelf.URLsNeedingReload)
strongSelf.URLsNeedingReload.removeAll()
}
But in the same code file, OperationQueue
doesn't need the capture list, despite having the same semantics: you hand over a closure with reference to self
to be executed asynchronously:
fileprivate let workerQueue: OperationQueue { ... }
...
self.workerQueue.addOperation {
if let thumbnail = self.loadThumbnailFromDiskForURL(URL) {
...
self.cache.setObject(scaledThumbnail!, forKey: documentIdentifier as AnyObject)
}
}
I have read about Swift's capture list above, as well as related SO answers like this and this and this, but I still don't know why [weak self]
or [unowned self]
are not needed in OperationQueue
API while they are in Dispatch
API. I'm also not sure how no leaks are found in OperationQueue
case.
Any clarifications would be much appreciated.
In addition to the accepted answer below, I also find the comment by QuinceyMorris in Apple forums quite helpful.
You do have a retain cycle there, but that doesn’t automatically lead to a memory leak. After the queue finished the operation it releases it thus breaking the cycle.
Such an temporary retain cycle can be very useful in some situations as you won’t have to hang on to an object and still have it finish its work.
As an experiment you can suspend the queue. Then you will see the memory leak.