Recently I was having a hard time tracking down a bug that was the cause of performance problems in the app I was working on. It took me a while but it turned out that these performance problems were caused by a memory leak. After this experience I thought it would be a good time to learn more about memory management in Swift which resulted in this blogpost. Lets start with the basics.
What is memory management?
Memory refers to all the hardware involved in storing information on your device. Your iPhone has two main ways of storing data 1. the disk 2. Random Access Memory (RAM). When an app is run on your iPhone a file containing all executable instructions will be loaded into the RAM. At the same time the system will claim a chunk of the RAM, which is called the heap. This is the place where all instances of our classes will live while the app is running. When we talk about memory management we refer to the process of managing heap memory. This means managing the life cycles of objects on the heap and making sure that these objects are freed when they are no longer needed so the memory can be reused. In Objective-C aside from primitives like Int, CGRect etc. everything is an object and therefore will be allocated on the heap. In Swift reference types are allocated on the heap, but values types are not. Managing the heap memory is very important because objects can be large and our apps get only so much memory from the system. Running low on memory will cause an iOS App to run slower and eventually will make the app crash. Although nowadays it is getting more rare to see a RAM overload since our devices are getting more powerful its always important to be a good memory citizen.
Reference counting
In Swift memory management is handled by Automatic Reference Counting (ARC). Whenever you create a new instance of a class ARC allocates a chunk of memory to store information about the type of instance and values of stored properties of that instance. Every instance of a class also has a property called reference count, which keeps track of all the properties, constants, and variables that have a strong reference to itself. A strong reference is basically a pointer that increments the reference count of the instance of a class it is pointing to with one. Whenever the reference count of an object reaches zero that object will be deallocated. This way ARC keeps class instances in memory as long as they are needed and frees up memory when they are no longer needed.
ARC has been around since iOS 5, and before that time developers used a system called Manual Reference Counting (MRR). MRR is not that different from ARC, and actually shares the same reference counting system and memory conventions, except that with ARC the compiler adds all the memory management code for you at compile time. To have a better understanding of how ARC works it is good to know more about MRR and its conventions:
1.You own any object you create
2.You can take ownership of an object using retain
3.When you no longer need it you must relinquish ownership of an object you own
4.You must not relinquish ownership of an object you do not own
Based on these conventions developers would decide when they needed to retain (increment reference count) or release (decrement reference count) an object by sending it the message retain or release. Then there was also autorelease with which you declare that you don’t want to own an object beyond the scope in which you sent autorelease. It then gets added to the autoreleasepool, which when it gets drained sends release to all the objects in the pool. This usually happens when the current runloop ends but it can be sooner. So with autorelease you basically say “I want this instance to be released some time in the future”. Autorelease is very handy when you are passing instances around in your code, for example when you have a method that creates and returns an instance of a class you can use autorelease to make the caller responsible for managing its memory.
Strong reference cycles
So Swift does most of the memory management for us, Awesome! Does that mean we as developers don’t have to think about memory management anymore? The answer is no, because with ARC we always have to be careful not to create any strong reference cycles. You create a strong reference cycle when two instances of a class both hold strong references to each other. Because in this situation the reference count of those objects will never get to zero they will hold each other in memory until the application terminates. This is called a memory leak. Strong reference cycles are not always easy to find but a common pitfall is working with closures. This is best illustrated with an example.
Shiny features is a basic app that lets you search on GitHub for repositories of projects that demonstrate new iOS and Swift features announced at this years WWDC. When you first run the app you are greeted with a simple tableview thats lists some new Swift and iOS features. Once you select one of those topics you will be taken to another screen which fetches and presents a list of GitHub repositories found on that topic. When you select a repository you navigate to the RepositoryDetailViewController which will load the GitHub page of that repository in a UIWebView.
In this example there are two classes which are important to have a look at:
RepositoryTableViewModel
RepositoryViewModel has a loadRepositories function which fires of a network request to the GithubAPI to load repositories for the selected iOS 11 or Swift feature. It also contains a stored property called changeHandler which gets called everytime the repositories get updated.
final class RepositoryTableViewModel {
typealias ChangeHandler = (() -> Void)
var repositories: [GitHubRepo] = [] {
didSet {
self.changeHandler?()
}
}
private let apiService: GithubAPIService
var changeHandler: ChangeHandler?
init(apiService: GithubAPIService = GithubAPIService()) {
self.apiService = apiService
}
func loadRepositories(feature: String) {
apiService.loadRepositories(searchTerm: feature) { result in
DispatchQueue.main.async {
switch result {
case .success(let repositories):
self.repositories = repositories
case .error:
// TODO: - Handle Error
break
}
}
}
}
}
RepositoryTableViewController
This is a simple UITableViewController. In viewDidLoad we bind the viewController to the viewModel by setting its changeHandler and in viewWillAppear we tell the view model to load data.
final class RepositoryTableViewController: UITableViewController {
private let feature: String
private let viewModel: RepositoryTableViewModel
init(feature: String, viewModel: RepositoryTableViewModel) {
self.feature = feature
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "RepositoryCell")
viewModel.changeHandler = {
self.tableView.reloadData()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
viewModel.loadRepositories(feature: feature)
}
...
}
To make it a bit easier to spot strong reference cycles I have added a breakpoint that makes a popping sound everytime a viewcontroller gets deallocated (Many thanks to Cédric Luthi for sharing this with the community). Now I want you to run the app, pick a feature e.g. ARKit and then select a repository so that you will end up on a detail page. When you are on the detail page tapp the back button to go back to the RepositoryTableViewController. Because the detailViewController gets popped of the stack it is not referenced anymore so it will get deallocated and you should hear the sound when that happens. So far so good. You would expect the same thing to happen when we again press the backbutton but somehow our viewcontroller is retained and we do not hear any sound. lets find out why?
Remember what I said about being careful with closures. It turns out that in this example there is a strong reference cycle between the viewModel and the viewcontroller. Our viewModel gets stored in a property of the viewcontroller and since all properties in Swift are strong by default it gets retained. Our changehandler gets retained by the viewModel and also retains our viewcontroller because it references self to reload the tableview. How can we solve this?
This can be solved by using a capture list. A capture list defines the rules to use when capturing one or more reference types within the closures body. In our case we can use this to create a weak or unowned reference to self. A weak or unowned reference is basically a reference that does not increment the reference count of the object it points to. You create a weak reference when the captured reference may become nil at some point in the future. You create an unowned reference when the closure and the instance will always refer to each other and have the same lifetime, meaning they will get deallocated at the same time. Unlike weak references unowned references are non optional and therefore a bit easier to handle but you have to be sure that at the time the closure gets called self still exists otherwise your app will crash. Now go ahead and make the following change in RepositoryTableViewController and press CMD+R to run the app.
override func viewDidLoad() {
super.viewDidLoad()
...
viewModel.changeHandler = { [weak self] in
self?.tableView.reloadData()
}
}
There you go! By capturing self as weak in our changeHandler RepositoryTableViewController and its viewmodel will deallocate once the viewcontroller is popped of the stack, thus we have broken up the strong reference cycle.
Thanks for reading this article and if you have any questions, comments or feedback let me know either through a comment in the comments section below or on Twitter @kairadiagne.