Memory management in RxSwift - DisposeBag

Memory management in RxSwift - DisposeBag

I’ve noticed a lot of beginners in RxSwift ask about DisposeBag. DisposeBag isn’t a standard thing in iOS development neither in other Rx’s implementations. Basically, it is how RxSwift handles memory management on iOS platform.

In this article, I want to answer for few question like what is the DisposeBag, Disposable and to talk generally about ARC memory management with RxSwift and how to protect yourself from memory leaks while using RxSwift. I hope you will enjoy it 📚💪


Observable && memory management

When it comes to implementing a library which serves for handling asynchronous events there are few … things you need to be aware of because of iOS reference counting.

The easiest way to describe the problem is to describe it by an example:

To cancel or not to cancel? 🤔

Imagine we have an Observable which represent a REST API call. When you call subscribe it sends a request to a server and waits for the response. Let’s say you subscribe for it in viewDidLoad in UIViewController.

It’s an easy example, but you need to have that in mind User can come back in navigation stack in any moment. With "normal" memory management coming back to the previous screen would deallocate the UIViewController and … would also cancel the Observable because it would lose the reference from the UIViewController.

As a result, our request wouldn’t have a chance to finish.

Sometimes it’s an expected behavior, however, sometimes you would like to wait until you receive the response, despite the fact user’s gone back in the navigation stack. The developer should be able to decide when the Observable is terminated.

Memory is a limited resource

Another thing about memory management is that memory is a limited resource. Observables can store some variables inside theirs implementations or they can also store what you have passed to them.

It means it is possible the Observable allocates some part of the memory for its internal needs.

On the other hand, as you probably know, one of the Observable characteristics is after it receives completed or error event it stops sending events.

If it’s not going to send new events anymore what’s the point of keeping its internal resources? A good idea would be to clean and free the memory the Observable keeps.

To be able to clean the Observable we need to have a possibility to clean the Observable on demand. This is why the subscribe method returns the Disposable

Just a quick reminder about reference counting. Reference counting is the type of memory management. On iOS platform, every object has additional numeric property retainCount. Every strong reference to the object increases its retainCount by one. When a reference is deleted the retainCount is decreased by one. When retainCount of an object reaches 0 then the object is deallocated.

Disposable – the type the story begins from

Disposable is just a protocol with one method dispose:

When you subscribe for an Observable the Disposable keeps a reference to the Observable and the Observable keeps a strong reference to the Disposable (Rx creates some kind of a retain cycle here). Thanks to that if user navigates back in navigation stack the Observable won’t be deallocated unless you want it to be deallocated.

To break the retain cycle somebody needs to call dispose on the Observable. If Observable ends by itself (by sending completed or error) it will automatically break the retain cycle. In any other scenario, the responsibility to call the dispose function is in our hands.

The easiest way would be to call dispose inside deinit function:

This solution is simple however, it isn’t scalable. Imagine how many additional fields you would need to have in your class.

To improve it, you can have an array of disposables [Disposable] and goes through all the array and calling dispose on every Disposable inside it:

It looks much better now and it’s scalable. No matter how many subscriptions you have deinit looks the same.

However, this is not the end of improvements. Current solution forces you to remember about manually disposing in deinit.

Yes, you probably know where it is going. We can use DisposeBag instead of [Disposable]:


Wait a minute! Where did the deinit go?

The cool thing about DisposeBag is it takes care of calling the dispose on every disposable inside it.

When it calls dispose you ask? DisposeBag calls dispose when it’s own deinit is called. It means when DisposeBag loses a reference from UIViewController its retainCount goes to 0 so it will be deallocated and it will call dispose on all the disposables.


DisposeBag && retain cycle 😱

Calling dispose on deinit is the simplest way to clean the memory, however … it will only work if the deinit will be called.

With DiposeBag it is easy to bring about a retain cycles between Observables and UIViewController. DisposeBag will wait for dealloc forever and will never dispose its disposables.

What you need to remember is every operator by default keeps a strong reference to every variable used in its closure:

In above example, transformedObservable keeps a strong reference to self, because self was used in map operator. Such a behavior is a natural way how Swift uses reference counting to ensure everything will be allocated when it’s needed.

The code above doesn’t create a retain cycle. Unfortunately, with few changes retain cycle becomes a real problem:

Lines which cause the retain cycle are .disposed(by: disposeBag) & the map operator.

Because of adding the Disposable into the DisposeBag it means the DisposeBag have a strong reference to Disposable.

The Disposable keeps Observable alive which hold a strong reference to MyViewController because self was used inside map closure.

At the end the ViewController holds a reference to the DisposeBag and … 💥💥BOOM💥💥 … You have a retain cycle! 😱

I recommend you to draw a diagram if you can’t see the dependencies:


How to avoid a retain cycle?

I need to say with a good design you going to have less and less possible areas to have a retain cycle there. Good separation of concerns is a key here.

The code above is too small to talk about architecture patterns. How to get rid of the retain cycle in this case?

Just use a capture list!

With capture list, you can pass a variable and tell the compiler how a closure should treat the variable in case of memory. Usually, the first idea is to pass [weak self]:

However, if we use [weak] then we need to tell the compiler what should be returned if self is nil. In this case is better to pass the parser in capture list instead of self:

Swift allows us to pass a variable into the capture list without any attribute like weak or unowned. If we do so, the compiler knows to keep a reference only to the parser (with strong reference), not to self.

This is it. Only one small change solves the entire problem! 💪

The 3rd option would be to use [unowned self] instead of [weak self]. However, I’m not a fan of using an unowned attribute. If something goes wrong you will have a crash 😟.

Using self != retain cycle

Now as we all know that every operator keeps strong reference to every variable in its closure including self, I want to emphasize that we don’t need to avoid using self everywhere.

If you have a class which just returns an Observable, it’s fine to use self in operators which created the Observable.

For example if you have a UIViewController which has APIClient as a dependency it’s ok to use self in APIClient implementation.

The rule of thumb is retain cycle usually occurs when self is also the owner of the DisposeBag. In any other case it’s rather safe to use self in operators closures.

Summary

I hope the DisposeBag isn’t anything magical now for you. It’s just an array with multiple Disposable inside. It’s a nice helper to dispose all the disposables in deinit. Otherwise, our life would be much harder.

Unfortunately, using a DisposeBag sometimes leads to memory leaks. Remember that every operator keeps a strong reference to dependencies used in its closure. If it is self it is also kept by the Observable. As a result, you have a retain cycle.

If you add a Disposable into the DisposeBag just use capture list and pass proper variables and attributes to closures. This is the way how you can avoid retain cycles.