Posted by Michael Dales on 2010-06-11 10:18:35
Recently I've been working on an iPhone app that does a reasonable amount of CoreData munging, which was causing my UI to lock up. After trying to optimise my use of CoreData, I managed to improve things, but still was having UI slowdown on things like the first every sync, where lots of data comes in, so I moved that processing to another thread. This is quite easy to do in Cocoa, but threading is something that tends to fill people with fear, so I thought I'd write up just how easy it is to do both the threading and make CoreData work well with multiple threads.
Let's start with some example code to set the scene:
- (void)processIncomingData: (ASIHTTPRequest*)request
{
NSString *rawdata = [request responseString];
NSArray* lotsOfData = [rawdata JSONValue];
[request release];
MyApplicationDelegate *delegate = [UIApplication sharedApplication];
NSManagedObjectContext *context = [delegate managedObjectContext];
for (NSDictionary* info in lotsOfData)
{
// create or update managed object here
// probably some searching too
}
NSError *error = nil;
[context save: *error];
if (error)
NSLog(@"error %@", [error localizedDescription]);
// let the app know we're done
[self reloadInfoFromCoreData];
}
So here we're using ASIHTTPRequet to get some JSON data, we then have an array of dictionaries that we're going to store in the CoreData store. As part of that we're probably going to check whether the objects already exist and update them if necessary, otherwise make new objects. Finally we want to update any UI representation of the data once we've loaded all the data. It's this loop and then the save that's potentially slow, and thus something we want to move to another thread, so as not to block our main thread of execution, which will lock up the UI.
Threading is typically a messy business. When I was a researcher at Intel many years ago, one of the things that we worried about was that multicore was on the horizon, and raw thread programming is difficult and error prone, and most programmers can't cope with it. When I saw NSOperationQueue appear in Cocoa I was hugely relieved - whilst you still have issues with shared data, here was at least a good model to deal with the messy world of creating. managing, and destroying threads. In fact, it's so easy, we can take our code above and parallelise it really easily.
NSOperationQueue is an object that managed a set of worker threads, and you simply give it jobs to do, and it'll queue them up until a worker thread can do the task. You don't need to worry about creating these threads, how many there are, etc. NSOperationQueue manages all that. You can just worry about giving it work and letting it deal with it. So let's do just that.
The first thing we need to do is create an operation queue. I do this lazily in my viewDidLoad method in my UIViewController for my iPhone application:
@interface MyViewController : UIViewController
{
NSOperationQueue *operationQueue;
}
- (void)viewDidLoad
{
if (operationQueue == nil)
operationQueue = [[NSOperationQueue alloc] init];
[super viewDidLoad];
}
And that's all that's required to get set up (you should remember of course to release the queue too when you're done with it). Now we're ready to take our method above and move it to threads. Let's assume we'll move the heavy lifting of our processIncomingData method above into a new method that will just tod the CoreData work called storeDataInCoreData, which we'll define later. It's this method we'll want to call in another thread. To do that we change the processing method to look like this:
- (void)processIncomingData: (ASIHTTPRequest*)request
{
NSString *rawdata = [request responseString];
NSArray* lotsOfData = [rawdata JSONValue];
[request release];
NSInvocationOperation *operation;
operation = [[NSInvocationOperation alloc] initWithTarget: self
selector: @selector(storeDataInCoreData:)
object: lotsOfData];
[operationQueue addOperation: operation];
}
Again, that's it - nice and clean and simple. We build an invocation operation which will run the specified method on a given object and pass that to our operation queue. This will now happen at some point in another thread, and we didn't have to do much at all. Now we just need to look at the way we use CoreData in our new thread, so let's take a look at storeDataInCoreData.
If you use the standard iPhone CoreData project templates, you'll be used to asking the UIApplication delegate for the managed object context. This is a really handy way to save you creating many contexts, particularly as the context also acts as a sort of cache for managed objects and data. Unfortunately you can't share contexts across threads, as they don't support concurrent access, we'll need to create a new NSManagedObjectContext for our thread to use. Thankfully, that's not too difficult.
The other thing we want to do is tell our view context to update any views on the data, which we did originally by calling a self definied reloadInfoFromCoreData method. We still want to do this, but we need to call it back on the main thread, as it's probably going to mangle data structures that the UI uses, and we don't want to worry about locking any of that, so we'll just let the main thread still handle the data reload. Thankfully it's easy to call back to the main thread from the worker thread.
Thus, we end up with:
- (void)storeDataInCoreData: (NSArray*)lotsOfData
{
// create our new context
MyApplicationDelegate *delegate = [UIApplication sharedApplication];
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
[context setPersistentStoreCoordinator: [delegate persistentStoreCoordinator]];
// do the work
for (NSDictionary* info in lotsOfData)
{
// create or update managed object here
// probably some searching too
}
NSError *error = nil;
[context save: *error];
if (error)
NSLog(@"error %@", [error localizedDescription]);
// free up our context
[context release];
// let the app know we're done
[self performSelectorOnMainThread: @selector(reloadInfoFromCoreData)
withObject: nil
waitUntilDone: YES];
}
Here you can see that the method is slightly longer than in the single threaded case it would be, but nothing too complicated. We create a new managed object context over the same CoreData store, use that for all our CoreData operations, then release it once done. After that we call back to the main thread to tell it it can now refresh its data from the CoreData store. And here comes in the last detail you need.
If you recall we said one of the nice things about NSManagedObjectContext was that it cached data to save you time. So if you've already loaded data from a particular table in the CoreData store, althought you've added or updated all this new data in the worker thread, the main thread by default won't see it, as there's no timeout on the cached data in the application delegates managed object context. We need to force a cache flush before the view controller will see the updated data.
In my application I have a root object that has relationships to all the rest of the data: my user object then has relationships to all the data they're interested in. So what I want to do is force a reload on that, and as I traverse relationships they'll all fault and load in new data. So in the top of my reloadInfoFromCoreData method I simply need to do:
MyApplicationDelegate *delegate = [UIApplication sharedApplication];
NSManagedObjectContext *context = [delegate managedObjectContext];
[context refreshObject: myRootManagedObject
mergeChanges: NO];
After this, any data I retrieve via this root object will be refetched from the store.
And that's all there is to it. We've managed with relatively little code and little complexity to move our CoreData processing to a child thread, meaning we can process lots of data without our UI becoming unresponsive. There's lots more you can do with NSOperationQueue, and I recommend you investigate it to see how it can help you defer batch processing from your main thread where possible. It's a small overhead to your code, but guarantees a much better user experience if you're not locking up your UI all the time.