Core Data 5 Tips for High-Performance Apps

We have recently refactored the data layer of our app pairs to improve overall performance and response time. A couple of weeks after releasing the update we have received a very positive response from our users. (i.e. AppStore reviews)

As Japan’s most successful matching service, our app’s database needs to handle huge amounts of data without affecting UI performance. We use CoreStore to manage our Core Data layer, but today I would like to share some important things to know when developing high-performance apps with Core Data, even when not using wrapper libraries such as CoreStore or MagicalRecord.

1. Avoid global “super entities” when possible

When designing models, we usually use an Object-oriented approach when handling attributes shared by several models. For example, if we have a “User” model and a “Message” model both with an “identity” attribute, it’s easy to think to make them share a common super class:

common tendency to make super-entities

One thing to be careful of when using Core Data, however, is that entities with a common super entity will be stored in a single sqlite table. Which means if you have 1 base class with 10 subclasses each with 10 attributes each, Core Data will create 1 database table with at least 100 columns! Needless to say, this is bad for performance so try to minimize inheritance especially between unrelated objects.

separate attributes

The common attribute “identity” is added separately among entities

When to use super classes

Super entities themselves are not bad. This is usually true when all sub-entities are connected to a common relationship. In our previous example, imagine that “Message” have 3 separate types “TextMessage”, “PhotoMessage”, and “VideoMessage”.

relationship with super-entity

In the example above, a User has a one-to-many relationship with Message. Chat apps for example display texts, photos, and videos in a single list. Using the “User.messages” relationship with the super class “Message” makes this implementation simpler.

2. Avoid connecting the main context directly to the NSPersistentStoreCoordinator

Don’t do this:

let coordinator: NSPersistentStoreCoordinator = // ...

let mainContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
mainContext.persistentStoreCoordinator = coordinator // NOooooo....

Connecting the main NSManagedObjectContext directly to the persistent store means that all disk accesses during save will be done on the UI thread. This means our UI will lag every time we save data. Unfortunately, Xcode’s default Core Data project template does this 🙁

As app developers we are always responsible for keeping the UI as responsive as possible. With Core Data, you’ll want to keep an intermediate background context between the NSPersistentStoreCoordinator and the main context.

let coordinator: NSPersistentStoreCoordinator = // ...

// Bind our writer context to a background queue
let writeContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
writeContext.persistentStoreCoordinator = coordinator
// ...

// Set our main context as a child of the writer context
let mainContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType) // Main Queue
mainContext.parentContext = writeContext
// ...

Doing so will free the main queue from lags.

3. Optimize your algorithm when saving JSON arrays

If you have developed an app that needs to save unique server objects, you are probably familiar with the very common “insert-or-update” pattern: Fetch an object given an ID; If it exists, update the object, otherwise create it. Pseudo code:

for json in jsonArray { // for each JSON item in the array…

    let object: MyEntity? = MyEntity.find(json["id"]) // search object with given "id"

    if object != nil {
        object!.update(json) // if found, update
    }
    else {
        MyEntity.insert(json) // if not found, create
    }
}

We normally do this in a background thread to prevent blocking the UI, but this is still inefficient. For every JSON item, you are doing 1 fetch, which costs a disk access, which runs on the main thread. If you have 100 JSON items, you will be doing:

100 fetches + 100 updates or inserts = 200 database operations

(This is an oversimplification, but demonstrates the complexity well)

One way to improve this is to collect all IDs from all JSON entries and fetch all existing items at once. Pseudo code:

// ... prepare a dictionary mapping the JSON with their unique IDs
var jsonByID: Dictionary<String: JSON> = // ...

// Fetch all existing objects at once
let existingObjects = fetchIDs(jsonByID.keys)
for object in existingObjects { // for each existing object...

    let json = jsonByID[object.ID]
    object.update(json) // update the existing object

    jsonByID.removeValueForKey(object.ID) // remove the updated object from the list
}

// By now jsonByID only contains objects we need to insert
for (ID, json) in jsonByID {

    MyEntity.insert(json) // create object
}

The code is longer, but our background thread now hits the disk less. Again as an example, if you have 100 JSON items our operation count is now:

1 fetch + 100 updates or inserts = 101 database operations

Less disk hits means less bottlenecks and potentially fewer deadlocks.

4. Prevent redundant fetches

Imagine you have written a method that fetches an object from Core Data:

func fetchPerson(predicate: NSPredicate) -> Person? {
    let fetchRequest = NSFetchRequest(...)
    // Core Data access...
    return // context.executeFetch(...)
}

Now along the way, someone from your team (or maybe even you yourself) decided to write utilities which return attributes of your object:

class SomeUtility {
    var personFirstName: String? {
        return fetchPerson(/* ... */)?.firstName
    }
    var personLastName: String? {
        return fetchPerson(/* ... */)?.lastName
    }
}

Okay, looks harmless still.

Then someone (maybe you) wrote this:

let fullName = utility.personFirstName + " " + utility.personLastName
// ...

Do you see it? This innocent-looking code just did 2 Core Data fetches in one line. Here is another example:

if utility.personFirstName != nil && utility.personLastName != nil {
    let fullName = utility.personFirstName! + " " + utility.personLastName!
    // ...
}

In two lines we just did 4 invisible fetches. Needless to say, this is wasteful and might stack up to lag our UI.

Always design code that avoids this problem. For example, make it a rule to avoid oversimplifying fetches by using properties and just use functions instead:

func fetchPersonFirstName() -> String? {
    return fetchPerson(/* ... */)?.firstName
}
func fetchPersonLastName() -> String? {
    return fetchPerson(/* ... */)?.lastName
}

This makes the function look “slower” from the caller side and users of our API may be more wary of overusing it.
If you need the fetched value in multiple places, try to cache this value within the scope of usage:

func someMethod() {

    let firstName = fetchPersonFirstName() // keep the fetched value so we don't need to keep fetching

    titleLabel.text = firstName + "'s Profile"
    greetingLabel.text = "Hi, " + firstName + "!" 
    // ...
}

5. Share NSFetchedResultsControllers between multiple view controllers when possible

If you find your app using this pattern

redundant NSFetchedResultsController

where two NSFetchedResultsController share the same predicate and the same entity, it might be more efficient to share a single NSFetchedResultsController between view controllers. Here’s an example setup:

shared NSFetchedResultsController

We create an object that holds a reference to the NSFetchedResultsController and implements the NSFetchedResultsControllerDelegate. It then broadcasts the notification, typically through NSNotificationCenter, so that all observers can handle the update.

So how is this different with just passing around an array? With this method we save memory from redundant data and we gain performance by reducing redundant NSFetchedResultsController overhead. Plus, with NSFetchedResultsController‘s notification mechanism we are guaranteed that our View Controller can display the latest and most accurate info.

The End?

While some teams jump to using other ORMs such as Realm, I believe Core Data is still the most powerful and “complete” ORM for iOS. As developers we just need to tame that power to suit our needs. And with iOS 10 bringing more improvements to Core Data, it’s exciting to think how much more efficient our apps can grow.

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

開発速度がグッと上がる!チーム全員でサービスの仕様を決める方法

DIを使ってAndroidでイイ感じにテストを書く