How To Debug A Memory Leak In An iOS App That Is Caused By A Third Party SDK (Part 3 of 3)
Hopper recreates your app’s source code from a compiled binary
Part 3 — Hopper To The Rescue
This is part three of a three part series on debugging a memory leak in an iOS app that is caused by a third party SDK.
👉 Part 1
👉 Part 2
In Part 1, we created an app called RecipeTime that loads recipes from a website into a WebView. We tested its memory footprint using Xcode’s Allocations and Generations tools.
In Part 2, we added an SDK to our app that introduced a memory leak. We investigated this using Xcode’s Allocations and Generations to learn how to spot a memory leak and what information can be gleaned from these tools.
In Part 3, we will use Hopper to recreate the SDK’s source code (as best we can) and try to track down the cause of the memory leak.
Let’s dive in!
What Is Hopper?
Hopper is a tool that lets you disassemble, decompile, and debug your applications. You can give it a compiled binary executable, and it will recreate the source code (as best it can).
Let’s see what happens when we upload our SDK’s binary into Hopper.
We know we’re looking for code that deals with WebViews, so why don’t we search in Hopper for that term and see what pops up?
We see a function called
decidePolicyForNavigationAction. From the function signature, we can see that a WebView is the first argument (labeled
arg2). That argument gets stored in
var28. We can see that
var28 is strongly retained, which means it is being permanently stored in memory.
Where is it being stored? It’s being added to an array called
webviewMemoryLeaker. I know the name seems pretty suspicious, but this is for learning purposes, people. Real bugs won’t always be that obvious.
How do we know
webviewMemoryLeaker is an array? It has an
It looks like the problem has something to do with how the SDK is connecting to our NavigationDelegate. The NavigationDelegate is responsible for approving or preventing network requests. You can think of it as a way to whitelist certain calls. For example, in our recipe app, our NavigationDelegate ensures that only the approved recipe websites can be loaded into the WebView.
So what’s the SDK doing that’s causing so much damage?
Well, analytics SDKs frequently collect data about network calls. To do this, they swizzle network requests. Swizzling is a form of proxying. It is commonly used to perform some actions after a network request has been sent, but before it gets to its original destination.
Swizzling is what happens when you jump to a function’s stub and intercept where the function was going to be called. At this point, you can add your own functionality to the function (e.g. call other functions and feed it the results as arguments) and then send the function call to its original memory address.
For example, an SDK might swizzle every network call to log details about the call. After it has done that, it sends the call along to its original path.
It looks like the problem is occurring at this intersection. The SDK is swizzling a network call. The bug appears to be that it is adding the WebView object to an array and storing it permanently. That means every single network call is building the app’s memory footprint.
Bada bing, bada boom.
(If you’d like to see the code for our evil SDK, it’s at the end of this post.)
What Have We Learned
Well, we can safely contact our SDK vendor and tell them they done messed up.
We realize this example may seem simple, but these kinds of bugs happen in production all the time. We didn’t want an overly complicated scenario to overshadow our goal, which was to highlight the amazing power of Instruments in Xcode (Allocations, Generations) and Hopper.
These tools will help you in your debugging. So next time you face a performance problem, consider using these tools to shed some light on what might be going wrong.
We’d love to hear feedback in the comments so we can improve our posts in the future!
Who We Are And Why We Care
We work at Embrace, a mobile monitoring and developer analytics platform. We care because we know how you feel. We’ve been there. You have an app in production, and you see the following:
- Users complaining that the app isn’t working
- Users giving you one-star reviews on the App Store
You want to know what the problem is so you can fix it. But the only information your users give you is, “The app is slow.” Or “The app froze.”
Where do you even start? Tools like Xcode’s Instruments and Hopper can be used when developing locally, but what are you supposed to do when an app is in production?
If you wanted to try recreating an individual user’s session, you would need a ton of information, such as:
- The device model
- The OS version
- The app version
- The CPU level
- The battery level
- The storage level
- The network connection
- The views they had visited before the one where the error occurred
- The network calls the app made
- The way the app was launched (cold start, push notification, from background)
The list goes on and on. Developers sink tons of time into recreating user sessions for debugging.
Imagine the time saved if you had access to every user session at the tip of your finger. If a user told you about a bug, you could look up the exact session and the exact view that it occurred on.
You could follow the exact footsteps your user took.
You would take the guesswork out of debugging.
That’s what you get with Embrace. Some of the information we provide for every user session is the following:
- Session Timeline — view all the user actions, the flow of the app, every single network call, and every log in one place.
- No Sampling — full access to every user session.
- User lookup — look up individual users and examine their sessions to easily track down bugs that users experience.
- Stitched Sessions — users switch apps all the time. Embrace stitches the sessions together to give you a view of the entire user journey.
- Device model
- App version
- OS version
- CPU level
- Battery level
- Storage level
- Did the app crash?
- Did the user terminate the session?
- Did the app run out of memory (OOM)?
- Was the app non-responsive (an ANR on Android)?
- Which network calls were made?
- Were they successful?
- Were they slow?
- Which views did they occur on?
- All error, warning, and info logs you specify
- Create specific app moments you want to monitor (e.g. app startup time, successful purchases from checkout view, etc.)
Embrace is your one-stop shop for application performance monitoring and error debugging. It was created to help mobile developers find and fix bugs quickly, so they could get back to the fun part — building features.
Also, let us know in the comments if you enjoyed this series and if there are other topics you’d like to see us cover!
App and SDK Code
If you’d like to explore the code we used for this series, it can be found on the Embrace GitHub here.
A few snippets are presented below. The first is our proxy setup. When you see EMBProxy, that is Embrace’s wrapper around iOS’s NSProxy.
Here is the code for
decidePolicyForNavigationAction, the cause of the memory leak.
Thanks for reading!