New thread + NSManagedObjectContext

风流意气都作罢 提交于 2019-12-03 20:12:12

You are using the old concurrency model of thread confinement, and violating it's rules (as described in the Core Data Concurrency Guide, which has not been updated yet for queue confinement). Specifically, you are trying to use an NSManagedObjectContext or NSManagedObject between multiple threads. This is bad. Thread confinement should not be used for new code, only to maintain the compatibility of old code while it's being migrated to queue confinement. This does not seem to apply to you.

To use queue confinement to solve your problem, first you should create a context attached to your persistent store coordinator. This will serve as the parent for all other contexts:

+ (NSManagedObjectContent *) parentContextWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)coordinator {
    NSManagedObjectContext  *result = nil;

    result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [result setPersistentStoreCoordinator:coordinator];

    return result;
}

Next, you want the ability to create child managed object contexts. You will use these to perform work on the data, wether reading or writing. An NSManagedObjectContext is a scratchpad of the work you are doing. You can think of it as a transaction. For example, if you're updating the store from a detail view controller you would create a new child context. Or if you were performing a multi-step import of a large data set, you would create a child for each step.

This will create a new child context from a parent:

+ (NSManagedObjectContext *) childContextWithParent:(NSManagedObjectContext *)parent {
    NSManagedObjectContext  *result = nil;

    result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [result setParent:parent];

    return result;
}

Now you have a parent context, and you can create child contexts to perform work. To perform work on a context, you must wrap that work in performBlock: to execute it on the context's queue. I do not recommend using performBlockAndWait:. That is intended only for re-rentrant methods, and does not provide an autorelease pool or processing of user events (user events are what drives nearly all of Core Data, so they're important. performBlockAndWait: is an easy way to introduce bugs).

Instead of performBlockAndWait: for your example above, create a method that takes a block to process the results of your fetch. The fetch, and the block, will run from the context's queue - the threading is done for you by Core Data:

- (void) doThingWithFetchResults:(void (^)(NSArray *results, NSError *error))resultsHandler{
    if (resultsHandler != nil){
        [[self context] performBlock:^{
            NSArray *fetchResults = [[self context] executeFetchRequest:request error:&error];
            resultsHandler(fetchResults, error);
        }];
    }
}

Which you would call like this:

[self doThingsWithFetchResults:^(NSArray *something, NSError *error){
    if ([something count] > 0){
      // Do stuff with your array of managed objects
    } else {
      // Handle the error
    }
}];

That said, always prefer using an NSFetchedResultsController over using executeFetch:. There seems to be a belief that NSFetchedResultsController is for powering table views or that it can only be used from the main thread or queue. This is not true. A fetched results controller can be used with a private queue context as shown above, it does not require a main queue context. The delegate callbacks the fetched results controller emits will come from whatever queue it's context is using, so UIKit calls need to be made on the main queue inside your delegate method implementations. The one issue with using a fetched results controller this way is that caching does not work due to a bug. Again, always prefer the higher level NSFetchedResultsController to executeFetch:.

When you save a context using queue confinement you are only saving that context, and the save will push the changes in that context to it's parent. To save to the store you must recursively save all the way. This is easy to do. Save the current context, then call save on the parent as well. Doing this recursively will save all the way to the store - the context that has no parent context.

Example:

- (void) saveContextAllTheWayBaby:(NSManagedObjectContext *)context {
[context performBlock:^{
        NSError *error  = nil;
        if (![context save:&error]){
            // Handle the error appropriately.
        } else {
            [self saveContextAllTheWayBaby:[context parentContext]];
        }

    }];

}

You do not, and should not, use merge notifications and mergeChangesFromContextDidSaveNotification: with queue confinement. mergeChangesFromContextDidSaveNotification: is a mechanism for the thread confinement model that is replaced by the parent-child context model. Using it can cause a whole slew of problems.

Following the examples above you should be able to abandon thread confinement and all of the issues that come with it. The problems you are seeing with your current implementation are only the tip of the iceberg.

There are a number of Core Data sessions from the past several years of WWDC that may also be of help. The 2012 WWDC Session "Core Data Best Practices" should be of particular interest.

if you want to use managed object context in background thread, there are two approaches,

1 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set the parentContext to main thread context

2 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set persistentStoreCoordinator to main thread persistentStoreCoordinator

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {

    NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    privateContext.persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator;

    [[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
        NSManagedObjectContext *moc = mainManagedObjectContext;
        if (note.object != moc) {
            [moc mergeChangesFromContextDidSaveNotification:note];
        }
    }];

    // do work here
    // remember managed object is not thread save, so you need to reload the object in private context
});

before exist the thread, make sure remove the observer, bad thing can happen if you don't

for more details read http://www.objc.io/issue-2/common-background-practices.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!