Disposing RxSwift’s Memory Leaks
Tracking down memory leaks in your RxSwift-backed app
As a new developer in your company, there are a lot of challenges. One of these challenges is how you approach an existing massive project: You don’t know the code, you’re not familiar with naming conventions or code style guides, and you also don’t know the architecture. Some of these challenges can be solved quickly, while some of them can take some time. One of the good and bad things in being a newbie is that you might naively accept hard challenges without an understanding of what you’re getting into.
One of the issues we, as a team, had with the app, is that we’ve noticed there are some unexplained crashes. Sounds like the right time for a good cleaning! I decided to take on this challenge.
This blog post outlines the bug squashing journey I’ve had, some of the techniques I’ve tried, and some pitfalls to look out for.
Investigating unrelated multiple crashes
I started with checking the stack trace of these crashes, but unfortunately, these didn’t provide any clarity into why these crashes are happening.
In most of these crashes, the application crashed while trying to pull an object from an array or dictionary. There was no seemingly good reason to believe these objects were
nil, or that we’ve looked for any non-existing keys or out-of-bound indexes. I knew that like most iOS applications, our app is multi-threaded, but our scenario didn’t have multiple threads modifying the same objects.
The second thing I did, is to look at the device’s hardware details where these crashes happened (Luckily, our crash reporting and analytics are highly detailed). There I found the culprit: many of our crashes were related to low memory (RAM) on the device.
Just to make sure, I made sure the app leaked by checking Xcode’s memory usage graph.
Starting with the obvious
So, how do you even know that your app has a memory leak?
Well, it’s quite simple:
- Launch the application.
- Navigate through your app, invoking all classes, network requests & computations, database transactions, etc, more than once.
- Open Debug Navigator (Command-7) In Xcode, tap on Memory and look at the Memory Use graph. If you see your memory bar quickly escalating, much like the image below, you can be quite positive that your app is retaining memory space without releasing it.
Focusing on Rx
As the title of this blog might’ve hinted, tracking RxSwift-specific memory leaks is a very interesting topic with its own strategies.
With that, how can we know that the RxSwift code leaks?
Well, it’s simpler than you would think. RxSwift provides its own internal mechanism that counts the current resource count for all subscriptions across your app. You have to compile RxSwift with the
TRACE_RESOURCES compiler flag to get this ability.
This information is exposed via the
You could create an interval timer and constantly print out the current resource count to see what’s going on:
This code creates an observable which run on the main thread. Every second, it prints how many resources are allocated by your subscriptions.
If your console prints something similar to this:
Rx Resource Count: 5434
Rx Resource Count: 5461
Rx Resource Count: 6179
Rx Resource Count: 6446
Rx Resource Count: 6450
Rx Resource Count: 6739
Rx Resource Count: 6771
Rx Resource Count: 7273
Rx Resource Count: 7807
Rx Resource Count: 8349
Rx Resource Count: 9184
Rx Resource Count: 10000
Rx Resource Count: 10510
Then congratulations! Your Rx code leaks.
Main reasons for Rx-related Memory Leaks
If you’ve ever developed with Rx, you’ll know that reactive architectures comprise streams and subscribers. The stream will only start producing elements once subscribed to.
In general, there are two common pitfalls causing RxSwift memory leaks:
- You forget to properly handle your subscription, by either adding it to a Dispose Bag (
disposed(by:)), or by storing the
Disposablefor manual disposal. This is a relatively rare scenario, as the compiler will warn you.
- The owner of the subscription or it’s
DisposeBagis still retained by another object. This prevents the subscription or the bag from properly deinitializing and freeing the memory.
Disposing your Memory Leaks
Your basic steps will be:
- The first rule of subscriptions is …
No, but seriously. The first rule is that you should always add your subscriptions to a
DisposeBag, even if you know your sequences will surely terminate. Actually, as mentioned earlier, you can’t disobey this step without getting a compiler warning.
To be on the safe side, whenever you see operators such as
emit, or any other operator which returns a
Disposable — you should take care of its disposal. Using
disposed(by:) is the easiest and safest way to do this. You can also “manually” control the lifecycle of your stream using operators such as
2. Your Dispose Bag will clear its subscriptions only when the Dispose Bag’s owner is released. Therefore, you should be sure your Dispose Bag is tied to the life cycle of your subscriptions.
As a thumb rule — Don’t create a dispose bag in one class, and share it with another class. Nothing good will come from that.
A very common scenario where developers accidentally do this is when using
UITableViewCells. As opposed to
UIViewControllers, which get released and empty their Dispose Bag, a cell doesn’t really get deallocated, but gets reused. For that reason, you must be sure to clear your Dispose Bag (or simply create a new one) whenever your cell is about to be reused.
Instead, your cells should have their own Dispose Bags, and destroy them upon reuse.
3. Finally, we get to the most common leak:
While this might seem OK at first, this is a classic retain cycle, since the closure holds
self holds the closure, letting none of them ever get released.
This isn’t specific to working with RxSwift, but it is still a very common pitfall when working with it. Using a
[weak self] (or
unowned) capture group is the quickest way to deal with this:
Now, consider the following code:
Do you really need the
[weak self] in that
UIView.animate closure there? Well, in this specific case there is no need to weakly capture
self, since even though the animation block retains
self doesn’t actually retain the animation block — so there’s no cycle.
The last example is a special case since in pure MVVM it shouldn’t really happen. Unfortunately, I worked with an SDK that required me to provide an entire prepared view.
In my view, I had code similar to this one:
Take a guess, what is the problem here?
When binding the stream of
viewModel.viewDidSwiped, the View Model now retains the Gesture Recognizer. What you might miss, is that the gesture recognizer also holds the view itself, which means it would be the same as the View Model holding
viewModel also holds
MyView as well, since it’s held by the gesture recognizer.
One solution to this is just to get a weak reference to the gesture:
An even better solution is to get only a portion of the gesture that doesn’t retain self:
If you suspect that a piece of code is leaking, simply use the handy resource count tracking method you learn about earlier:
This will easily save you tons of hours trying to figure out if a memory leak actually exists, as well as confirming that it’s fixed.
Hope you’ve enjoyed this article, and may your apps be leak-free. What’s the next leak you plan to fix with this?
If you have any comments or notes, feel free to leave a comment below.