Rocket Data: Faster Model Management for iOS
July 21, 2016
In early 2015, we started rewriting the LinkedIn flagship mobile application. We wanted a caching system which would present content to the user, while data was loading from the network. Core Data is a commonly used technology for this problem, and we’ve used Core Data in a few applications at LinkedIn. Core Data is a modeling and database framework provided by Apple, which implements an abstraction over a SQLite database. However, we found that Core Data has many problems.
- Core Data is usually accessed on the main UI thread, which can block rendering and cause frames to be dropped.
- Core Data models are not thread-safe.
- Core Data does not provide an easy way to implement eviction.
- Core Data does not scale well in large applications.
- The database needs to be migrated whenever the schema changes.
Core Data is a powerful framework, but it pays for this power with complexity. The framework is notorious for crashing the application when something goes wrong.
Finally, we wanted to use immutable models. Core Data’s programming model relies on mutability, but we wanted to embrace immutability. For a detailed explanation of the advantages provided by immutable models (performance, thread safety, easier debugging, etc.), see our previous blog post about the open source release of iOS Consistency Manager.
For our caching system, we decided on these requirements:
- Immutable, thread-safe models;
- Consistency among models in both memory and in the cache, so that when a model is updated, all other instances of this model should also update;
- Non-blocking access on all reads and writes;
- A simple eviction strategy;
- Ability to scale well with a large number of model types, schema changes and listeners.
- Automatic migrations.
We looked at some other options including a simple URL cache, Realm, or just serializing the models to disk. However, none of these solutions addressed the requirements above or provided a solution for keeping immutable models consistent. So we decided to write Rocket Data.
Rocket Data is a non-blocking, immutable model management system with a persistent synchronization layer. It can use any cache and has a simple API which easily hooks up to key-value stores.
First, let’s take a look at the architecture of Rocket Data:
Each view controller has a reference to one or more data providers. There are two types of data providers: regular data providers, which hold a reference to a single model, and collection data providers, which hold a reference to an ordered array of models. The data is easy to consume with simple accessors in both providers. This data access is always instant since the models are always kept in memory.
Here’s an example application run to show how data flows through Rocket Data.
- The view controller calls loadFromCache on the data provider. This call is asynchronous so it will not block the main UI thread. When the data returns, the view controller is notified and it will rerender.
- The view controller receives fresh data from the server. The view controller sets this data on the data provider. The data provider then lazily propagates this to the cache and Consistency Manager, again on a background thread.
- Later in the application execution, a model is changed which affects our view controller. The Consistency Manager will notify the data provider which will update its data and notify the view controller so it can rerender.
Any models that are added to a data provider become “managed” models. Rocket Data will ensure that these models are kept in sync with the cache and any other data providers. Any changes to one data provider will automatically propagate to all the other data providers as well as update the cache. All these updates happen asynchronously and will never block the UI thread. However, all this complexity is hidden from the view controller, which has a simple API for accessing and updating its data.
Since the models are immutable, the data in the models can’t change. Instead, the Consistency Manager generates a new model and sets it on the data provider. This provides a single point of change for the view controller, making it easier to reason about the code. The Consistency Manager handles keeping both the top level models and all child models consistent.
The collection data provider provides a way to store an ordered array of models. The data provider provides useful methods for adding, removing and replacing models in the collection. It also can take an identifier for the collection. If another collection data provider has the same identifier, it will automatically be kept in sync with this data provider. Removing an element from one collection will instantly remove it from all other collections with the same identifier.
Even if Rocket Data does not block the UI thread, rendering a view could slow down the application. For instance, if an off-screen view gets updated data from the Consistency Manager, it could rerender and slow down the current view. Rocket Data offers the ability to pause a data provider for updates. It will keep providing the same data until it’s unpaused. Then, it will update with all the changes that have been missed in one callback so the view can rerender.
Rocket Data works with any immutable models that implement a certain protocol defined by Rocket Data. This protocol allows Rocket Data to access the model identifier, compare models and traverse a model’s child models. This makes it flexible to many different modeling solutions. It works with Swift classes, Swift value types and Objective-C classes.
Rocket Data doesn’t implement a specific cache and instead offers an interface to hook up any cache implementation. The API is simple and works well with any key-value store. We chose LMDB because of its fast read access. Since any cache can be used, it’s easy to add LRU eviction support.
Since Rocket Data uses a key-value store, data does not need to be migrated when the schema changes. If the current data cannot be parsed into a model, we simply treat it as a cache miss and reload from the network.
Using this caching system, we’ve been able to easily add caching to all features with very little additional work from developers. The cache and data providers are automatically kept consistent across screens. Despite updating the schemas of multiple models every week, we have never needed to add code for any migrations. And best of all, our application has never crashed due to a Core Data exception!
Since we thought other projects could benefit from Rocket Data’s flexibility to work with multiple modeling and caching solutions, we’ve open sourced Rocket Data. You can view the project on GitHub and read through the documentation here. We’re looking forward to hearing what you think.