Chapters

Hide chapters

Swift Apprentice: Beyond the Basics

First Edition · iOS 16 · Swift 5.8 · Xcode 14.3

Section I: Beyond the Basics

Section 1: 13 chapters
Show chapters Hide chapters

7. Memory Management
Written by Matt Galloway

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In “Swift Apprentice: Fundamentals - Chapter 15, Advanced Classes”, you explored elementary memory management when examining the class lifetime. You also learned about automatic reference counting (ARC). In most cases, Swift’s memory management works automatically with little to no effort from you.

However, certain relationships between objects sometimes present a problem the compiler can’t help you with. That’s where you come in.

In this chapter, you’ll revisit the concept of reference cycles and learn about resolving them. You’ll also learn to use capture lists in closures to capture values from the enclosing scope to resolve memory management problems. By the end of the chapter, you’ll have mastered the art of breaking reference cycles — but now it’s time to start by learning how they happen.

Reference cycles for classes

Two class instances with a strong reference to each other create a strong reference cycle. This situation can lead to a memory leak if the cycle is never broken. That’s because each instance keeps the other one alive, so their reference counts never reach zero. If no other object has a reference to either of the objects, this will likely result in a leak since there is no way to access them for deallocation, even though they may no longer be in use.

For example, our website has a mountain of top-notch programming tutorials, most of which an editor scrutinizes before you see them. Create a new playground and add the following code:

class Tutorial {
  let title: String
  var editor: Editor?

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

  deinit {
    print("Goodbye tutorial \(title)!")
  }
}

This class models a tutorial. In addition to a title property, a tutorial might have an editor — or it might not. It’s optional. Recall that Swift automatically calls the deinitializer, deinit, and releases the object from memory when the reference count drops to zero.

Now that you’ve defined an editor for each tutorial, you need to declare an Editor class, like so:

class Editor {
  let name: String
  var tutorials: [Tutorial] = []

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

  deinit {
    print("Goodbye editor \(name)!")
  }
}

Each editor has a name and a list of tutorials they have edited. The tutorials property is an array that you can add to.

Now, define a brand-new tutorial for publishing and an editor to ensure it meets our high standards:

do {
  let tutorial = Tutorial(title: "Memory Management")
  let editor = Editor(name: "Ray")
}

This code and subsequent examples use do {} to add a new scope. Any references created in a scope will be cleared at the end of the scope. In this case above, tutorial and editor will be cleared at the closing brace of the do {} scope. We expect both tutorial and editor to be deallocated because nothing is referencing them after these references are cleared.

Run the code above, and you’ll see the following in the console:

Goodbye editor Ray!
Goodbye tutorial Memory Management!

This output is what you might expect. If you’re wondering why it’s in that order, it’s because reference counts decrement in the reverse order of creation. Hence, the Editor reference decrements to zero first, and the Editor object deallocates since no more references exist. Then, the Tutorial reference decrements to zero, and the Tutorial object deallocates.

Note: You should be careful about relying on the exact ordering of deallocation as it is still an area of discussion and development. The direction is for it to become more stable and predictable as it works in the playground, but different versions of Swift may behave differently when compiler optimizations are enabled.

Now add the following code:

do {
  let tutorial = Tutorial(title: "Memory Management")
  let editor = Editor(name: "Ray")
  tutorial.editor = editor
  editor.tutorials.append(tutorial)
}

Although both references go out of scope and decrement, deinitializers aren’t called, and nothing prints to the console — bummer! You created a reference cycle between the tutorial and its corresponding editor. The runtime system never releases the objects from memory even though you don’t need them anymore.

Notice how the objects don’t deallocate, but there isn’t a way to access them since you no longer have any variable you can refer to after the do {} scope finishes. This situation is a memory leak.

Now that you understand how reference cycles happen, you can break them. Weak references to the rescue!

Weak references

Weak references are references that don’t play any role in the ownership of an object. The great thing about using them is that they automatically detect when the underlying object has disappeared. This automatic detection is why you always declare them with an optional type. They become nil once the reference count of the referenced object reaches zero.

weak var editor: Editor?
Goodbye editor Ray!
Goodbye tutorial Memory Management!

Unowned References

You have another means to break reference cycles: Unowned references. These behave like weak ones in that they don’t change the object’s reference count.

class Tutorial {
  let title: String
  let author: Author  
  weak var editor: Editor?

  init(title: String, author: Author) {
    self.title = title
    self.author = author
  }
  
  deinit {
    print("Goodbye tutorial \(title)!")
  }
}
class Author {
  let name: String
  var tutorials: [Tutorial] = []

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

  deinit {
    print("Goodbye author \(name)!")
  }
}
do {
  let author = Author(name: "Alice")
  let tutorial = Tutorial(title: "Memory Management", 
                          author: author)
  let editor = Editor(name: "Ray")                         
  author.tutorials.append(tutorial)
  tutorial.editor = editor
  editor.tutorials.append(tutorial)
}
Goodbye editor Ray!
class Tutorial {
  unowned let author: Author
  // original code
}
Goodbye editor Ray!
Goodbye author Alice!
Goodbye tutorial Memory management!

Reference Cycles with Closures

In Chapter 8 of the Fundamentals book, “Collection Iteration With Closures”, you learned that closures capture values from the enclosing scope. Because Swift is a safe language, closures extend the lifetime of any object they use to guarantee those objects are alive and valid. This automatic safety is convenient, but the downside is that you can inadvertently create a reference cycle if you extend the lifetime of an object that captures the closure. Closures, you see, are reference types themselves.

lazy var description: () -> String = {
  "\(self.title) by \(self.author.name)"
}
print(tutorial.description())

Capture Lists

Capture lists are a language feature to help you control exactly how a closure extends the lifetime of instances it references. Capture lists are lists of variables captured by a closure. They appear at the beginning of the closure before any arguments.

var counter = 0
var fooClosure = {
  print(counter)
}
counter = 1
fooClosure()
counter = 0
fooClosure = { [c = counter] in 
  print(c)
}
counter = 1
fooClosure()
counter = 0
fooClosure = { [counter] in
  print(counter)
}
counter = 1
fooClosure()

Unowned Self

Take another look at the code you have for your description lazy property on Tutorial:

lazy var description: () -> String = {
  "\(self.title) by \(self.author.name)"
}
lazy var description: () -> String = {
  [unowned self] in
  "\(self.title) by \(self.author.name)"
}
Memory management by Alice
Goodbye editor Ray!
Goodbye author Alice!
Goodbye tutorial Memory management!

Weak Self

Sometimes you can’t capture self as an unowned reference because it might become nil. Consider the following example:

let tutorialDescription: () -> String
do {
  let author = Author(name: "Alice")
  let tutorial = Tutorial(title: "Memory Management", 
                          author: author)
  tutorialDescription = tutorial.description
}
print(tutorialDescription())
lazy var description: () -> String = {
  [weak self] in
  "\(self?.title) by \(self?.author.name)"
}
nil by nil

The Weak-Strong Pattern

The weak-strong pattern (sometimes affectionately called the weak-strong dance) also does not extend the lifetime of self but converts the weak reference to a strong one after it enters the closure:

lazy var description: () -> String = {
  [weak self] in
  guard let self else {
    return "The tutorial is no longer available."
  }
  return "\(self.title) by \(self.author.name)"
}

Rules of Capturing self in Closures

There are a few rules to be aware of when capturing self in closures. The rules are there to help you avoid making accidental memory-management mistakes.

class Calculator {
  let values: [Int]

  init(values: [Int]) {
    self.values = values
  }

  func add() -> Int {
    return values.reduce(into: 0) { $0 += $1 }
  }

  func multiply() -> Int {
    return values.reduce(into: 1) { $0 *= $1 }
  }

  func calculate() {
    let closure = {
      let add = add()
      print("Values added = \(add)")
      let multiply = multiply()
      print("Values multiplied = \(multiply)")
    }
    closure()
  }
}

Call to method 'add' in closure requires explicit use of 'self' to make capture semantics explicit
Call to method 'multiply' in closure requires explicit use of 'self' to make capture semantics explicit
// Option 1: Explicitly capture `self`
func calculate() {
  let closure = { [self] in
    let add = add()
    print("Values added = \(add)")
    let multiply = multiply()
    print("Values multiplied = \(multiply)")
  }
  closure()
}

// Option 2: Write `self.` before method calls
func calculate() {
  let closure = {
    let add = self.add()
    print("Values added = \(add)")
    let multiply = self.multiply()
    print("Values multiplied = \(multiply)")
  }
  closure()
}
struct Calculator {
  let values: [Int]

  init(values: [Int]) {
    self.values = values
  }

  func add() -> Int {
    return values.reduce(into: 0) { $0 += $1 }
  }

  func multiply() -> Int {
    return values.reduce(into: 1) { $0 *= $1 }
  }

  func calculate() {
    let closure = {
      let add = add()
      print("Values added = \(add)")
      let multiply = multiply()
      print("Values multiplied = \(multiply)")
    }
    closure()
  }
}

Escaping Closures

In “Swift Fundamentals: Chapter 8 - Collection Iteration With Closures”, the closures you used as arguments were marked non-escaping. This designation means you can rest assured that a closure argument will not be called after the function returns. Such is the case for map, filter, reduce, sort and more.

final class FunctionKeeper {
  // 1
  private let function: () -> Void

  // 2
  init(function: @escaping () -> Void) {
    self.function = function
  }

  // 3
  func run() {
    function()
  }
}
let name = "Alice"
let f = FunctionKeeper {
  print("Hello, \(name)")
}
f.run()

Challenges

Before moving on, here are some challenges to test your memory-management knowledge. It’s best to try and solve them yourself, but solutions are available with the download or at the printed book’s source code link in the introduction if you get stuck.

Challenge 1: Break the Cycle

Break the strong reference cycle in the following code:

class Person {
  let name: String
  let email: String
  var car: Car?

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

  deinit {
    print("Goodbye \(name)!")
  }
}

class Car {
  let id: Int
  let type: String
  var owner: Person?

 init(id: Int, type: String) {
   self.id = id
   self.type = type
 }

 deinit {
   print("Goodbye \(type)!")
 }
}

var owner: Person? = Person(name: "Alice", 
                            email: "alice@wonderland.magical")
var car: Car? = Car(id: 10, type: "BMW")

owner?.car = car
car?.owner = owner

owner = nil
car = nil

Challenge 2: Break Another Cycle

Break the strong reference cycle in the following code:

class Customer {
  let name: String
  let email: String
  var account: Account?

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

  deinit {
    print("Goodbye \(name)!")
  }
}

class Account {
  let number: Int
  let type: String
  let customer: Customer

  init(number: Int, type: String, customer: Customer) {
    self.number = number
    self.type = type
    self.customer = customer
  }

  deinit {
    print("Goodbye \(type) account number \(number)!")
  }
}

var customer: Customer? = Customer(name: "George", 
                                   email: "george@whatever.com")
var account: Account? = Account(number: 10, type: "PayPal", 
                                customer: customer!)

customer?.account = account

account = nil
customer = nil

Challenge 3: Break This Retain Cycle Involving Closures

Break the strong reference cycle in the following code:

class Calculator {
  var result: Int = 0
  var command: ((Int) -> Int)? = nil

  func execute(value: Int) {
    guard let command = command else { return }
    result = command(value)
  }

  deinit {
    print("Goodbye MathCommand! Result was \(result).")
  }
}

do {
  var calculator = Calculator()
  calculator.command = { (value: Int) in
    return calculator.result + value
  }

  calculator.execute(value: 1)
  calculator.execute(value: 2)
}

Key Points

  • Use a weak reference to break a strong reference cycle if a reference may become nil at some point in its lifecycle.
  • Use an unowned reference to break a strong reference cycle when you know a reference always has a value and will never be nil.
  • You must use self inside a closure’s body of a reference type. This requirement is a way the Swift compiler hints that you need to be careful not to make a circular reference.
  • Capture lists define how you capture values and references in closures.
  • The weak-strong pattern converts a weak reference to a strong one.
  • An escaping closure is a closure parameter that can be stored and called after the function returns. You should consider the capture list of escaping closures carefully because their lifetimes can be arbitrarily extended.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now