Understanding Swift Memory Management

What Are Strong Reference Cycles

In the previous episode of this series, you learned about Automatic Reference Counting and how it helps to keep your application's memory usage in check. Remember that a class instance is deallocated if no other objects keep a strong reference to it. In this episode, we zoom in on strong references and reference cycles.

What Is a Strong Reference?

A reference to a class instance is strong by default. Let's revisit an example from the previous episode.

class Device {

    let model: String
    let manufacturer: String

    init(model: String, manufacturer: String) {
        self.model = model
        self.manufacturer = manufacturer
    }

}

var deviceJim: Device?
var deviceJane: Device?

deviceJim = Device(model: "iPhone 7", manufacturer: "Apple")

The deviceJim variable holds a strong reference to the Device instance. Strong simply means that the class instance the reference points to cannot be deallocated as long as the reference exists. A strong reference prevents the class instance it points to from being deallocated.

Take your time to let this sink in. You need to understand what a strong reference is before we can discuss strong reference cycles.

What Is a Reference Cycle?

A strong reference prevents the class instance it points to from being deallocated and that introduces a risk. Let me show you a few examples.

Delegation

The delegation pattern is a simple but powerful pattern and Apple's frameworks make ample use of it. Let's take table views as an example. A table view isn't responsible for handling user interaction. It delegates that responsibility to a delegate, an object that conforms to the UITableViewDelegate protocol. A table view notifies its delegate when the user taps a row in the table view. By delegating user interaction to a delegate, the UITableView class is flexible and reusable.

A view controller that displays a table view usually keeps a strong reference to the table view. Take a look at this example. The ViewController class defines an outlet, tableView, of type UITableView!. The view controller strongly references the table view it displays.

import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var tableView: UITableView!

}

The table view cannot be deallocated as long as the view controller is alive because it keeps a strong reference to the table view. In the didSet property observer of the tableView property, the view controller configures the table view. It sets itself as the delegate and the data source of the table view. This is a common pattern.

import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var tableView: UITableView! {
        didSet {
            // Configure Table View
            tableView.delegate = self
            tableView.dataSource = self
        }
    }

}

The table view keeps a reference to its delegate and, in most scenarios, the view controller is the delegate of the table view. If the reference the table view keeps to its delegate, the view controller in this example, is strong, then the view controller cannot be deallocated as long as the table view is alive.

Do you see the problem? The table view cannot be deallocated as long as the view controller is alive and the view controller cannot be deallocated as long as the table view is alive. Take a look at this diagram to better understand the problem.

A Strong Reference Cycle

This is known as a strong reference cycle, a reference cycle, or a retain cycle. The view controller and the table view keep each other alive. Even if the view controller and the table view are no longer needed, they are not deallocated. They continue to take up memory and resources. The application ends up leaking memory. The solution to resolve this problem isn't complex. We explore it in detail later in this series.

Dependencies

Strong reference cycles can also be caused by objects that depend on one another. Let me illustrate this with another example. When you sign up for a music service, you create an account and you choose a plan. The account references the plan the user chose and the plan references the account the user created.

Because references are strong by default, an account holds a strong reference to a plan. The same is true for a plan. A plan holds a strong reference to an account. This means we end up with another strong reference cycle.

We end up with another strong reference cycle.

Closures

Another common source of reference cycles or retain cycles is through the use of closures. Closures are a powerful tool in your toolbox, but you always need to be mindful of memory management when using them.

Like classes, closures are reference types. Because a closure can capture values from its surrounding scope, a closure can introduce a reference cycle. Let me illustrate this with an example.

Closures or handlers are a common alternative to the delegation pattern. The RootViewController class defines an action, showSettings(_:), to present a SettingsViewController instance to the user. It lazily instantiates an instance of the SettingsViewController class and keeps a reference to the instance in its settingsViewController property. It configures the settings view controller by installing the didModifySettings handler.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    private lazy var settingsViewController: SettingsViewController = {
        // Initialize Settings View Controller
        guard let settingsViewController = UIStoryboard.main.instantiateViewController(withIdentifier: SettingsViewController.storyboardIdentifier) as? SettingsViewController else {
            fatalError("Unable to Instantiate Settings View Controller")
        }

        // Install Handler
        settingsViewController.didModifySettings = {
            self.updateView()
        }

        return settingsViewController
    }()

    // MARK: - Navigation

    @IBAction func showSettings(_ sender: Any) {
        // Present Settings View Controller
        present(settingsViewController, animated: true)
    }

    // MARK: - View Methods

    private func updateView() {}

}

For this example, I have kept the implementation of the SettingsViewController class to a bare minimum. It defines a property, didModifySettings, a closure that accepts no arguments and returns no value. It also defines an action, dismiss(_:), to dismiss itself when the user taps a button.

import UIKit

class SettingsViewController: UIViewController {

    // MARK: - Properties

    var didModifySettings: (() -> Void)?

    // MARK: - Actions

    @IBAction func dismiss(_ sender: Any) {
        dismiss(animated: true)
    }

}

These are the ingredients for an unpleasant surprise, a memory leak caused by a reference cycle. Let's return to the RootViewController class. Notice that the body of the closure that is assigned to the didModifySettings property references self, the RootViewController instance. While this may not seem like a problem, it means that the settings view controller indirectly keeps a strong reference to the root view controller. It does this through its didModifySettings property. This is what the object graph looks like.

The use of closures can easily lead to memory leaks.

The root view controller keeps a strong reference to the settings view controller and the settings view controller indirectly keeps a strong reference to the root view controller. Resolving this reference cycle is very easy, but the first step is being aware of the problem.

Resolving Strong Reference Cycles

In the remainder of this series, we explore solutions to resolve the strong reference cycles we created in this episode. Don't worry, though, the solutions are not difficult to implement.

Fixing a memory leak is usually not difficult, but they are often hard to find. A reference cycle is easy to overlook and that is almost always the problem. It is easy to miss a retain cycle if you're not paying attention.

Next Episode "How to Break a Strong Reference Cycle"