3

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

tonytran
  • 1,068
  • 2
  • 14
  • 24
  • 1
    When you set `sample = nil`, all you're doing is resolving that one particular strong reference to the `Sample`. But it has no effect on any other strong references that might be out there (such as `strongSelf`, which was assigned before you did `sample = nil`). The entire purpose of doing a `strongSelf` pattern inside the `[weak self]` closure (sometimes jokingly called the "strongSelf/weakSelf dance") is to make sure that if that `Sample` wasn't deallocated when that closure started, it will keep it around until that closure completes. – Rob Aug 05 '16 at 01:54
  • @Rob: I think I understood but not 100%. From the OP, when closure starts, Sample has not been deallocated yet until the first `additionOf(3, b: 3)` is executed. I still not understand why `strongSelf` does not get changed since self is nil out.Unless it is captured inside the closure ?? – tonytran Aug 05 '16 at 02:23
  • 1
    It's not that it is "captured" by the closure. Simply that the closure has established a new, second strong reference to the `Sample` object. So, when you called `var sample : Sample? = Sample()`, there was one strong reference. When the code encountered `guard let strongSelf = self else ...`, that established a second strong reference, for a total of two strong references. When you performed `sample = nil` on the main thread, there is still one strong reference remaining. Only when `strongSelf` falls out of scope is this final strong reference resolved and the object is deallocated. – Rob Aug 05 '16 at 04:52

1 Answers1

4

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)
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044