ARC and Memory Management in Swift

In this tutorial, you’ll learn how ARC works and how to code in Swift for optimal memory management. You’ll learn what reference cycles are, how to use the Xcode 10 visual debugger to discover them when they happen and how to break them using an example of a reference cycle in practice. By Mark Struzinski.

Leave a rating/review
Download materials
Save for later
Share
Update note: Mark Struzinski updated this tutorial for Xcode 10 and Swift 4.2. Maxime Defauw wrote the original.

As a modern, high-level programming language, Swift handles much of the memory management of your apps and allocates or deallocates memory on your behalf. It does so using a feature of the Clang compiler called Automatic Reference Counting, or ARC. In this tutorial, you’ll learn all about ARC and memory management in Swift.

With an understanding of this system, you can influence when the life of a heap object ends. Swift uses ARC to be predictable and efficient in resource-constrained environments.

ARC works automatically, so you don’t need to participate in reference counting, but you do need to consider relationships between objects to avoid memory leaks. This is an important requirement that is often overlooked by new developers.

In this tutorial, you’ll level up your Swift and ARC skills by learning the following:

  • How ARC works.
  • What reference cycles are and how to break them.
  • An example of a reference cycle in practice.
  • How to detect reference cycles with the latest Xcode visualization tools.
  • How to deal with mixed value and reference types.

Getting Started

Click the Download Materials button at the top or bottom of this tutorial. In the folder named Cycles, open the starter project. For the first part of this tutorial, you’ll be working completely inside MainViewController.swift to learn some core concepts.

Add the following class to the bottom of MainViewController.swift:

class User {
  let name: String
  
  init(name: String) {
    self.name = name
    print("User \(name) was initialized")
  }

  deinit {
    print("Deallocating user named: \(name)")
  }
}

This defines a class User which has print statements to show when you have initialized or deallocated it.

Now, initialize an instance of User at the top of MainViewController.

Put the following code above viewDidLoad():

let user = User(name: "John")

Build and run the app. Make sure the console is visible with Command-Shift-Y so you can see the result of the print statements.

Notice that the console shows User John was initialized and that the print within deinit is never called. This means that the object is never deallocated because it never goes out of scope.

In other words, since the view controller that contains this object never goes out of scope, the object is never removed from memory.

Is That in Scope?

Wrapping the instance of user in a method will allow it to go out of scope, letting ARC deallocate it.

Create a method called runScenario() inside the MainViewController class. Move the initialization of User inside of it.

func runScenario() {
  let user = User(name: "John")
}    

runScenario() defines the scope for the instance of User. At the end of this scope, user should be deallocated.

Now, call runScenario() by adding the following at the end of viewDidLoad():

runScenario()

Build and run again. The console output now looks like this:

User John was initialized
Deallocating user named: John

The initialization and deallocation print statements both appear. These statements show that you’ve deallocated the object at the end of the scope.

An Object’s Lifetime

The lifetime of a Swift object consists of five stages:

  1. Allocation: Takes memory from a stack or heap.
  2. Initialization: init code runs.
  3. Usage.
  4. Deinitialization: deinit code runs.
  5. Deallocation: Returns memory to a stack or heap.

There are no direct hooks into allocation and deallocation, but you can use print statements in init and deinit as a proxy for monitoring those processes.

Reference counts, also known as usage counts, determine when an object is no longer needed. This count indicates how many “things” reference the object. The object is no longer needed when its usage count reaches zero and no clients of the object remain. The object then deinitializes and deallocates.

SchemeOne

When you initialize the User object, it starts with a reference count of one, since the constant user references that object.

At the end of runScenario(), user goes out of scope and the reference count decrements to zero. As a result, user deinitializes and subsequently deallocates.

Reference Cycles

In most cases, ARC works like a charm. As an app developer, you don’t usually have to worry about memory leaks, where unused objects stay alive indefinitely.

But it’s not all smooth sailing. Leaks can happen!

How can these leaks occur? Imagine a situation where two objects are no longer required, but each references the other. Since each has a non-zero reference count, neither object can deallocate.

This is a strong reference cycle. It fools ARC and prevents it from cleaning up.

As you can see, the reference count at the end is not zero, and even though neither is still required, object1 and object2 are never deallocated.

Checking Your References

To see this in action, add the following code after User in MainViewController.swift:

class Phone {
  let model: String
  var owner: User?
  
  init(model: String) {
    self.model = model
    print("Phone \(model) was initialized")
  }

  deinit {
    print("Deallocating phone named: \(model)")
  }
}          

This adds a new class called Phone. It has two properties, one for the model and one for the owner, with init and deinit methods. The owner property is optional, since a Phone can exist without a User.

Next add the following line to runScenario():

let iPhone = Phone(model: "iPhone Xs")

This creates an instance of Phone.

Hold the Phone(s)

Next, add the following code to User, immediately after the name property:

private(set) var phones: [Phone] = []

func add(phone: Phone) {
  phones.append(phone)
  phone.owner = self
}

This adds a phones array property to hold all phones owned by a user. The setter is private, so clients have to use add(phone:). This method ensures that owner is set properly when you add it.

Build and run. As you can see in the console, the Phone and User objects deallocate as expected.

User John was initialized
Phone iPhone XS was initialized
Deallocating phone named: iPhone Xs
Deallocating user named: John

Now, add the following at the end of runScenario():

user.add(phone: iPhone)

Here, you add iPhone to user. add(phone:) also sets the owner property of iPhone to user.

Now build and run, and you’ll see user and iPhone do not deallocate. A strong reference cycle between the two objects prevents ARC from deallocating either of them.

UserIphoneCycle