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.
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.
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 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.