Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Saving NSManagedObjectContext without hitting the main thread

What I'm trying to do:

  • perform background sync with a web API without freezing the UI. I'm using MagicalRecord but it's not really specific to it.
  • make sure I'm using contexts & such correctly

What my question really is: is my understanding correct? Plus a couple questions at the end.

So, the contexts made available by MagicalRecord are:

  • MR_rootSavingContext of PrivateQueueConcurrencyType which is used to persist data to the store, which is a slow process
  • MR_defaultContext of MainQueueConcurrencyType
  • and for background you would want to work with a context generated by MR_context(), which is a child of MR_defaultContext and is of PrivateQueueConcurrencyType

Now, for saving in an asynchronous way, we have two options:

  • MR_saveToPersistentStoreWithCompletion() which will save all the way up to MR_rootSavingContext and write to disk
  • MR_saveOnlySelfWithCompletion() which will save only up to the parent context (i?e. MR_defaultContext for a context created with MR_context)

From there, I thought that I could do the following (let's call it Attempt#1) without freezing the UI:

let context = NSManagedObjectContext.MR_context()
for i in 1...1_000 {
    let user = User.MR_createInContext(context) as User
    context.MR_saveOnlySelfWithCompletion(nil)
}
// I would normally call MR_saveOnlySelfWithCompletion here, but calling it inside the loop makes any UI block easier to spot

But, my assumption was wrong. I looked into MR_saveOnlySelfWithCompletion and saw that it relies on

[self performBlock:saveBlock];

which according to Apple Docs

Asynchronously performs a given block on the receiver’s queue.

So I was a bit puzzled, since I would expect it not to block the UI because of that.

Then I tried (let's call it Attempt#2)

let context = NSManagedObjectContext.MR_context()
for i in 1...1_000 {
    let user = User.MR_createInContext(context) as User
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
        context.MR_saveOnlySelfWithCompletion(nil)
    }
}

And this does the job, but it doesn't feel right.

Then I found something in the release notes of iOS 5.0

When sending messages to a context created with a queue association, you must use the performBlock: or performBlockAndWait: method if your code is not already executing on that queue (for the main queue type) or within the scope of a performBlock... invocation (for the private queue type). Within the blocks passed to those methods, you can use the methods of NSManagedObjectContext freely.

So, I'm assuming that:

  • Attempt#1 freezes the UI because I'm actually calling it from the main queue and not within the scope of a performBlock
  • Attempt#2 works, but I'm creating yet another thread while the context already has its own background thread

So of course what I should do is use saveWithBlock:

MagicalRecord.saveWithBlock { (localContext) -> Void in
    for i in 1...1_000 {
        User.MR_createInContext(context)
    }
}

This performs the operation on a direct child of MR_rootSavingContext which is of PrivateQueueConcurrencyType. Thanks to rootContextChanged, any change that goes up to MR_rootSavingContext will be available to MR_defaultContext.

So it seems that:

  • MR_defaultContext is the perfect context when it comes to displaying data
  • edits are preferably done in an MR_context (child of MR_defaultContext)
  • long running tasks such as a server sync are preferably done using saveWithBlock

What it still don't get is how to work with MR_save[…]WithCompletion(). I would use it on MR_context but since it blocked the main thread in my test cases I don't see when it becomes relevant (or what I missed…).

Thanks for your time :)

like image 342
Arnaud Avatar asked Mar 27 '15 19:03

Arnaud


Video Answer


1 Answers

Ok, I am rarely using magical records but since you said you question is more general I will attempt an answer.

Some theory: When creating a context you pass an indicator as to whether you want it to be bound on the main or a background thread

let context = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType)

By "bound" we mean that a thread is referenced by the context internally. In the example above a new thread is created and owned by the context. This thread is not used automatically but must be called explicitly as:

context.performBlock({ () -> Void in
   context.save(nil)
   return
});

So your code with 'dispatch_async' is wrong because the thread the context is bound to can only be referenced from the context itself (it is a private thread).

What you have to infer from the above is that if the context is bound to the main thread, calling performBlock from the main thread will not do anything different that calling context methods straight.

To comment on your bullet points at the end:

  • MR_defaultContext is the perfect context when it comes to displaying data: An NSManagedObject must be accessed from the context it is created so it is actually the only context that you can feed the UI from.

  • edits are preferably done in an MR_context (child of MR_defaultContext): Edits are not expensive and you should follow the rule above. If you are calling a function that edits an NSManagedObject's properties from the main thread (like at the tap of a button) you should update the main context. Saves on the other hand are expensive and this is why your main context should not be linked to a persistent store directly but just push its edits down to a root context with background concurrency owning a persistent store.

  • long running tasks such as a server sync are preferably done using saveWithBlock Yes.

Now, In attempt 1

for i in 1...1_000 {
    let user = User.MR_createInContext(context) as User
}
context.MR_saveOnlySelfWithCompletion(nil)

There is no need to save for every object creation. Even if the UI was not blocked it is wasteful.

About MR_context. In the documentation for magical records I cannot see a 'MR_context' so I am wondering if it is a quick method to access the main context. If it is so, it will block.

like image 95
Mike M Avatar answered Nov 14 '22 23:11

Mike M