使用 GCD 和 Core Data 会导致崩溃

Posted

技术标签:

【中文标题】使用 GCD 和 Core Data 会导致崩溃【英文标题】:Using GCD and Core Data causes crash 【发布时间】:2014-02-19 17:51:13 【问题描述】:

我从我们的服务器检索数据,我需要处理它。

对于每个键,我创建一个NSManagedObject。每个对象都是在相同的上下文中创建的。我正在使用魔法唱片。

-(id)init 
    if (self = [super init])
        self.context = [NSManagedObjectContext MR_contextForCurrentThread];
    

    return self;

线程:

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_t group = dispatch_group_create();

for (id key in boundariesDictionary) 
    dispatch_group_async(group, queue, ^
        DLog(@"nsthread: %@", [NSThread currentThread]);

        NSString *boundaryIDString;

        if ([key isKindOfClass:[NSString class]]) 
            boundaryIDString = key;
        
        else if ([key isKindOfClass:[NSNumber class]]) 
            boundaryIDString = [key stringValue];
        

        if (boundaryIDString) 
            DLog(@"boundaryIDString: %@", boundaryIDString)

            NSDictionary *boundaryDictionary = [boundariesDictionary objectForKey:key];

            Boundary *boundary = [Boundary MR_findFirstWithPredicate:[NSPredicate predicateWithFormat:@"boundaryID == %@ AND api == %@", [NSNumber numberWithInteger:[boundaryIDString integerValue]], self.serverCall.API] inContext:self.context];

            if ([boundaryDictionary objectForKey:AVI_NAME]) 
                if (boundary == nil) 
                    DLog(@"creating boundary %@", boundaryIDString);

                    boundary = [Boundary MR_createInContext:self.context];
                    boundary.boundaryID = [NSNumber numberWithInteger:[boundaryIDString integerValue]];
                
            

            boundary = [self processBoundary:boundary fromBoundaryDictionary:boundaryDictionary];
         
    

[self processBoundary] 只是获取字典并将其设置为托管对象的属性。

if ([boundaryDictionary objectForKey:@"name"]) 
    boundary.name = [boundaryDictionary objectForKey:@"name"];


//more data processing

这会导致错误:

*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x1776f7f0> was mutated while being enumerated.'

如果我不为每个线程使用相同的上下文,它运行良好。

除了我列举的NSDictionary boundsDictionary 之外,我不明白什么设置。我根本没有改变边界字典,只是将数据复制到核心数据中。

当我 PO 对象(在本例中为 0x1776f7f0)时,我会得到一组 Boundary 对象的列表。那些Boundary 对象只存在于NSManagedObjectContext“集合”中,我不会将它们添加到NSArrayNSDictionaryNSSet。但我不相信我列举了那套。我确实通过为它创建新的边界对象来改变它。

我认为发生了一些我不理解或完全掌握的事情。

有什么想法吗?

更新:

    for (id key in boundariesDictionary) 
        NSString *boundaryIDString;

        if ([key isKindOfClass:[NSString class]]) 
            boundaryIDString = key;
        
        else if ([key isKindOfClass:[NSNumber class]]) 
            boundaryIDString = [key stringValue];
        

        if (boundaryIDString) 
            [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) 
                DLog(@"saveWithBlock thread: %@", [NSThread currentThread]);

                NSDictionary *boundaryDictionary = [boundariesDictionary objectForKey:key];

                Boundary *boundary = [Boundary MR_findFirstWithPredicate:[NSPredicate predicateWithFormat:@"boundaryID == %@ AND api == %@", [NSNumber numberWithInteger:[boundaryIDString integerValue]], self.serverCall.API] inContext:localContext];

                if ([boundaryDictionary objectForKey:AVI_NAME]) 
                    if (boundary == nil) 
                        boundary = [Boundary MR_createInContext:localContext];
                        boundary.boundaryID = [NSNumber numberWithInteger:[boundaryIDString integerValue]];
                    

                    boundary = [self processBoundary:boundary fromBoundaryDictionary:boundaryDictionary];

                    if (boundary == nil) 
                        //Prompt Error
                    
                    else 
                        for (NSNumber *groupID in groupIDs) 
                            if ([groupID isKindOfClass:[NSNumber class]]) 
                                Group *group = [Group MR_findFirstWithPredicate:[NSPredicate predicateWithFormat:@"groupID == %@ OR groupID == 0 AND api == %@", groupID, self.serverCall.API]];

                                if (group != nil) 
                                    group.lastUpdated = [NSDate date];

                                    [group addBoundariesObject:boundary];
                                
                                else 
                                    DLog(@"group %@ DNE", groupID);
                                
                            
                        
                    
                
             completion:^(BOOL success, NSError *error) 
                DLog(@"saveWithBlock completion Block | time: %f", [[NSDate date] timeIntervalSinceDate:startTime]);
            ];
        
    

所以对于我的Group 29 应该看到我正在创建的所有边界,但事实并非如此。其不一致。有时看到全部,有时看到一些,有时没有。

还有,我经常看到

NO CHANGES IN ** BACKGROUND SAVING (ROOT) ** CONTEXT - NOT SAVING

在日志中。我看到的这些消息的数量也不一致,而保存的上下文将插入超过 1 个对象。

不确定它是否应该表现得如此,如果每个块都有自己的上下文并且每个块只创建 1 个对象,它似乎应该是 1 比 1 的比率。

我记录每个线程 ID,并为每个块创建一个新线程。没有线程 ID 被记录两次,因此不应重复使用线程。

【问题讨论】:

【参考方案1】:

托管对象上下文不是线程安全的,您不得使用它们或任何托管对象/集合/数据结构/它们可能从不同队列或线程返回的任何内容。

我不知道魔法记录是否支持它,但你绝对应该为你的上下文使用队列包含,然后你必须在 MOC 的私有队列的上下文中做所有事情。

如果您使用线程包含,则必须保证上下文和由上下文创建的任何托管对象始终是串行访问的,而您在上面的代码中绝对不会这样做。

【讨论】:

神奇的记录有助于减少样板代码并有助于线程化。它允许您创建上下文并保存回所有新上下文产生的主上下文。它有助于使核心数据更容易成为线程安全的。在这一点上,我还没有保存它。只是试图在相同的上下文中创建它们。 @Log139 我知道 Magical Record 是什么,但上面的代码在线程方面对您没有任何帮助。它使用的代码不是线程安全的。你不能合法地用 CoreData 做你想做的事情。 CoreData 要求您串行访问上下文。 不,如果您遵循托管对象和上下文不应该跨越线程边界的简单规则,那么核心数据当然可以与线程和队列一起使用。这里的问题是 gcd 队列不是线程。它们重用线程,因此您可能违反了这条简单的规则,将您的上下文绑定到线程并重用它。 @casademora 这就是我所说的,它与线程边界无关,而是对上下文和托管对象的串行访问。例如,如果您使用线程包含和串行队列,即使工作块最终在不同的线程上执行,您也不会遇到问题。在这里,他同时跨多个线程/队列访问相同的上下文和托管对象。这对 CoreData 来说是完全违法的。 当然,具有单个约束上下文的串行队列现在可以工作,但更改队列类型最终会导致代码曾经工作的崩溃。总的来说,我所规定的方法是我从许多应用程序崩溃中吸取的经验教训中的良好做法。【参考方案2】:

您的问题是 gcd 队列线程不是一对一的映射。 GCD 重用线程,因此您可能会在不知不觉中跨越线程边界。我的建议是简单地创建一个新上下文并停止使用 contextForCurrentThread。我写了有关on my blog 问题的更多详细信息。 ContextForCurrentThread 将在即将发布的版本中删除。

【讨论】:

如果他使用串行队列,他可能还会考虑dispatch_[set/get]_specific 将上下文(或其他对象)直接与队列而不是线程相关联。他不是,所以这是评论而不是答案。 完全错误。他正在从多个线程同时访问上下文和托管对象。线程包含有效,因为如果对上下文/对象的所有访问都在同一个线程上,则可以保证是串行访问。您还可以使用具有线程包含的串行队列并获得相同的效果,只要所有访问始终在串行队列上执行的块内完成。在这里,他使用的是全局队列,因此即使创建上下文也无济于事,因为它仍然会违反 CoreData 的多线程策略。 @casademora 我实现了 [MagicalRecord saveWithBlock:^]。它创建了新实体,但很难保存关系。每个边界都需要设置为一个组。 group.boundaries 不一致。假设我有 20 个边界对象,所有对象都应该与一个组有关系。有时该组有 10 个边界,有时是 16 个,有时是全部或没有关系。我将更新我的问题以包括我如何设置关系。 将问题更新为 [MagicalRecord saveWithBlock:^] 后的结果。 确保您还在使用 inContext: 参数在与边界相同的上下文中创建组。看起来你没有这样做。

以上是关于使用 GCD 和 Core Data 会导致崩溃的主要内容,如果未能解决你的问题,请参考以下文章

发送到 Core Data 的无法识别的选择器导致应用程序崩溃

将对象添加到 NSSet 时与 Core Data 的反比关系导致崩溃

iOS:将 GCD 与 Core Data 结合使用

Core Data - executeFetchRequest Swift 1.2 发布模式崩溃

使用 Core Data 等待 withTaskGroup 有时会失败

重新启动活动时,Android Wear 上的融合位置提供程序会导致崩溃