How To Debug A Memory Leak In An iOS App That Is Caused By A Third Party SDK (Part 1 of 3)
Yay, memory leaks!
Okay, you normally wouldn’t be excited about those. But today, my friends, is different. Today is a day of empowerment. We’re going to teach you a few tricks to help you debug memory leaks of your own.
This is part one of a three part series on debugging a memory leak in an iOS app that is caused by a third party SDK. While we are focused on debugging an SDK memory leak in this article, the basic ideas can also be applied to a memory leak stemming from your own app.
Note that in this series we’re using an iOS app and some features in Xcode. If you are an Android developer, the information here can still inform your approach to the debugging process. In fact, in Part 3, we will use a non-platform specific tool called Hopper. So there’s something for everyone!
Here’s our roadmap:
👉 Part 1 — We create a simple app called RecipeTime that loads recipes from a website into a WebView. We will then use some of Xcode’s Instruments features — Allocations and Generations — to profile its memory usage.
👉 Part 2 — We add an SDK to our app that introduces a memory leak. We investigate this using Xcode’s Allocations and Generations to learn how to spot a memory leak and what information can be gleaned from these tools.
👉 Part 3 — We load our SDK’s binary into Hopper, a disassembler and static analysis tool. We investigate what caused the memory leak and sum up what we’ve learned about debugging mobile applications.
By the end of this series, you should have a few more tools in your debugging toolkit and feel more confident in your ability to diagnose problems. No more dilly-dallying, let’s get to it!
Part 1 — A Simple App Called RecipeTime
Let’s start with our sample application. It’s called RecipeTime, and it’s very simple. All it does is load a random recipe from a website every two seconds.
Our app does a few things:
- It creates a WebView that opens a recipe page on a website
- It uses a NavigationDelegate to control which websites the WebView can access
- It deletes the WebView
- Two seconds later, it creates a new WebView and opens a different recipe page
- It repeats this process until we shut down the app
If you’d like to see our app’s code, here it is. (By no means do you have to read it. It’s just here if you’re curious.)
We realize this is a very boring app, but it’s the simplest example we can create for this lesson. It also somewhat resembles an app that allows users to click around and view different websites (e.g. Medium, other news apps, recipes, etc.).
For our purposes, the important part of the app is that it continually makes network requests to view webpages.
Our app uses a small amount of memory. By clearing the WebView after each download, we are only ever storing a single WebView in memory.
So let’s benchmark our app’s performance by going into the Instruments panel of Xcode. From here, we can use Allocations to see all the pieces of our running application.
What is the Allocations Instrument?
Allocations is an Instrument that captures information about memory allocations in an app. For a deep dive into it, you can check out this helpful technical note from Apple here, or learn all about Instruments here.
If we look at the highlighted line, we see our app only has one WebView. If we let the app run, we will see the WebView instance disappear and be replaced by a new instance every two seconds. This is illustrated in the image below.
If we would like to take a closer look at how the memory footprint changes over time, we can use the Generations tool.
What are Generations?
You can think of Generations as memory snapshots in your app’s timeline. You mark them at whatever time intervals you wish to examine. They work by showing you the difference in memory between snapshots.
In the above screenshot, let’s focus on the Growth column. This column shows us the additional memory added since the previous Generation. So Generation A’s Growth value would be all the memory created from the app’s launch through loading a few recipes. Generation B should contain only the unique memory created since the marking of Generation A. Generation C should contain the new memory created since Generation B, and so on.
If you’re a little confused about the difference between Allocations and Generations, don’t worry. They’re similar and they’re almost always used together. Allocations helps you understand the totality of what is in memory, whereas Generations lets you dig into exactly how memory changes as the user performs actions.
If we look closely, we see the Growth of Generation A is 5.37MB, whereas every subsequent Growth is under 5KB. That’s good. It means once our app is running, it isn’t creating increasing memory usage.
A great way to think of Generations is to think of an apartment listed for Airbnb. If we wanted to monitor the change in its state, we could take a picture of the apartment before a renter checked in. If we take another picture of the apartment after they check out and compare the two photos, we’ll be able to tell what wasn’t properly cleaned. Likewise, if we took photos every time a renter checked out, over time we would get a sense of whether the apartment is being properly maintained or is getting increasingly messy.
Generations is a great tool to help developers monitor their application’s memory usage over time. You can load specific views or go through common user actions to see how the app performs.
We realize our app isn’t perfectly optimized. Creating and destroying WebViews has worse performance in terms of CPU and battery, but it improves the memory usage of an application. Developers must make these optimization decisions themselves.
What Have We Learned So Far
Before adding any third party SDKs into your app, it’s a good idea to benchmark its performance. In this case, we’re focusing on memory, but we could also use Instruments to examine other areas like battery usage, CPU usage, networking, rendering performance (e.g. for a game), and so much more.
We’ve introduced the Allocations and Generations tools and showed they can be used for checking an app’s memory usage over time.
We’re feeling pretty good that we’ve written a memory-efficient app.
So let’s throw a wrench into it!
In Part 2, we’ll add an SDK that introduces a memory leak. We’ll dive back into Allocations and Generations to see the difference and gain insight in how to debug the problem.
Who We Are
We work at Embrace, a mobile monitoring and developer analytics platform. We take the guesswork out of debugging, shortening the time it takes to solve bugs from hours or days to just minutes. If you’d like to learn more, you can check out our website or visit our docs!