Question

So the objective I'm trying to achieve

is a syncing process that is supposed to be completed in the background using AFNetworking and Magical Record, but causes a perpetual hang when a view controller hooked up to a NSFetchedResultsController is currently open or has been opened (but popped).

The app syncs the first time the user opens the phone and then uses the data always in the Core Data persistance store via the Magical Record framework. Then when the user wants to make sure that there data is the most recent version, they go into settings and click "Re-Sync", which causes the following code to execute:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^{
    [[CoreDataOperator sharedOperator] commenceSync:self];
});

This starts the syncing process using the singleton CoreDataOperator (subclass of NSObject -- maybe it should be NSOperation?), which fires off the following code:

[[ApiClient sharedClient] getDataRequest];

which then then fires off this bad boy in the singleton AFHTTPClient subclass:

[[ApiClient sharedClient] postPath:url parameters:dict
 success:^(AFHTTPRequestOperation *operation, id responseObject) {
 [request.sender performSelector:request.succeeded withObject:response];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
 [request.sender performSelector:request.failed withObject:response];
}
 ];

which effectively says: AFHTTPClient post this infomation and when it succeeds, pass back the information to the provided selector (I understand that this is generic, but the request isn't the issue)

NOW, AFNetworking is coded such that all completion selectors (in this case specifically, success and failure) are called on the main thread; so in order to prevent blocking the main thread, the code that processes the response and prepares it for saving is sent back into the background thread:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    id responseData;
    [self clearAndSaveGetData:responseData];
});

This then calls the function that causes the saving using the Magical Record framework (still in background thread):

NSManagedObjectContext *localContext = [NSManagedObjectContext contextForCurrentThread];

[DATAENTITY truncateAllInContext:localContext];
<!--- PROCESS DATA INTO DATAENTITY -->
[localContext saveNestedContexts];

I chose saveNestedContexts because since I am working in the background, I wanted it pushed all the way up to the defaults contexts, which I'm assuming is a parent context? (but this so far hasn't been an issue).

Now, this data can become thousands and thousands of rows, so I'm using a NSFetchedResultsController to access this data safely and efficiently, and they are used in a different view controller than the settings or main page.

Here are three cases:

  1. The ViewController containing the FRC has not been accessed (not visible and hasn't been visible before) - the background sync works flawlessly, minus a little lag due to the saving up the stack.
  2. The ViewController containing the FRC has been accessed and is currently visible - background sync process HANGS due to what appears to be the FRC receiving context updates.
  3. The ViewController containing the FRC has been previously access, but it isn't currently visible (the VC was popped using the following code: [self.navigationController popViewControllerAnimated:YES];) AND the FRC is set to nil in ViewDidUnload - background sync process HANGS due to what appears to be the FRC receiving context updates (just like in case 2).

[PS: after hanging for a few minutes, I kill the debugger and it gives my a SIGKILL on the following code, which is why I'm assuing the FRC is receiving context updates that causes it to hang:

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller 
{
    UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
    [tableView endUpdates]; <---- SIGKILL
}

Other important information of note:

  • I am using 2 separate FRCs, one for normal ALL data and one for searching
  • I am using a cache for both the FRC and the separate search FRC, which is purged at the appropriate times (before this context update stuff)
  • The FRCs are fetching in the mainthread (which does cause a slight hang when there is THOUSANDS of rows of data) and I have been looking into fetching in the background, however that is currently not implemented.

The QUESTION(s):

  1. Why is this hanging occuring, where the VC is visible or has been popped?
  2. How can I make it such that the FRC doesn't listen for the save, but uses what it has until the save is completed and then it refreshes the data (unless this is what already occuring and causing the hanging)?
  3. Would implementing a background fetch (because the lag when opening the VC with the FRC to access the thousands of rows of data creates a noticable lag, even with a cache and lessened predicates/section headers - of between 1 and 4 seconds) be viable? Too complex? How can it be done?

Thank you, hope I was detailed enough in my question.

Was it helpful?

Solution

I know this question is old but since this is a common gotcha with MagicalRecord maybe the following will help somebody.

The problem is caused by the fetchRequest of the FRC and the save operation deadlocking each other. The following solved it for me:

Update to the latest MagicalRecord release (2.1 at the time of writing).

Then do all your background saves using the following:

MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
  // create new objects, update existing objects. make sure you're only 
  // accessing objects inside localContext 
  [myObjectFromOutsideTheBlock MR_inContext:localContext]; //is your friend
}
completion:^(BOOL success, NSError *error) {
  // this gets called in the main queue. safe to update UI.
}];
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top