【问题标题】:Strange issue with privateQueue managedObjectContextprivateQueue managedObjectContext 的奇怪问题
【发布时间】:2017-08-02 17:48:07
【问题描述】:

设置(您可以稍后阅读并先跳到场景部分)

这是一个旧应用程序,手动设置 CoreData 堆栈如下:

+ (NSManagedObjectContext *)masterManagedObjectContext
{
    if (_masterManagedObjectContext) {
        return _masterManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self createPersistentStoreCoordinator];

    if (coordinator != nil) {
        _masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        _masterManagedObjectContext.retainsRegisteredObjects = YES;
        _masterManagedObjectContext.mergePolicy = NSOverwriteMergePolicy;
        _masterManagedObjectContext.persistentStoreCoordinator = coordinator;
    }
    return _masterManagedObjectContext;
}

+ (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext) {
        return _managedObjectContext;
    }

    NSManagedObjectContext *masterContext = [self masterManagedObjectContext];

    if (masterContext) {
        _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        _managedObjectContext.retainsRegisteredObjects = YES;
        _managedObjectContext.mergePolicy = NSOverwriteMergePolicy;
        _managedObjectContext.parentContext = masterContext;
    }

    return _managedObjectContext;
}

+ (NSManagedObjectContext *)newManagedObjectContext
{
    __block NSManagedObjectContext *newContext = nil;
    NSManagedObjectContext *parentContext = [self managedObjectContext];

    if (parentContext) {
        newContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        newContext.parentContext = parentContext;
    }

    return newContext;
}

然后递归保存上下文:

+ (void)saveContext:(NSManagedObjectContext *)context
{
    [context performBlockAndWait:^{
        if (context.hasChanges && context.persistentStoreCoordinator.persistentStores.count) {
            NSError *error = nil;

            if ([context save:&error]) {
                NSLog(@"saved context: %@", context);

                // Recursive save parent context.
                if (context.parentContext) [self saveContext:context.parentContext];
            }
            else {
                // do some real error handling
                NSLog(@"Could not save master context due to %@", error);
            }
        }
    }];
}

场景

应用程序从服务器加载大量数据,然后先在newContext内部执行更新,然后合并到mainContext -> masterContext -> persistentStore

由于数据量大,同步过程被分成了大约 10 个异步线程 => 我们一次有 10 个newContext

现在,数据很复杂,比如parents <-> children (same class)。 1 个parent 可以有多个children,一个child 可以有一个mother, father, god father, step mother...,所以是n-n relationship。首先,我们获取parent,然后执行获取child,然后将child设置为parent,以此类推。

服务器有点笨,它不能发送禁用的对象。但是客户想从后端控制应用程序对象的显示,所以我有 2 个属性可以做到这一点:

  1. hasUpdated:在加载过程开始时,执行批量更新,将所有对象的hasUpdated设置为NO。从服务器获取数据后,将此属性更新为 YES。
  2. isActive:当所有加载完成后,如果hasUpdate == NO,则将此属性批量更新为NO。然后,我有一个过滤器不会显示 isActive == NO 的对象

问题

客户抱怨为什么即使在后端启用了某些对象也会丢失。遇到这个奇怪的问题后,我一直在挣扎和调试:

  1. newContext.updatedObjects : { obj1.ID = 100, hasUpdated == YES }
  2. “保存的新上下文”
  3. mainContext.updatedObjects: {obj1.ID = 100, hasUpdated == NO }

// 我会停在这里。显然,master 得到了更新 = NO,最后isActive 将设置为 no,这会导致对象丢失。

如果它每次都发生,那么可能更容易修复(¿也许?)。但是,它是这样发生的:

  • 第一次运行(第一次,我的意思是应用程序从调用 appDidFinishLaunch... 的位置开始):都正确
  • 第二次:丢失(153 个对象)
  • 第三次:全部正确
  • 第四次:失踪(153 个对象)(再次?正是那些有多个父母的人,我相信是这样!)
  • 第 5 次:再次更正
  • ... 以此类推。

此外,这似乎发生在具有相同上下文(相同newContext)的对象上。难以置信。

问题

为什么会这样?我该如何解决?如果这些对象没有孩子,我的生活会更轻松!!!!

奖金

如果您想知道批量更新是如何进行的,请看下面。注意:

  1. 下载请求在异步队列中:_shareInstance.apiQueue = dispatch_queue_create("product_request_queue", DISPATCH_QUEUE_CONCURRENT);
  2. 解析响应和更新属性在队列中是同步的:_shareInstance.saveQueue = dispatch_queue_create("product_save_queue", DISPATCH_QUEUE_SERIAL);
  3. 每当解析完成时,我都会在同一个串行队列中执行保存newContext 并调用updateProductActiveStatus:。如果所有请求都完成,则执行批量更新状态。由于请求是在并发队列中完成的,它总是比保存(串行)队列早完成,所以这几乎是一个简单的过程。

代码:

// Load Manager
- (void)resetProductUpdatedStatus
{
    NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
    request.propertiesToUpdate = @{ @"hasUpdated" : @(NO) };
    request.resultType = NSUpdatedObjectsCountResultType;

    NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];

    NSLog(@"Batch update hasUpdated: %@", result.result);

    [self.masterContext performBlockAndWait:^{
        [self.masterContext refreshAllObjects];

        [[CoreDataUtil managedObjectContext] performBlockAndWait:^{
            [[CoreDataUtil managedObjectContext] refreshAllObjects];
        }];
    }];
}

- (void)updateProductActiveStatus:(SyncComplete)callback
{
    if (self.apiRequestList.count) return;

    NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
    request.predicate = [NSPredicate predicateWithFormat:@"hasUpdated = NO AND isActive = YES"];
    request.propertiesToUpdate = @{ @"isActive" : @(NO) };
    request.resultType = NSUpdatedObjectsCountResultType;

    NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
    NSLog(@"Batch update isActive: %@", result.result);

    [self.masterContext performBlockAndWait:^{
        [self.masterContext refreshAllObjects];

        NSManagedObjectContext *maincontext = [CoreDataUtil managedObjectContext];
        NSLog(@"Refreshed master");

        [maincontext performBlockAndWait:^{
            [maincontext refreshAllObjects];

            NSLog(@"Refreshed main");

            // Callback
            if (callback) dispatch_async(dispatch_get_main_queue(), ^{ callback(YES, nil); });
        }];
    }];
}

【问题讨论】:

    标签: ios objective-c core-data concurrency batch-updates


    【解决方案1】:

    mergePolicy 是邪恶的。唯一正确的 mergePolicy 是 NSErrorMergePolicy 任何其他策略都是要求 core-data 静默失败,并且不会在您期望的时候更新。

    我怀疑您的问题是您正在使用背景上下文同时写入核心数据。 (我知道你说你有一个串行队列——但如果你在队列中调用 performBlock ,那么每个块都会同时执行)。当发生冲突时,东西会被覆盖。您应该只以一种同步方式写入核心数据。

    我写了一个关于如何使用 NSPersistentContainer 完成此任务的答案: NSPersistentContainer concurrency for saving to core data 我建议您将代码迁移到它。真的不应该那么难。

    如果您想让代码尽可能接近当前的代码,那也不是那么难。

    制作串行操作队列:

    _persistentContainerQueue = [[NSOperationQueue alloc] init];
    _persistentContainerQueue.maxConcurrentOperationCount = 1;
    

    并使用此队列进行所有写入:

    - (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
        void (^blockCopy)(NSManagedObjectContext*) = [block copy];
    
        [self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
            NSManagedObjectContext* context =  [CoreDataUtil newManagedObjectContext];
            [context performBlockAndWait:^{
                blockCopy(context);
                [CoreDataUtil saveContext:context];
            }];
        }]];
    }
    

    也可能是对象已更新,但您没有看到它,因为您依赖于 fetchedResultsController 来更新。并且 fetchedResultsController 不会从批量更新请求中更新。

    【讨论】:

    • 感谢您的回答,我会尝试迁移这个并告诉您结果!作为您的最后评论,我 100% 确定这里不是这种情况。数据列表正确更新,这就是为什么客户抱怨数据在同步后出现和消失的原因。另外,你能告诉我如何处理NSErrorMergePolicy吗?它会落入[context save:&error]错误分支吗??
    • 在这个问题上挣扎了几天(将近半个月)后,我放弃了。使用MagicalRecord 进行一些简单的迁移并像以前一样执行批量更新:使用NSFetchRequest,执行获取并保存。不知何故,我仍然使用您的enqueueCoreDataBlock: 进行所有更新和保存。感谢您的工作:(
    猜你喜欢
    • 1970-01-01
    • 2015-02-27
    • 2018-06-26
    • 2011-08-13
    • 2011-08-17
    • 2011-02-02
    • 2021-10-10
    • 2014-04-21
    • 2010-12-30
    相关资源
    最近更新 更多