Retain Cycles and Memory Leaks in Swift

Memory Leaks A memory leak can happen in your Swift application when it is allocating memory but failing to release it back to the OS when it's no longer needed. When coding in Swift (and even in Objective-C), with the level of abstraction that the language has, it's common that you don't think about memory management, but this behavior can lead you to performance issues, or in the worst scenarios to crashes. Today we'll review how and why it happens, and good strategies to debug and fix these problems. Automatic Reference Counting (ARC) Maybe you were just a kid, but in 2011 at the WWDC of that year, Apple announced the Automatic Reference Counting, or just ARC. You can find the video here. Screenshot of the WWDC11 slides presentation introducing ARC With ARC, Apple was trying to introduce an easier way to handle memory and avoid crashes in our apps. For that, ARC automatically (that's the A in the acronym) tracks and manages the memory usage of objects, deallocating them when it identify that it's not needed anymore. With that explanation, it's easy to think that ARC works as a garbage collector, but you'll see below that this is NOT the case. How it works In Swift, every time you instantiate a reference type and sets it to a variable/constant, ARC will literally count (hence the 'C' in the acronym) the number of references to a specific instance. If you set the same reference (not instantianting an equal value again) to another variable or constant, ARC will count one more reference to that instance. class Person { let name: String init(name: String) { self.name = name print("\(name) is initialized") } deinit { print("\(name) is being deinitialized") } } var person1: Person? = Person(name: "Alice") // Reference count = 1 var person2 = person1 // Reference count = 2 This counting value is stored in the Object's Header Metadata, in the Heap Memory. Every time the execution of your code reaches an end of scope (as the end of a function, or the end of an if/else block, for example), ARC will automatically check all the new references made inside this scope, and reduce its value in the counter if it has no references for it: func createPerson() { var person1: Person? = Person(name: "Alice") // Reference count = 1 var person2 = person1 // Reference count = 2 } // Reference count = 0, since 2 new references were made inside the scope Or yet: var person1: Person? = Person(name: "Alice") // Reference count = 1 func createPerson() { var person2 = person1 // Reference count = 2 } // Reference count = 1, since only 1 new reference was made inside the scope ARC will not reduce the counting only when reaching an end of scope. It also controls the counting when explicitly deallocating an object: var person: Person? = Person(name: "Alice") // Reference count = 1 person = nil // Reference count = 0 Every time a reference counting reaches 0, ARC free the space that this object was taking in memory, making it avaible for new allocations. That eliminates the need for a Garbage Collector, avoiding performance issues such as pauses for periodic GC cycles. ARC will also count references when setting reference type objects to other object properties, creating a nested dependency: class Person { var name: String var dog: Dog? init(name: String, dog: Dog?) { self.name = name self.dog = dog } } class Dog { var name: String init(name: String) { self.name = name } } func createPerson() { var alice = Person(name: "Alice", dog: nil) // Reference count to Alice = 1 var fido = Dog(name: "Fido") // Reference count to Fido = 1 alice.dog = fido // Reference count to Fido = 2 } /* Here ARC will start reducing the counting 1. Reduce the `alice` reference count from 1 to 0, since it is not referenced anywhere else. This will trigger the deallocation of the `alice` object. 2. After `alice` is deallocated, the reference count of `fido` will be reduced from 2 to 1, since it was referenced in `alice.dog` but has no other references. 3. Finally, reduce the `fido` reference count from 1 to 0, triggering the deallocation of the `fido` object, as it is no longer referenced anywhere. */ Retain Cycles Even though ARC helps us with memory management, its existence doesn't eliminate all the problems we could have while developing with Swift. The most common issue you may face in that area is a retain cycle. How do Retain Cycles Happen in Swift? Retain cycles can happen in Swift when at least two objects reference each other, closing a cycle. Here’s an example: class Person { var name: String var dog: Dog? init(name: String, dog: Dog?) { self.name = na

Jan 17, 2025 - 16:42
Retain Cycles and Memory Leaks in Swift

Memory Leaks

A memory leak can happen in your Swift application when it is allocating memory but failing to release it back to the OS when it's no longer needed. When coding in Swift (and even in Objective-C), with the level of abstraction that the language has, it's common that you don't think about memory management, but this behavior can lead you to performance issues, or in the worst scenarios to crashes. Today we'll review how and why it happens, and good strategies to debug and fix these problems.

Automatic Reference Counting (ARC)

Maybe you were just a kid, but in 2011 at the WWDC of that year, Apple announced the Automatic Reference Counting, or just ARC. You can find the video here.

Screenshot of the WWDC11 slides presentation introducing ARC Screenshot of the WWDC11 slides presentation introducing ARC

With ARC, Apple was trying to introduce an easier way to handle memory and avoid crashes in our apps. For that, ARC automatically (that's the A in the acronym) tracks and manages the memory usage of objects, deallocating them when it identify that it's not needed anymore.

With that explanation, it's easy to think that ARC works as a garbage collector, but you'll see below that this is NOT the case.

How it works

In Swift, every time you instantiate a reference type and sets it to a variable/constant, ARC will literally count (hence the 'C' in the acronym) the number of references to a specific instance.
If you set the same reference (not instantianting an equal value again) to another variable or constant, ARC will count one more reference to that instance.

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var person1: Person? = Person(name: "Alice") // Reference count = 1
var person2 = person1                        // Reference count = 2

This counting value is stored in the Object's Header Metadata, in the Heap Memory.

Every time the execution of your code reaches an end of scope (as the end of a function, or the end of an if/else block, for example), ARC will automatically check all the new references made inside this scope, and reduce its value in the counter if it has no references for it:

func createPerson() {
   var person1: Person? = Person(name: "Alice") // Reference count = 1
   var person2 = person1                        // Reference count = 2
} // Reference count = 0, since 2 new references were made inside the scope

Or yet:

var person1: Person? = Person(name: "Alice") // Reference count = 1

func createPerson() {
   var person2 = person1 // Reference count = 2
} // Reference count = 1, since only 1 new reference was made inside the scope

ARC will not reduce the counting only when reaching an end of scope. It also controls the counting when explicitly deallocating an object:

var person: Person? = Person(name: "Alice") // Reference count = 1
person = nil                                // Reference count = 0

Every time a reference counting reaches 0, ARC free the space that this object was taking in memory, making it avaible for new allocations. That eliminates the need for a Garbage Collector, avoiding performance issues such as pauses for periodic GC cycles.

ARC will also count references when setting reference type objects to other object properties, creating a nested dependency:

class Person {  
    var name: String  
    var dog: Dog?

    init(name: String, dog: Dog?) {  
        self.name = name  
        self.dog = dog  
    }  
}  

class Dog {  
    var name: String

    init(name: String) {  
        self.name = name
    }  
}

func createPerson() {
   var alice = Person(name: "Alice", dog: nil) // Reference count to Alice = 1
   var fido = Dog(name: "Fido")                // Reference count to Fido = 1

   alice.dog = fido                            // Reference count to Fido = 2
} /* Here ARC will start reducing the counting

1. Reduce the `alice` reference count from 1 to 0, since it is not referenced anywhere else. This will trigger the deallocation of the `alice` object.
2. After `alice` is deallocated, the reference count of `fido` will be reduced from 2 to 1, since it was referenced in `alice.dog` but has no other references.
3. Finally, reduce the `fido` reference count from 1 to 0, triggering the deallocation of the `fido` object, as it is no longer referenced anywhere.

*/

Retain Cycles

Even though ARC helps us with memory management, its existence doesn't eliminate all the problems we could have while developing with Swift. The most common issue you may face in that area is a retain cycle.

Retain Cycle representation

How do Retain Cycles Happen in Swift?

Retain cycles can happen in Swift when at least two objects reference each other, closing a cycle. Here’s an example:

class Person {  
    var name: String  
    var dog: Dog?

    init(name: String, dog: Dog?) {  
        self.name = name  
        self.dog = dog  
    }  
}  

class Dog {  
    var name: String  
    var owner: Person?

    init(name: String, owner: Person?) {  
        self.name = name  
        self.owner = owner  
    }  
}

func createPersonAndDog() {
   var alice = Person(name: "Alice", dog: nil)
   var fido = Dog(name: "Fido", owner: nil)

   alice.dog = fido
   fido.owner = alice
} // Here ARC will fail to deallocate the references made inside the function

createPersonAndDog()

Differently from the previous example, here ARC will not be able to reduce the count for the alice object to 0, because it is referenced somewhere else: in the owner property of the fido object. And it also cannot reduce the fido counting to 0, because it's referenced in alice.dog. This is a Retain Cycle.

How to Prevent Retain Cycles in Swift Code?

In Swift, when you assign an instance of a reference type to anything (variable, constant or property), it creates a reference. But Swift has two types of references: the strong and weak ones. Strong references are the default, and every reference that you saw in this article's code snippets so far are the strong ones. To create weak references, you need to use one of the reserved words for it: weak or unowned. Let's take a look in the first one:

// `Person` class remains unchanged

class Dog {  
    var name: String  
    // Made this property weak
    weak var owner: Person?

    init(name: String, owner: Person?) {  
        self.name = name  
        self.owner = owner  
    }  
}

func createPersonAndDog() {
   var alice = Person(name: "Alice", dog: nil)
   var fido = Dog(name: "Fido", owner: nil)

   alice.dog = fido
   fido.owner = alice
}

createPersonAndDog()

Look that we changed var owner: Person? to weak var owner: Person?

Making the relation "Dog has an Owner" a weak one, solved the retain cycle. But how?

Making a reference weak is just telling to ARC: Please, do not count this reference. The count for it will always be 0, breaking the cycle. Let's analyze the code sequentially for a better understanding:

  1. When we do alice.dog = fido, we create a strong reference to the Persons dog property, so ARC will count +1 there.
  2. However, when we do fido.owner = alice, we are adding an owner to a weak reference (Dog has an owner), so ARC will ignore it and not count that alice is being referenced.
  3. When the execution reaches the end of scope, ARC will check if it's possible to deallocate alice, and now it will be, since it's not referenced anywhere.
  4. After deallocating it, nowhere else will reference fido, so ARC will decrease the reference counter of it to 0, and then deallocate it.

Now, both objects were deallocated, and you have more free space in memory for new allocations.

So, if you need one sentence that summarizes how to avoid a retain cycle, it would be: "When you have a cycle of dependencies, at least one of the references needs to be weak".

Precautions When Creating Weak References

Maybe you now have a question: "You told me that every time an object reference count reaches zero ARC deallocate it. And weak references do not increase the reference count. Why isn't it deallocated immediately after being created?".

And the answer for it is: "It will be. At least if you're doing it wrongly".

XCode Warning indicating that the variable will be immediately deallocated

In this example above, the variable dogOwner is an weak one, which means that the count for it in ARC will always remain zero. So setting a value in it, that is not being referenced anywhere else, will take no effect.

So, in order to set values to weak references, you need to have this value set somewhere else, in a strong reference:

XCode is not complaining anymore

Weak are always Optionals

Another precaution that you should have when working with weak references is that they're always an Optional value:

XCode error message telling that weak vars must be optionals

As I previously mentioned, Swift has the unowned word for also creating weak references. You can use it the exact same way as weak, but with a difference: you don't need to specify that that variable has an optional value. But trust me, it still an Optional:

class Dog {
    var name: String
    unowned var owner: Person

What happens here is that the compiler does an automatic force unwrap to you, as you're adding an ! in everyplace you are using this variable. That's considered by many as a bad smell, and personally, I try to avoid using it.

Conclusion

Today, we explored memory management in Swift, understanding how ARC works, the causes of retain cycles, and strategies to avoid them. These are critical concepts to master and valuable practices to integrate into your development workflow.

Despite our best efforts to prevent memory leaks, unexpected issues can still arise, leading to flawed software. In the next #memorymanagement article, we will delve into How to Debug Memory Leaks with Instruments, Xcode's official tool for memory debugging. Stay tuned!