crypt3c crypt3c -4 years ago 126
iOS Question

What happens to Dispatch Queues when UIViewController is Deallocated?

I am trying to better understand retain cycles, especially relative to Dispatch Queues. I am working with AVFoundation and managing an AVCaptureSession on a sessionQueue:

private let sessionQueue = DispatchQueue(label: "com.andrewferrarone.sessionQueue")


in a lot of code examples in the apple documentation I see this:

self.sessionQueue.async { [unowned self]
//
}


Is the
[unowned self]
self here necessary? self (the viewController) references
self.sessionQueue
and the closure dispatched to
self.sessionQueue
captures self. Is this a reference cycle? self is not referencing the closure, just the DispatchQueue. If
[unowned self]
is necessary, then from what I understand, I only want to use
unowned self
if I am certain that self will not be nil. So lets say I put a task on
sessionQueue
that takes a long time and the viewController gets popped off and is deallocated before the task finishes? What happens to
sessionQueue
and the task? If its still around then, when it tries to access self, the app will crash. On the other hand, since unowned self doesn't increment the retain count of self, then it won't prevent the viewController from being deallocated.

So My question is what happens to DispatchQueues when a viewController is deallocated and what happens in this case, if a viewController gets deallocated before a dispatchQueue task is finished? If someone could shed some light on what all is going on here that would be very helpful and appreciated.

Thanks for the help my friends!

Answer Source

Is the [unowned self] self here necessary?

Not only is the use of [unowned self] not necessary, but it's very dangerous in an asynchronously dispatched block. You end up with a dangling pointer to a deallocated object.

If you don't want to keep keep a strong reference to self in an asynchronous call, use [weak self], instead. You should only use unowned if you know the block can never be called after self is deallocated. Obviously, with async call, you don't know this, so [unowned self] should not be used in that context.

Whether you use [weak self] or use strong references is a question of whether you need the asynchronously executed block to keep a strong reference to the object in question or not. For example, if you're updating a view controller's view's controls only, then [weak self] is fine (no point in updating a view that has been dismissed).

The more critical use of weak and unowned references is to avoid strong reference cycles. But that doesn't apply in the example you've provided. You only need to worry about those cycles if the view controller keeps some reference to the blocks itself (e.g. you have some closure property) and those closures reference self, but without a weak/unowned qualifier.

My question is what happens to DispatchQueues when a view controller is deallocated?

Those queues will continue to exist, as will any dispatched blocks, until (a) all dispatched blocks finish; and (b) there are no more strong references to the queue.

So if you asynchronously dispatch blocks with weak references to self (i.e. the view controller), they will continue to run after the view controller is released. This is why it's critical to not use unowned in this context.


For what it's worth, empirical tests can be illuminating. Consider:

class SecondViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let queue = DispatchQueue(label: "com.domain.app.SecondViewController")

        for i in 0 ..< 10 {
            queue.async { [weak self] in
                print("closure \(i) start")
                self?.performSomeTask(i)
                print("closure \(i) finish")
            }
        }
    }

    private func performSomeTask(_ value: Int) {
        print("performSomeTask starting \(value)")
        Thread.sleep(forTimeInterval: 5)        // you wouldn't generally `sleep`, but merely for diagnostic purposes
        print("performSomeTask finishing \(value)")
    }

    deinit {
        print("deinit SecondViewController")
    }

}

If you dismiss this view controller while the dispatched blocks are queued up and running, you'll see:

  • With [weak self], the view controller is retained only until the current dispatched block finishes, the view controller will then be released, and the rest of the blocks will rapidly fire off, but because of [weak self], the performSomeTask won't run after the view controller is dismissed.

  • If you replace weak with unowned (and obviously remove the ? in self?.performSomeTask(...)), you'll see it crash if you dismiss the view controller before the queued blocks have had a chance to start. This is illustrative of why [unowned self] is so dangerous with asynchronous code.

  • If you simply remove [weak self] altogether and let it use a implicitly strong reference to self, you'll see it won't deallocate the view controller until all queued blocks finish.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download