A deep cut into RxSwift: Explore its mechanisms

7 min read

The purpose

The main purpose of this tutorial is to provide you with a deep understanding of the core components of RxSwift, focusing mainly on the implementation behind the scenes of RxSwift. 

After reading this post, it is hoped that you will have a clear understanding of what observables are, what subscriptions are, and how observables work. 

This tutorial assumes that you have some experience with RxSwift, which will make it easier for you to understand. At zen8labs, we often train our members in RxSwift, and we highly recommend that you carefully read this awesome tutorial: “Learn Rx by implementing Observable” by Serg Dort.

A big thanks to Serg Dort for writing this very helpful tutorial. This tutorial simplifies the implementation of RxSwift in a way that makes it easy for you to understand the main components of RxSwift and how they work together. 

Once again, please read the tutorial carefully and follow the code step by step. Believe me, it’s worth it. 

Honestly, in my opinion, you don’t need to know the inner workings of RxSwift. You just need to focus on when the observable emits elements, when it stops, when errors occur, how many elements the observable can emit, and when you need to dispose of your subscription. If you have a good understanding of these things, you can use RxSwift without any problems. 

But from my perspective, I always want to understand what happens behind the scenes in what I’m using. That is the best way to ensure that I am doing things correctly. 

The questions 

Let’s consider a simple example: 

This is a basic example, but it contains most of the important concepts of RxSwift.  

We create a result Observable<Int> by composing operators of, map, filter, and take. Then, we subscribe to the result Observable to print the value. The subscribe method returns a subscription, which is an instance of the Disposable type.  

That’s it. However, there are many hidden details in this example.  

There are some questions about this:  

  • Observable is an abstract class, and Disposable is a protocol. So, what are the actual types that are created here?  
  • How do these operators actually work?  
  • How is the data processed through these operators?  
  • How are the operators composed together?  
  • Why do we need to subscribe to execute our logic? 
  • When does the subscription end?  

To answer these questions, let’s take a look at the implementation of these operators. 

The implementation 

Of operator: 

Map operator: 

Filter operator: 

Take operator: 

The same point here is that these four operators return an instance of Producer. This is also true for the rest of the operators in RxSwift as well. To gain a clear understanding of RxSwift observables and operators, it is important to understand what a Producer is. Let’s examine the implementation of the Producer to delve deeper into this concept. 

(1) The Producer class is actually a subclass of the Observable class, so the RxSwift operators are basically methods that return different subtypes of the Producer

(2) Every time you subscribe to a Producer, it returns a SinkDisposer. In fact, the Observable class is an abstract type, meaning we never directly create an instance of it. Instead, we create observables using operators. Therefore, most of the observables we use in everyday programming are actually subclasses of the Producer. As a result, most of the subscriptions that we get when subscribing to an observable are SinkDisposers. The subscription in our example is also a SinkDisposer

(3) The Producer class has an internal abstract method called “run“. This is where any subclass of Producer needs to implement its business logic. The “run method always returns two things: a sink and a subscription.  

Now, let’s explore the implementation of the four observables in our example (ObservableSequence, Map, Filter, TakeCount) to answer the question of what a sink and subscription are. 

ObservableSequence 

Map 

Filter 

Take 

What are the similarities here?  

Let’s focus on the run method of these four observables. You can see that each type of observable has a respective Sink class: ObservableSequence has ObservableSequenceSink, Map has MapSink, Filter has FilterSink, TakeCount has TakeCountSink. And this is also true for any other observables in RxSwift

Additionally, you can also observe that Observables themselves do not execute any logic; they merely serve as designs or templates for observable logic. The guy that actually does everything is the Sink.  

To visualize this, let’s imagine that our Observable is a blueprint for a machine, while the Sink instance is the actual machine built based on that blueprint. In fact, the Sink is where all the magical occurrences in RxSwift take place. It retains a reference to our observer, executes the observable logic, and forwards the result events to the observer. If you want to truly understand how a specific kind of Observable in RxSwift works and how the data is processed by it, you just need to take a look at the corresponding Sink of that Observable.  

Another important point to note is that a Sink instance is only created when you subscribe to an Observable in the run method. This explains why the logic of an Observable only runs when subscribed to, and why the logic is re-executed each time you subscribe to the Observable. This occurs because a new Sink instance is created, that’s it.  

There is one more component that is always returned by the run method of a Producer – a subscription. So, what is the subscription?  

Before we answer that question, let’s keep in mind that RxSwift is composable, meaning you can combine different types of observables into a single one, similar to a train with multiple carriages. 

RxSwift Chain

There are two kinds of observables: Generation observables and Transformation observables

  1. Generation observables are observables that generate events themselves. For example, observables that are returned from create, of, and from operators. A Generation observable is like a train head that generates all the power, any events always start from the Generation observable. When looking at the ObservableSequence implementation above, the subscription is the result of running the sink instance. It is a type that implements the Disposable protocol to manage resources allocated for the Sink running. When the Sink completes generation, the subscription will be disposed to release all the allocated resources of the Sink.  
  1. Transformation observables are observables that transform the events they receive from their source into observable. For example, observables that are returned from map, filter, and take… operators. Let’s take a look at the Map, Filter, and TakeCount implementations. Each class always has a source observable, which is basically a reference to the previous observable in the chain. The subscription is the result of subscribing it’s sink to its source observable. And because it’s source observable is actually another Producer, so that the subscription is also a SinkDisposer. This creates a recursive call, which is how RxSwift chains multiple operators together.  

Connect everything together, we can visualize our example: 

Into this 

A final model of the RxSwift mechanism

This is what happened in our example: 

1. Creation phase:  

The result observable is actually TakeCount. The TakeCount has an observable source, which is Filter. The Filter has an observable source, which is Map. The Map has an observable source, which is ObservableSequence. The ObservableSequence is a generation observable that can emit events on it own.

2. Subscription phase:  

When you subscribe to the result observable, you’re actually subscribing to the TakeCount observable. It creates a TakeCountSink to execute the logic of TakeCount and creates a subscription by subscribing the Filter observable to that Sink. Both the sink and the subscription are managed by the SinkDisposer that was returned when you subscribed to the TakeCount. The subscription in our example is exactly this SinkDisposer.  

The same behavior happens for Filter, Map, and ObservableSequence because they’re also Producers. Each step will create its own Sink and SinkDisposer, and the subscription in each SinkDisposer is the SinkDisposer of the previous step. Except for the ObservableSequence, because it’s a generation observable, therefore the subscription only is an instance that manages allocated resources for running ObservableSequenceSink itself. 

3. Running phase:  

ObservableSequenceSink starts emitting events, and its observer is the MapSink. The MapSink takes events from ObservableSequenceSink, handles its business (multiplying by 2), and then emits its results to its observer – the FilterSink, and so on… The last sink is the TakeCountSink, which emits events to the closure that print the value in our example. 

4. Dispose phase:  

When we finish our business, we dispose of our subscription – it’s actually a SinkDisposer of the TakeCount. Then, this SinkDisposer will dispose TakeCountSink and its subscription – which is the SinkDisposer of the Filter. The disposing process continues like this until the last SinkDisposer of ObservableSequence

Conclusion

This is the complete picture of RxSwift. Hopefully, this post can help you to have a deeper understanding of what happened inside RxSwift. If you are interested in other topics, check out our useful Tech insights here!

Toan Nguyen, Senior Mobile Engineer

Related posts

Normally when programming a mobile application, we often encounter apps crashing, which is when the current application cannot operate (force close). But there is another status that is less serious: Application Not Responding (ANR). Why is it less serious because your application can continue to be used normally after waiting? The question is how to minimize ANR errors? Let's find out below.
3 min read
The need to make mock A.P.I responses has become increasingly crucial for efficient testing and development workflows. While various tools offer powerful capabilities for intercepting and modifying HTTP traffic, they often come with an expensive price tag. This is where mitmproxy steps in as a cost-effective and feature-rich alternative.
5 min read
Communication between different software systems and services is essential in today's digital world. Developers constantly seek efficient and reliable ways to make these connections, and one technology that has gained significant traction in recent years is gRPC. In this article, we will explore what gRPC is, how it works, and why it has become a popular choice for building modern applications.
3 min read