Swift 5: Memory Management

It would be great if we as devs got to play with limitless memory and never had to care about working with it judiciously. Unfortunately, that isn’t true, and hence, we have to behave like a renter to the OS—rent the memory for a while, use the memory, and then hand it back.

Swift is a smart language, and it knows that many devs don’t like handing the memory back to the environment; hence, it keeps track of the allocated memory using a mechanism called ARC (automatic reference counting).

ARC vs Garbage collection(GC)

Many programming languages use garbage collection to collect unused memory, whereas swift uses ARC.

ARC is technically a form of Garbage collection. However, while talking about garbage collection, we’re referring to a separate process that runs independent of an application. As such, memory deallocation cannot be predicted with GC. Also, under low memory conditions, garbage collection might halt thread execution, which requires a lot more resources to run smoothly.

ARC runs part of the application code, and hence it’s very deterministic. Objects get deallocated as soon as they are not required. In contrast with GC, though, ARC is unable to detect reference cycles. Therefore, ARC requires developers to understand the relationship between reference objects in the system and prevent making ownership/reference cycles.

Avoiding Reference Cycles

The main thing a developer needs to do is prevent the reference cycles in ownership. When we reference an object, the default implementation that comes to mind is a strong reference. To monitor retain cycles, we need to watch out for parent-child relationships and make sure that a child doesn’t reference the parent “STRONGLY”, and is instead marked as a non-owning relationship i.e. either weak or unowned.

Reference Cycles

In this section, we’ll see how to use weak references to break reference cycles in the run time object graph. First, we’ll create a retain cycle in our code and then break that retain cycle by making use of weak references.

We have an Employee class as follows:

import Foundation

open class Employee {
  
  public var name: String
  
  public init(_ name: String) {
    self.name = name
    print("Employee (name) initialised 🥳")
  }
  
  deinit {
    print("Employee (name) de-initialised 💀")
  }
}

We can see that when an Employee is initialised, we’ll print the message Employee (employee name) initialised 🥳, and when the same employee is de-initialised, we’ll print Employee (name) de-initialised 💀 so that we can observe the lifetime of the Employee objects.

Let’s subclass the Employee class to create a Manager class and a Worker class as shown below:

public class Manager: Employee {
    var reports: [Employee] = []
}

public class Worker: Employee {
    var manager: Employee?
}

do {
    let manager = Manager("Manager")

    let employee1 = Worker("Employee 1")
    employee1.manager = manager

    let employee2 = Worker("Employee 2")
    employee2.manager = manager

    let employee3 = Worker("Employee 3")
    employee3.manager = manager

    manager.reports = [employee1, employee2, employee3]
}

Once we have both these classes, we create one Manager object manager, three Worker objects— employee1, employee2, employee3 —and then we assign respective manager and reports properties, as shown in the code snippet.

Can you predict what will be printed if we run our playground now?

Retain Cycle

The execution results will print the following on our Xcode console:

We can see that all four objects are initialised as expected, but none of them were de-initialised, even as we went out of scope. Why was that?

To answer this question, let’s have a look at the memory graph for these four objects that got initialised in scope:

As you can see, manager holds a strong reference to its employees, and in return each employee holds a strong reference to the manager. Hence, when manager goes out of scope, its reference count is 3, so it can’t be de-initialised. Similarly, each employee has a reference count of 1, so none of them will be de-initialised, and we have a memory leak.

“Weak” to the rescue

We can fix the problem by marking the manager relationship in the Worker classes as weak.

The property marked with weak is always optional, so that when the non-owned object goes away, the optional property will automatically switch to nil. So all we need to do is safely handle the optional type and our work is done 🙂

Let’s fix code in the example above.

public class Worker: Employee {
    // Adding weak to this property will break the retain cycle
    weak var manager: Employee?
}

All we needed to do was to add the keyword weak to the optional manager property in the Worker class.

Please keep in mind that if you set any property observers on weak properties, they won’t be called when ARC sets that reference to nil.

Retain cycle broken

The execution result will now print the following on our Xcode console:

All the objects that were initialised are now de-initialised as expected once they’re out of scope. If the reference to the manager goes away, nothing owns it anymore, and hence the manager is de-initialised. And since the reference from the reports goes away when the manager is deallocated, all the reports go away, too. Now the object graph looks as follows:

In upcoming article we will go through unowned references and closures.

For other updates you can follow me on Twitter on my twitter handle @NavRudraSambyal

Thanks for reading, please share it if you found it useful 🙂

Avatar photo

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *

wix banner square