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
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! 💪
[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.