swiftmemory-leaksautomatic-ref-countingstrong-references

Is this a strong reference cycle or memory leak at all?


The examples about strong reference cycles I usually see involve two classes with properties pointing to each other. However, what if only one of the classes has a property pointing to the other instance like this:

class ClassA {
    var classB: ClassB? = nil
}

class ClassB {

}

Then I create my instances like this:

var myClassA = ClassA()
var myClassB = ClassB() //Reference count 1
myClassA.classB = myClassB //Reference count 2

// Now deallocate
myClassB = nil  //Reference count 1
myClassA = nil

Since I've deallocated myClassB, the reference count is 1. What happened to the reference count of myClassA.classB? It never reached zero since I never did myClassA.classB = nil or used deinit to do this. Is this implicitly done since I did myClassA = nil?

Is this what can be categorized as a strong reference cycle? I would imagine that it is at least a memory leak though, is this true?


Solution

  • As @ozgur, @jtbandes, @Avi, and @Rob explained in the comments, there is no strong reference cycle or leak.

    Here is an example based upon @Rob's comment that you can run in a Playground:

    class ClassA {
        var classB: ClassB?
    
        deinit {
            print("ClassA deallocated")
        }
    }
    
    class ClassB {
        deinit {
            print("ClassB deallocated")
        }
    }
    
    class Tester {
        func test() {
            var myClassA: ClassA! = ClassA()
            var myClassB: ClassB! = ClassB() //Reference count 1
            myClassA.classB = myClassB //Reference count 2
    
            // Now deallocate
            print("setting myClassB to nil")
            myClassB = nil  //Reference count 1
            print("setting myClassA to nil")
            myClassA = nil
            print("all done")
        }
    }
    
    // Create `Tester` object and call `test`:
    
    Tester().test()
    

    Output:

    setting myClassB to nil
    setting myClassA to nil
    ClassA deallocated
    ClassB deallocated
    all done
    

    The thing to note is that even though you set myClassB to nil first, that myClassA gets freed first. When myClassA gets freed, the final reference to myClassB is released by ARC and then myClassB is freed.


    To demonstrate a strong reference cycle, have ClassB retain a strong reference to ClassA:

    class ClassA {
        var classB: ClassB?
    
        deinit {
            print("ClassA deallocated")
        }
    }
    
    class ClassB {
        var classA: ClassA?
    
        deinit {
            print("ClassB deallocated")
        }
    }
    
    class Tester {
        func test() {
            var myClassA:ClassA! = ClassA()
            var myClassB:ClassB! = ClassB() //Reference count 1
            myClassA.classB = myClassB //Reference count 2
            myClassB.classA = myClassA
    
            // Now deallocate
            print("setting myClassB to nil")
            myClassB = nil  //Reference count 1
            print("setting myClassA to nil")
            myClassA = nil
            print("all done")
        }
    }
    
    Tester().test()
    

    Output:

    setting myClassB to nil
    setting myClassA to nil
    all done
    

    Neither object is deallocated if they both contain strong reference to the other. To break this strong reference cycle, declare one of the classB or classA properties to be weak. Which one you choose affects the order that the objects get freed:

    If you declare weak var classB: ClassB in ClassA:

    Output:

    setting myClassB to nil
    ClassB deallocated
    setting myClassA to nil
    ClassA deallocated
    all done
    

    If instead, you declare weak var classA: ClassA in ClassB:

    Output:

    setting myClassB to nil
    setting myClassA to nil
    ClassA deallocated
    ClassB deallocated
    all done