iOS-RxSwift
Guarantee Rx memory leaks absence
Should I add [weak self] here?
Does this subscription leak out? Is it safe to capture this object?
Does this subscription leak out? Is it safe to capture this object?
Developers with RxSwift stack should not think about these questions. They also should not waste code review time on such thoughts. It must be automated and covered by tests. This article is a quick guide, how to implement it.
Retain cycle
During my latest tech talk, I accidentally told: “…unit tests on Rx leaks”. It was absolutely surprising for me to realize that almost nobody even knows it’s possible to have such tests. I can’t imagine pushing my reactive code to production without them.
Just a quick reminder about the problem we are trying to solve. RxSwift based on a reference cycle, which is resolved only after disposing of subscription. It’s not a downside or overhead, it’s just how it works.
Bad: leaks & imperative
You should carefully avoid such code because it leaks. disposeBag holds subscription and subscription holds self. So, an object of this class will never be deallocated. You can read more about how it works here.
Preventing leaks
There are two popular ways to prevent this problem: capture list and unowned/weak specifier. I would like to add two advanced options.
Try to not be imperative at all
I know, it’s kind of hard in complicated production code, but just try. Other developers on your project will say thank you, cause declarative style is always more readable and safe. For instance, we can refactor the previous code sample in the following way:
Good: doesn’t leak, declarative
As you can see, we remove self usage with the simple binding of one events stream to another. There is no need to think about the reference cycle anymore, the problem is eliminated.
Use Binder
It’s very hard to use a declarative style with UIKit. That’s why we love RxCocoa with its extensions so much. But what if there is no suitable extension for you? For instance, you want to change many different properties. You will probably use an imperative style:
Bad: leaks, imperative & declarative code is mixed
Obviously, this is not good as you need to think about the reference cycle by yourself again. Moreover, it mixes a clean declarative code with imperative assigning, which is bad from the code style point of view.
At this moment it’s time to remember that RxCocoa already solved that problem for us. There is a convenient struct Binder which is used all around UIKit extensions implementation. Here is its init method, which tells you everything about this type’s purpose. It handles the retain cycle for you, holding a weak reference on the target object.
Good: doesn’t leak, imperative & declarative code is clearly separated
So, you can create a bindable property passing the target object and binding callback, which will be triggered with each event in sequence. Inside it, you can be as much imperative as you want.
We don’t use the second parameter here, because rx.tap type is Void. Of course, you can change it to needed type and perform more complicated logic inside the callback.
Just look at the last line! It became as clear as possible, every developer can easily understand what’s going on here. If a fellow developer is interested in how exactly you created shadow — he or she will check the enableShadowproperty. Until that time, it is ok seeing just the line of subscription.
Automate this!
Well, actually… It’s still bad. Yes, there are many handy tools to avoid leaks, but how can it be guaranteed that they are used correctly? Manual code review is not good enough, because developers are not robots. We can easily miss a critical bug, which will cause out-of-memory crashes.
There is a way to automate it! Let’s write the test on Rx memory leaks.
Enable debug mode
You can easily enable debug mode following this instruction. For CocoaPods users, just add the following script to the bottom of your Podfile:
post_install do |installer| installer.pods_project.targets.each do |target| if target.name == 'RxSwift' target.build_configurations.each do |config| if config.name == 'Debug' config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['-D', 'TRACE_RESOURCES'] end end end end end
TRACE_RESOURCES compilation flag is needed for access to the Resourcesstruct. It’s a simple namespace for the counter of all allocated RxSwift objects. Observable, Observer and Disposable objects are tracked by this counter.
Unit test
With this tool, it’s quite easy to write a unit test on leaks. We need:
- Make a snapshot A of currently allocated resources
- Initialize subject under test, trigger subscriptions, wait for deinit of sut
- Make a snapshot B of currently allocated resources
- Check that A == B. That means all subscription were disposed and resources freed
`autoreleasepool` need just to create internal scope
Only 6 lines of code to guarantee that your view controller creates subscriptions correctly. That’s it, no more worries about [weak self], just run tests and you are done with checking.
Thinking about this a bit more, my team adopt this solution. We inherit all our tests classes from the custom XCTestCase subclass. This guarantee that there are no memory leaks in all test cases. RxSwift repo uses this pattern for all its tests.
Custom test parent class
Notes
Be careful with singletons, which has reactive properties (like RxKeyboard). It initializes after first access and lives till application completion (resources won’t be released). Tests order is random, so you don’t know when singleton object will be created. That can be a reason of fluky tests. My advice — use fake AppDelegate and init all your Rx singletons there.
Wait a bit. Usage of observeOn(MainScheduler.asyncInstance) or time-shifting operators (e.g. debounce, delay) will likely fail synchronous check of resources releasing. I suggest you use toEventually Nimble matcher, which polls test condition with provided interval and fails if it’s not fulfilled after a timeout. Or just implement your own “waiting” as main repo did.
This is actually it.
- Understand why there is a reference cycle in RxSwift design
- Remember that [weak self] is not the only tool you can use to avoid memory leaks
- Reduce manual code review time with automatic checks
- Let Rx go in your heart
I am going to share more RxSwift experience on Saint AppsСonf. Meet me there!
The original article was posted here on Medium.
The original article was posted here on Medium.