swiftmemory-leaksnullclosuresstrong-reference-cycle

Breaking a strong reference cycle with an explicit 'nil' assignment


This question was driving me crazy for a few days now and nobody could yet give me a clear view on what's actually happening. Here is the first snippet of the code

class Animal {
    var name = "Fischer"
    
    var command: () -> Void = { }
    
    deinit {
        print(#function, #line)
    }
}

do {
    var pet: Animal? = Animal()
    pet?.command = { print(pet?.name ?? "Bobby") }
}

This code causes a memory leak, because

  1. Reference 'pet' is created.
  2. Independent copy of the reference 'pet' is created inside the closure. now there are two references to the same object, which are 'pet' outside the closure and 'pet' inside the closure.
  3. As we exit the 'do' scope, the 'pet' reference is deleted, but ARC does not deallocate the object due to the strong reference 'pet', that is still referencing to the same object. And all of that causes a memory leak. Now here is the code, that is pretty similar, except for the fact, that we assign a nil to the 'pet' reference
class Animal {
    var name = "Fischer"
    
    var command: () -> Void = { }
    
    deinit {
        print(#function, #line)
    }
}

do {
    var pet: Animal? = Animal()
    pet?.command = { print(pet?.name ?? "Bobby") }
    pet = nil
}

And boom! deinit is called, meaning that the object was deallocated, but how? Why was the object deallocated? If we are deleting the exact same reference, that was deleted by the end of the 'do' scope in the first snippet? Am I misunderstanding something? I've been struggling to understand this for a lot of time yet I still don't get how it works.


Solution

  • Your reasoning would be correct if the closure were

    { [pet] in print(pet?.name ?? "Bobby") }
    

    The above closure indeed has a strong reference to the Animal object, and the Animal object has a strong reference to the closure, creating a retain cycle.

    However, your closure captures the variable pet. It has a strong reference to whatever object the variable pet refers to. If pet is set to nil, then the closure will not have a strong reference to the object that pet previously refers to.

    Compare:

    do {
        var pet: Animal? = Animal()
        // this closure captures the variable 'pet', so 'pet = nil' later will affect what this closure does
        pet?.command = { print(pet?.name ?? "Bobby") }
        let pet2 = pet
        // the 'pet' in the closure now becomes nil
        pet = nil
        pet2?.command() // this prints "Bobby"
    }
    
    do {
        var pet: Animal? = Animal()
        // this closure captures the object referred to by 'pet' at this moment
        pet?.command = { [pet] in print(pet?.name ?? "Bobby") }
        let pet2 = pet
        // this does not change the object captured by the closure
        pet = nil
        pet2?.command() // this prints "Fischer"
    }
    

    Even after the do block is done, the variable pet is not "deleted" - it is held in the closure. Since it is captured by a closure, it gets put on the heap, and does not stay on the stack.