Advanced Memory Management in Swift

Introduction

In this article, we’ll use unowned references to break retain cycles in the object memory graph, learn how to handle memory in closures, and explore closure capture lists.

Unlike weak references, unowned references are used with non-optional types. Hence, it can’t go away if the reference type it’s pointing to goes away; instead it traps.

Unowned in action

Let’s consider the example of the following Employee class. You can see that when initializing, Employee prints: Employee his/her name initialised 🥳 ; and while de-initializing, the same employee prints: Employee his/her name de-initialised 💀, as shown in the code below:

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 💀")
  }
}

Let’s now create Manager and Worker classes, as follows:

import Foundation

class Manager: Employee {
  var workers: [Worker] = []
}

class Worker: Employee {
  
  let manager: Manager
  
  init(_ name: String, manager: Manager) {
    self.manager = manager
    super.init(name)
  }
}

Next, let’s create some objects inside a scope (do blocks create a scope within playgrounds):

do {
  let manager = Manager("Manager 1")
  let worker = Worker("Worker 1", manager: manager)
  manager.workers.append(worker)
  print(manager.name, "is still in memory!")
  print(worker.name, "is still in memory!")
}

If we run the playground file now, we’ll see the following result:

Even after releasing all the visible references to Manager 1 and Worker 1, both the objects are still in memory and are not de-initialized. Can you think of the reason for this problem?

Issue and Fix

The issue with the code mentioned above is a retain cycle. If we have a look at the memory graph of the above-mentioned block of code, we’ll have something like this:

Both of the objects hold strong references to each other. Because of this, the reference count of each object never becomes zero, even when both the objects go out of scope. This is an example of a retain cycle.

In order to break this strong reference cycle, we can make use of unowned. Unowned notifies the compiler not to increment the counter of the object since the object marked as unowned is not an Optional. This makes it easier to manage these objects rather than resorting to using optional binding.

The modified Worker class will look something like this:

class Worker: Employee {
  
  unowned let manager: Manager
  
  init(_ name: String, manager: Manager) {
    self.manager = manager
    super.init(name)
  }
}

By adding the unowned keyword, we’re explicitly telling the compiler that the manager will outlive any worker, and if this is not true we’ll crash at runtime. So we need to be very careful while using unowned. The use of unowned references in some cases can lead to dangling pointers.

Given that both weak and unowned references don’t increment the reference count for an object, then where do we use them? According to apple:

Closures and Memory

Closures can create memory traps if you’re not careful. In this section, we’ll look at memory management for closures and explore how leaks can happen while working with them.

Swift is a safe language, and by default it will never let us access uninitialized memory—it really works hard to achieve this. In particular, when we make a closure, it makes sure that all the variables we access in the body of the closure are alive. This means it will bump the reference count for any object that’s accessed.

Let’s see this in action. In the following example, we have a counter closure which increments the global variable tickCount by one.

var tickCount = 0

let incrementTickCount : () -> Void = {
    tickCount += 1
    print("Number of ticks =", tickCount)
}

incrementTickCount()
incrementTickCount()
incrementTickCount()
incrementTickCount()

This closure has safe access to the variable tickCounter because of tickCounter’s global scope. If tickCounter was an object, using it inside the closure would bump up its reference count so that the object stays alive until the closure stays alive.

Problems start occurring when one of the objects accessed inside the closure have a reference back to the closure. This situation can be represented like this:

The closure is referring three objects within its scope and strongly holds them. Things are fine until then, but the problem starts occurring when one or more of these three objects starts referring back to the closure (strong reference), as we can see in the image.

It might be possible that Object 3 has created the closure and owns it. This owning relationship gives rise to a retain/reference cycle.

The solution to the problem mentioned above is to break the strong reference cycle by altering how we access Object 3 inside the closure and making that reference as either weak or unowned as shown in the image below:

The block of code shown below provides an example of a leak:

class Person {

    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
        print("(name) Initialised")
    }

    deinit {
        print("(name) De-initialised")
    }

    lazy var growOld: (Int) -> Void = { by in
        self.age += by
        print(self.age)
    }
}

let mark = Person(name: "Mark", age: 10)
mark.growOld(3)

If you run this code in a playground, you’ll find that the result of execution never results in de-initialization. That happens because we have a strong reference cycle in our growOld closure that gets initialized lazily to a closure that increments the age by some amount. The code captures self inside a closure, and the memory is never vacated. You’ll see such behavior inside big iOS ViewControllers that have closure properties.

But is there a way to decide how self got captured? And what is the solution to the problem mentioned above? In the next section we’ll find answers to these questions.

Closure capture list

Let’s have a look at the syntax of closure captures. This will eventually help us break retain cycles. Let’s have a look at the following code sample:

var currentYear = 2019
let printCurrentYear = {
    print("We are in year =", currentYear)
}

printCurrentYear()

We have a variable currentYear and a closure printCurrentYear that prints the value inside the variable.

The printCurrentYear closure captured the global variable value, and to see that this happened we’ve called the function in line 6. The result of this execution prints the following:

As expected, the closure captured currentYear’s value. Let’s now set the value of currentYear to 2020 and see what happens.

The result of this execution prints:

We can see that the value captured by the closure has changed.

Let’s write a closure with a capture list:

let printCurrentYearUsingClosureList = { [currentYear] in
    print("Using closure lists, we are in year =", currentYear)
}
currentYear = 2021
printCurrentYearUsingClosureList()

When we use a capture list, right at the point when the closure is created, the value inside currentYear is captured and stored in an immutable shadow variable also named currentYear. As a result, if we change the value after it’s captured inside the capture list, it won’t impact the result. So the result of executing the code above will result in the following:

As expected, because the variable’s value was captured and assigned to a shadow variable with the same name, any change in the global variable’s value did not reflect in the captured value inside the closure, and the closure prints the old value. We can try changing currentYear once again to 2022 and call printCurrentYearUsingClosureList() again, but the result will remain the same.

Let’s extend the example and try to set the value of currentYear to 2022, but this time we’ll create yet another closure that doesn’t shadow, execute the closure, then change currentYear’s value to 2024 and execute the closure once again, as follows:

currentYear = 2022
let printCurrentYearWithOutShadow = { [curYear = currentYear, nextYear = currentYear + 1] in
    print("We are in year =", curYear, "and next year is =", nextYear)

}
printCurrentYearWithOutShadow()
currentYear = 2024
printCurrentYearWithOutShadow()

The result of the above mentioned sequence prints:

Since the currentYear was already captured and set to local variables curYear and nextYear, changing currentYear to 2024 didn’t update those values.

Reference types

So far, we’ve been working with Value types (currentYear was an Int). Let’s play a little with reference types.

In this section, we’ll create some reference types and try to explore how closures behave while interacting with reference types. We have the following Year class:

class Year {

    var currentYear: String
    init(currentYear: String) {
        self.currentYear = currentYear
    }

    deinit {
        print("Year de-initialised")
    }

    func getYear() { print("Current year is =", currentYear) }
}

Next, let’s create a closure and try to get the value of currentYear from the Year object inside the closure (within a scope and then outside it).

var getCurrentYear : () -> Void
do {
    let year = Year(currentYear: "2019")
    getCurrentYear = { [curYear = year] in
        curYear.getYear()
    }

    getCurrentYear()
    year.currentYear = "2020"
    getCurrentYear()
}
getCurrentYear()

We’ll try to see if year gets deallocated/de-initialized once it goes out of scope.

You can see that we have initialized year inside the scope (within the “do” block), and then we call the getCurrentYear() closure thrice. The result of executing this code prints:

As you can see, the object is not de-initialized, but the new value of currentYear is propagated inside the closure, unlike value types. Since year is not deallocated even when it’s out of scope, we have a memory leak.

Let’s try to fix this memory leak. The root cause of the leak here is a strong reference cycle between the closure getCurrentYear and year.

We can fix it by breaking the strong reference cycle and marking the passed in value as weak, as shown in the code below.

var getCurrentYear : () -> Void
do {
    let year = Year(currentYear: "2019")
    getCurrentYear = { [weak curYear = year] in
        guard let currYear = curYear else {
            print("Unknown year ")
            return
        }
        currYear.getYear()
    }

    getCurrentYear()
    year.currentYear = "2020"
    getCurrentYear()
}
getCurrentYear()

We’ve marked the copied value as a weak reference to year. Once year goes out of scope, curYear will be set to nil, and we won’t have any reference cycle. The result of executing this code will print following to console:

Once year goes out of scope, it’s deinit is invoked, which prints: Year de-initialised to the console. Because of this, when getCurrentYear() is invoked outside the scope, Unknown year is printed on the console since curYear now points to nil.

This is how we can use closure captures to prevent memory leaks in our real life projects.

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 🙂

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