memory-managementswift2closuresweak-referencesstrong-references

convert the reference back to a strong one inside the closure, memory management, swift


I'm experimenting the retain cycle in closure like following

 class Sample {
        deinit {
            print("Destroying Sample")
        }

        func additionOf(a: Int, b:Int) {
            print("Addition is \(a + b)")
        }

        func closure() {                
          dispatch_async(dispatch_get_global_queue(0, 0)) { [weak self] in
            self?.additionOf(3, b: 3)   
            usleep(500)                 
            self?.additionOf(3, b: 5)  
          }
        }
    }

Later at some point, I'm doing

var sample : Sample? = Sample()
sample?.closure()

dispatch_async(dispatch_get_global_queue(0, 0)) {
  usleep(100)
  sample = nil
}

The output will be

Addition is 6
Destroying Sample

This is understandable because self is nil out before doing self?.additionOf(3, b:5)

If I made a change inside the closure by creating another variable which references to [weak self] like following

dispatch_async(dispatch_get_global_queue(0, 0)) { [weak self] in
   guard let strongSelf = self else { return }
   strongSelf.additionOf(3, b: 3)
   usleep(500)
   strongSelf.additionOf(3, b: 5)
}

This time, the output is

Addition is 6
Addition is 8
Destroying C

My question is why strongSelf is not nil out after sample = nil. Is it because it is captured inside the closure before sample = nil


Solution

  • Let's consider the contemporary equivalent to your second example:

    DispatchQueue.global().async { [weak self] in
        guard let self else { return }
    
        self.additionOf(3, b: 3)
        Thread.sleep(forTimeInterval: 0.0005)
        self.additionOf(3, b: 5)
    }
    

    This is a common pattern and it effectively says “if self has been deallocated, return immediately, otherwise establish a strong reference to self, and maintain this strong reference until the closure finishes.”

    So, in your example, if self was not nil when this dispatched block started, so as soon as we hit the guard statement, we now have two strong references to this Sample object, both the original sample reference and this new reference inside this closure.

    So, when your other thread removes its own strong reference, sample, to this Sample object (either by falling out of scope or by explicitly setting sample to nil), this closure’s strong reference is still present and will keep the object from being deallocated (or at least not until this dispatched block finishes).

    In your comment above, you ask:

    I still not understand why strongSelf does not get changed since self is nil out...

    Strong references never get set to nil just because some other strong reference (even if it was the original strong reference) was set to nil. That behavior of a reference getting set to nil only applies to weak references, not to strong references.

    When your code performs sample = nil on the first thread, all that does is remove a strong reference. It does not delete the object or anything like that. It just removes its strong reference. And now that the dispatched block has its own strong reference, all that happens upon sample = nil is that the object that had two strong references now only has one strong reference left (the reference to it in the dispatched block). Only when this final strong reference is removed will the object be deallocated and deinit will be called.


    FWIW, the [weak self] capture list actually does not have much utility if you are immediately dispatching to the global queue. You would probably excise it and just do:

    DispatchQueue.global().async { [self] in
        additionOf(3, b: 3)
        Thread.sleep(forTimeInterval: 0.0005)
        additionOf(3, b: 5)
    }
    

    The [weak self] capture list is most useful if the closure is running at some later point in time, e.g., a completion handler or asyncAfter, where there is a practical chance that self might fall out of scope before the closure runs:

    DispatchQueue.global().asyncAfter(deadline: .now() + 3) { [weak self] in
        guard let self else { return }
    
        self.additionOf(3, b: 3)
        Thread.sleep(forTimeInterval: 0.0005)
        self.additionOf(3, b: 5)
    }