🔁ARC

Fall 2023 | Vin Bui

Have you ever wondered why we had to label our delegate variables as weak? Why do we use weak self inside of a closure? We do this to prevent memory leaks!

What is ARC?

ARC, which stands for Automatic Reference Counting, is Swift’s way of tracking and managing your app’s memory usage. Every time you create a new instance of a class, ARC allocates a chunk of memory to store the class’ information. This includes any stored properties and functions associated with that class instance.

When we no longer need that instance, ARC automatically deallocates memory used up by that instance. For example, when a view controller is displayed on screen, memory is allocated for that instance. When we dismiss that view controller, we want the stored memory to be deallocated.

How does ARC know when to deallocate memory? It uses reference counting to keep track of the number of references to that instance. If there is at least one active reference, then that instance will not be deallocated.

For example, a Person can own three devices: a Laptop, a Phone, and a Watch. Think of each device as a separate class. These three devices have a strong reference pointing to the Person. So in total, there are 4 strong references pointing to this Person instance (including itself). If we were to try to deinitialize this Person (by setting it to nil), then that instance will not be deallocated since there are 3 other strong references.

Retain Cycles

A retain cycle is a condition where two objects (instances) keep a strong reference to each other and are retained, unable to be deinitialized. A strong reference is essentially a normal reference that protects the referred object from being deallocated by increasing it’s retain count by 1.

For example, consider the following two classes. A Person has a dog property and a Dog has an owner property.

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

    init(name: String, dog: Dog) {
        self.name = name
	self.dog = dog
        print("\(name) is being initialized")
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

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

    init(name: String, owner: Person) {
        self.name = name
	self.owner = owner
        print("\(name) is being initialized")
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

By default, when we initialize these properties, a strong reference is retained. If we try to run the code below, the print statement inside of the initializer will be executed.

let vin = Person(name: "Vin", dog: nil)
let chubby = Dog(name: "Chubby", owner: nil)
vin.dog = chubby
chubby.owner = vin

Now, what happens if we set these properties to nil?

vin.dog = nil
chubby.owner = nil

If we try to execute the above code, we will notice that the print statements inside of the deinit function will not be executed, indicating that there is still at least one strong reference to that instance.

How do we prevent this retain cycle? By making one of the properties a weak reference. A weak reference is a pointer to the object that does not protect the referred object from being deallocated. This is possible because the retain count due to a weak reference does not increment by one.

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

    init(name: String, dog: Dog) {
        self.name = name
	self.dog = dog
        print("\(name) is being initialized")
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

class Dog {
    var name: String
    weak var owner: Person? // Make this a weak variable

    init(name: String, owner: Person) {
        self.name = name
	self.owner = owner
        print("\(name) is being initialized")
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

If we try to set the properties to nil again, the print statements inside of the deinit function will now be executed.

Detecting Memory Leaks

What is the problem with retain cycles? If our view controllers are not being deallocated when they should, all of the storage occupied by the view controller’s data will stay put. Imagine having a view controller with a bunch of images. As we all know, images can take up a lot of storage. If the view controller is not deallocated when we dismiss it, the storage taken up by those images will remain. Every time we present that view controller, additional memory will be allocated, increasing the memory usage significantly. This is known as a memory leak!

You can check our app’s memory usage by clicking on the Debug Navigator (3rd icon from the right) in the project navigator, then clicking on Memory.

For a more advanced usage, we can view the memory graph by clicking on this icon:

On the left hand side, we can select the view and see all of the references to that view. Another powerful tool we can use is Instruments. If you want to learn more, this video is very informational.

Weak vs Unowned References

Weak and unowned references are very similar but not the same. They both do not increase the retain count by 1, but unowned references do not have to be an optional. When do we use which? According to Apple,

“Use a weak reference whenever it is valid for that reference to become nil at some point during its lifetime. Conversely, use an unowned reference when you know that the reference will never be nil once it has been set during initialization.”

If you would like to learn more, this blog post explains this concept very well.

Last updated