Ir al contenido

Blog


How to detect iOS memory leaks and retain cycles using Xcode’s memory graph debugger

May 22, 2019

|
Vince Chau

Vince Chau

At DoorDash we are consistently making an effort to increase our user experience by increasing our app's stability. A major part of this effort is to prevent, fix and remove any retain cycles and memory leaks in our large codebase. In order to detect and fix these issues, we have found the Memory Graph Debugger to be quick and easy to use. After significantly increasing our OOM-free session rate on our Dasher iOS app, we would like to share some tips on avoiding and fixing retain cycles as well as a quick introduction using Xcode’s memory graph debugger for those who are not familiar.

If pinpointing root causes of problematic memory is interesting to you, check out our new blog post Examining Problematic Memory in C/C++ Applications with BPF, perf, and Memcheck for a detailed explanation of how memory works.

I. What are retain cycles and memory leaks?

A memory leak in iOS is when an amount of allocated space in memory cannot be deallocated due to retain cycles. Since Swift uses Automatic Reference Counting (ARC), a retain cycle occurs when two or more objects hold strong references to each other. As a result these objects retain each other in memory because their retain count would never decrement to 0, which would prevent deinit from ever being called and memory from being freed.

II. Why should we care about memory leaks?

Memory leaks increase the memory footprint incrementally in your app, and when it reaches a certain threshold the operating system (iOS) this triggers a memory warning. If that memory warning is not handled, your app would be force-killed, which is an OOM (Out of memory) crash. As you can see, memory leaks can be very problematic if a substantial leak occurs because after using your app for a period of time, the app would crash.

In addition, memory leaks can introduce side effects in your app. Typically this happens when observers are retained in memory when they should have been deallocated. These leaked observers would still listen to notifications and when triggered the app would be prone to unpredictable behaviors or crashes. In the next section we will go over an introduction to Xcode's memory graph debugger and later use it find memory leaks in a sample app.

III. Introduction to Xcode’s Memory Graph Debugger

To open, run your app (In this case I am running a demo app) and then tap on the 3-node button in between the visual debugger and location simulator button. This will take a memory snapshot of the current state of your app.

The memory graph debugger button

The left panel shows you the objects in memory for this snapshot followed by the number of instances of each class next to it's name.

ex: (MainViewController(1))

The classes in-memory in Xcode

Signifies that there is only one MainViewController in memory at the time of the snapshot, followed by the address of that instance in memory below.

Manténgase informado con las actualizaciones semanales

Suscríbase a nuestro blog de ingeniería para estar al día de los proyectos más interesantes en los que trabaja nuestro equipo.

If you select an object on the left panel, you will see the chain of references that keep the selected object in memory. For example, selecting 0x7f85204227c0 under MainViewController would show us a graph like this:

The memory graph with the strong referencing and unknown referencing
  • The bold lines mean there is a strong reference to the object it points to.
  • The light gray lines mean there is an unknown reference (could be weak or strong) to the object it points to.
  • Tapping an instance from the left panel will only show you the chain of references that is keeping the selected object in memory. But it will not show you what references that the selected object has references to.

For example, to verify that there is no retain cycle in the objects which MainViewController has a strong reference to, you would need to look at your codebase to identify the referenced objects, and then individually select each of the object graphs to check if there is a retain cycle.

In addition, the memory graph debugger can auto-detect simple memory leaks and prompt you warnings such as this purple ! mark. Tapping it would show you the leaked instances on the left panel.

The retain cycles automatically detected by Xcode

Please note that the Xcode’s auto-detection does not always catch every memory leak, and oftentimes you will have to find them yourself. In the next section, I will explain the approach to using the memory graph debugger for debugging.

IV. The approach to using the Memory Graph Debugger

A useful approach for catching memory leaks is running the app through some core flows and taking a memory snapshot for the first and subsequent iterations.

  1. Run through a core flow/feature and leave it, then repeat this several times and take a memory snapshot of the app. Take a look at what objects are in-memory and how much of each instance exists per object.
  2. Check for these signs of a retain cycle/memory leak:
    • In the left panel do you see any objects/classes/views and etc on the list that should not be there or should have been deallocated?
    • Are there increasingly more of the same instance of a class that is kept in memory? ex: MainViewController (1) becomes MainViewController (5) after going through the flow 4 more iterations?
    • Look at the Debug Navigator on the left panel, do you notice an increase in Memory? Is the app now consuming a greater amount of megabytes (MB) than before despite returning to the original state
  3. If you have found an instance that shouldn’t be in memory anymore, you have found a leaked instance of an object.
  4. Tap on that leaked instance and use the object graph to track down the object that is retaining it in memory.
  5. You may need to keep navigating the object graphs as you track down what is the parent node that is keeping the chain of objects in memory.
  6. Once you believe you found the parent node, look at the code for that object and figure out where the circular strong referencing is coming from and fix it.

In the next section, I will go through an example of common use cases of code that I’ve personally seen that causes retain cycles. To follow along, please download this sample project called LeakyApp.

V. Fixing memory leaks with an example

Once you have downloaded the same Xcode project, run the app. We will go through one example using the memory graph debugger.

  1. Once the app is running you will see three buttons. We will go through one example so tap on “Leaky Controller”
  2. This will present the ObservableViewController which is just an empty view with a navigation bar.
  3. Tap on the back navigation item.
  4. Repeat this a few times.
  5. Now take a memory snapshot.

After taking a memory snapshot, you will see something like this:

Snapshot of the retain cycles and leaked instances of the classes

Since we repeated this flow several times, once we return back to the main screen MainViewController the observable view controller should have been deallocated if there were no memory leaks. However, we see ObservableViewController (25) in the left panel, which means we have 25 instances of that view controller still in memory! Also note that Xcode did not recognize this as a memory leak!

Now, tap on ObservableViewController (25). You will see the object graph and it would look similar to this:

Closure holding a strong referencing causing a memory leak

As you can see, it shows a Swift closure context, retaining ObservableViewController in memory. This closure is retained in memory by __NSObserver. Now let’s go to the code and fix this leak.

Now we go to the file ObservableViewController.swift. At first glance, we have a pretty common use case:
https://gist.github.com/chauvincent/33cf83b0894d9bb12d38166c15dd84a5
We are registering an observer in viewDidLoad and removing self as an observer in deinit. However, there is one tricky usage of code here:
https://gist.github.com/chauvincent/b191414d54ba4cbb04614b1f85ac2e24
We are passing a function as a closure! Doing this captures self strongly by default. You may refer back to the object graph as proof that this is the case. NotificationCenter seems to keep a strong reference to the closure, and the handleNotification function holds a strong reference to self, keeping this UIViewController and objects it holds strong references to in memory!

We can simply fix this by not passing a function as a closure and adding weak self to the capture list:

https://gist.github.com/chauvincent/a35a8f08c7dd4fc183ab2bd5b2ba5e6d

Now rebuild the app and re-run that flow several times and verify that the object has now been deallocated by taking a memory snapshot.

You should see something like this where ObservableViewController is nowhere on the list after you have exited the flow!

Snapshot of the memory graph after fixing the memory leak

The memory leak has been fixed! ? Feel free to test out the other examples in the LeakyApp repo, and read through the comments. I have included comments in each file explaining the causes of each retain cycle/memory leak.

VI. Additional tips to avoid retain cycles

  1. Keep in mind that using a function as a closure keeps a strong reference by default. If you have to pass in a function as a closure and it causes a retain cycle, you can make an extension or operator overload to break strong reference. I won’t be going over this topic but there are many resources online for this.
  2. When using views that have action handlers through closures, be careful to not reference the view inside its own closure! And if you do, you must use the capture list to keep a weak reference to that view, with the closure that the view has a strong reference to.

For example, we may have some reusable view like this:

https://gist.github.com/chauvincent/b2da3c76b0b811c947487ef3bf171d5a

In the caller, we have some presentation code like this:

https://gist.github.com/chauvincent/c049136b236c8b358d81ad16168a0243

This is a retain cycle here because someModalVC’s actionHandler captures a strong reference to someModalVC. Meanwhile someModalVC holds a strong reference to the actionHandler

To fix this:

https://gist.github.com/chauvincent/fe868818e9be6f61cf3bc032539ff3a8

We need to make sure the reference to someModalVC is weak by updating the capture list with [weak someModalVC] in to break the retain cycle.

3. When you are declaring properties on your objects and you have a variable that is a protocol type, be sure to add a class constraint and declare it as weak if needed! This is because the compiler will give you an error by default if you do not add a class constraint.  Although It is pretty well known that the delegate in the delegation pattern is supposed to be weak, but keep in mind that this rule still applies for other abstractions and design patterns, or any protocol variables you declare.

For example, here we a stubbed out clean swift pattern:

https://gist.github.com/chauvincent/8882082ea1280c722955b4803ca6854b
https://gist.github.com/chauvincent/15f52e6908a70ea36d099a16d2d660e2

Here, we need the OrdersListPresenter’s view property must be a weak reference or else we will have a strong circular reference from the View -> Interacter -> Presenter -> View. However when updating that property to weak var view: OrdersListDisplayLogic we will get a compiler error.

Errors from not adding a class-bound protocol while making a reference weak

 This compiler error may look discouraging to some when declaring a protocol-typed variable as weak! But in this case, you have to fix this by adding a class constraint to the protocol!

https://gist.github.com/chauvincent/bbc2c2fc42df62bad61a9d4c49b0290e

Overall, I have found using Xcode Memory Graph Debugger to be a quick and easy way to find and fix retain cycles and memory leaks! I hope you find this information useful and keep these tips in mind regularly as you develop! Thanks!

About the Author

Trabajos relacionados

Ubicación
Oakland, CA; San Francisco, CA
Departamento
Ingeniería
Ubicación
Oakland, CA; San Francisco, CA
Departamento
Ingeniería
Job ID: 2980899
Ubicación
San Francisco, CA; Sunnyvale, CA
Departamento
Ingeniería
Ubicación
Pune, India
Departamento
Ingeniería
Ubicación
Pune, India
Departamento
Ingeniería