How can I create a reference cycle using dispatchQueues?

前端 未结 1 1745
南方客
南方客 2020-12-03 20:39

I feel that I\'ve always misunderstood that when reference cycles are created. Before I use to think that almost any where that you have a block and the compiler is forcing

相关标签:
1条回答
  • 2020-12-03 21:07

    You say:

    From what I understand the setup here is:

    self ---> queue
    self <--- block
    

    The queue is merely a shell/wrapper for the block. Which is why even if I nil the queue, the block will continue its execution. They’re independent.

    The fact that self happens to have a strong reference to the queue is inconsequential. A better way of thinking about it is that a GCD, itself, keeps a reference to all dispatch queues on which there is anything queued. (It’s analogous to a custom URLSession instance that won’t be deallocated until all tasks on that session are done.)

    So, GCD keeps reference to the queue with dispatched tasks. The queue keeps a strong reference to the dispatched blocks/items. The queued block keeps a strong reference to any reference types they capture. When the dispatched task finishes, it resolves any strong references to any captured reference types and is removed from the queue (unless you keep your own reference to it elsewhere.), generally thereby resolving any strong reference cycles.


    Setting that aside, where the absence of [weak self] can get you into trouble is where GCD keeps a reference to the block for some reason, such as dispatch sources. The classic example is the repeating timer:

    class Ticker {
        private var timer: DispatchSourceTimer?
    
        func startTicker() {    
            let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".ticker")
            timer = DispatchSource.makeTimerSource(queue: queue)
            timer!.schedule(deadline: .now(), repeating: 1)
            timer!.setEventHandler {                         // whoops; missing `[weak self]`
                self.tick()
            }
            timer!.resume()
        }
    
        func tick() { ... }
    }
    

    Even if the view controller in which I started the above timer is dismissed, GCD keeps firing this timer and Ticker won’t be released. As the “Debug Memory Graph” feature shows, the block, created in the startTicker routine, is keeping a persistent strong reference to the Ticker object:

    This is obviously resolved if I use [weak self] in that block used as the event handler for the timer scheduled on that dispatch queue.

    Other scenarios include a slow (or indefinite length) dispatched task, where you want to cancel it (e.g., in the deinit):

    class Calculator {
        private var item: DispatchWorkItem!
    
        deinit {
            item?.cancel()
            item = nil
        }
    
        func startCalculation() {
            let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".calcs")
            item = DispatchWorkItem {                         // whoops; missing `[weak self]`
                while true {
                    if self.item?.isCancelled ?? true { break }
                    self.calculateNextDataPoint()
                }
                self.item = nil
            }
            queue.async(execute: item)
        }
    
        func calculateNextDataPoint() {
            // some intense calculation here
        }
    }
    

    All of that having been said, in the vast majority of GCD use-cases, the choice of [weak self] is not one of strong reference cycles, but rather merely whether we mind if strong reference to self persists until the task is done or not.

    • If we’re just going to update the the UI when the task is done, there’s no need to keep the view controller and its views in the hierarchy waiting some UI update if the view controller has been dismissed.

    • If we need to update the data store when the task is done, then we definitely don’t want to use [weak self] if we want to make sure that update happens.

    • Frequently, the dispatched tasks aren’t consequential enough to worry about the lifespan of self. For example, you might have a URLSession completion handler dispatch UI update back to the main queue when the request is done. Sure, we theoretically would want [weak self] (as there’s no reason to keep the view hierarchy around for a view controller that’s been dismissed), but then again that adds noise to our code, often with little material benefit.


    Unrelated, but playgrounds are a horrible place to test memory behavior because they have their own idiosyncrasies. It’s much better to do it in an actual app. Plus, in an actual app, you then have the “Debug Memory Graph” feature where you can see the actual strong references. See https://stackoverflow.com/a/30993476/1271826.

    0 讨论(0)
提交回复
热议问题