【问题标题】:A Few Questions About Using Core Data and Multithreading关于使用核心数据和多线程的几个问题
【发布时间】:2013-12-28 08:32:24
【问题描述】:

我正在制作一个应用程序,它解析链接返回的 XML 并使用 Core Data 保存该数据。问题是,为了让 UI 保持响应,我必须在后台线程中进行所有解析。谷歌搜索了一段时间后,我在 AppDelegate 中设置了两个上下文,一个用于处理背景解析,一个用于加载 UI。

首先:这是为我的情况设置两个上下文的正确方法吗?

//In AppDelegate.m

//This one is for parsing in the background
- (NSManagedObjectContext *)backgroundManagedObjectContext {
    if (_backgroundManagedObjectContext != nil) {
         return _backgroundManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        _backgroundManagedObjectContext.persistentStoreCoordinator = coordinator;
    }

    return _backgroundManagedObjectContext;
}

//This one is for updating the UI
- (NSManagedObjectContext *)mainManagedObjectContext {
    if (_mainManagedObjectContext != nil) {
        return _mainManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
         _mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        _mainManagedObjectContext.persistentStoreCoordinator = coordinator;
        _mainManagedObjectContext.parentContext = self.backgroundManagedObjectContext];
    }

    return _mainManagedObjectContext;
}

第二:这是调用各个同步和加载方法的正确方法吗?

然后我会调用我的同步方法(用于后台):

[self.appDelegate.backgroundManagedObjectContext performBlock:^{
    [self syncData];
}

并调用我的加载方法(用于 UI)

[self.appDelegate.mainManagedObjectContext performBlock:^{
    [self loadData];
}

第三:我的核心数据以一个实体(用户)为中心,所有其他实体都连接到该实体(用户)。我是否必须创建一个局部变量并在每次我想使用它时获取它,还是有更好的方法使 NSManagedObject 不会在两个线程之间共享?

第四:如果我保存了父背景上下文,子上下文(UI)会自动更新还是有我需要调用的特定方法?

【问题讨论】:

  • mainManagedObjectContext 被调用时,_backgroundManagedObjectContext 是否有可能成为nil
  • @AaronBrager 这就是我问这是否是正确设置的原因。我应该在- (NSManagedObjectContext *)mainManagedObjectContext 方法中检查吗?
  • 或者你可以使用[self backgroundManagedObjectContext]而不是_ _backgroundManagedObjectContext,如果它没有被实例化,它会实例化它。
  • @AaronBrager 我编辑了问题以包含您的建议。你对我的任何更一般的问题有答案吗?

标签: ios objective-c


【解决方案1】:

对于 Core Data,最好的学习方法似乎是与有效的方法进行比较。我将发布一些处理类似于您的情况的代码。

首先:这是为我的情况设置两个上下文的正确方法吗?

这就是我现在的 AppDelegate 中的内容:

- (NSManagedObjectContext *)auxiliaryManagedObjectContext {
    NSManagedObjectContext *managedObjectContext = nil;

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        managedObjectContext = [[NSManagedObjectContext alloc] init];
        [managedObjectContext setPersistentStoreCoordinator:coordinator];
        [managedObjectContext setUndoManager:nil];
    }

    return managedObjectContext;
}

- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _managedObjectContext = [[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }

    return _managedObjectContext;
}

第二:这是否是调用单个同步和 加载方法?

这实际上取决于这些方法中发生的情况。当您在后台处理上下文时,您应该向设置发送一个通知,该通知将在进行更改时触发。在此方法中,您可以将后台上下文中的更改合并到 UI 上下文中:

dispatch_queue_t background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long) NULL);
dispatch_async(background, ^{


    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *backgroundManagedObjectContext = [appDelegate auxiliaryManagedObjectContext];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(mergeContexts:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:backgroundManagedObjectContext];

    NSManagedObject *threadSafeManagedObject =
    [backgroundManagedObjectContext objectWithID:self.currentManagedObject.objectID];

    NSManagedObject *insertedThreadSafeManagedObject = [NSEntityDescription insertNewObjectForEntityForName:@"Entity" inManagedObjectContext:backgroundManagedObjectContext];

    NSError *error;
    // Will call the mergeContexts: selector registered above
    if(![backgroundManagedObjectContext save:&error]) {
        NSLog(@"Error! %@", error);
    }
});

- (void)mergeContexts:(NSNotification *)notification {
    SEL selector = @selector(mergeChangesFromContextDidSaveNotification:);
    [self.managedObjectContext performSelectorOnMainThread:selector withObject:notification waitUntilDone:YES];
}

在 mergeContexts: 方法中,背景上下文与您的 UI 上下文合并。从后台开始的上下文一直在后台运行,反之亦然,这一点非常重要。此外,托管对象不是线程安全的,因此要在后台线程中使用来自 UI 线程的对象,您必须将其传输到后台上下文视图中,如上例所示。

第三:我的核心数据以一个实体(用户)为中心, 其他实体连接到。我是否必须创建一个局部变量 并在每次我想使用它时获取它,或者有更好的方法吗 NSManagedObject 不是在两个线程之间共享的?

上面的描述应该已经回答了这个问题。

第四:如果我保存父背景上下文,孩子会不会 上下文(UI)会自动更新还是有特定的方法我 需要打电话吗?

这就是上面例子中的 mergeContexts: 方法。让我知道这是否有意义。

编辑 1:initWithConcurrencyType

正如您所提到的,在托管对象上下文的初始化过程中使用了 NSMainQueueConcurrencyType 和 NSPrivateQueueConcurrencyType 来处理后台和 UI 上下文之间的这种来回交换。

AppDelegate 初始化器实际上是相同的。唯一的区别是 init 语句:

// Background context
managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];

// Main thread context
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

然后,您将获取后台上下文(在这种情况下从 AppDelegate 获取)并使用后台线程开始工作并在主线程上完成工作:

AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *backgroundManagedObjectContext = [appDelegate auxiliaryManagedObjectContext];

[backgroundManagedObjectContext performBlock:^{
    // Do work
    [self.managedObjectContext performBlock:^{
        // merge work into main context
    }];
}]; 

声明应用相同的线程安全规则很重要(例如,必须将背景对象转移到主线程查看其 objectID)。要将工作从后台线程传递到主线程,您应该在后台上下文的执行块中调用主线程上下文的 performBlock。尽管无论您如何初始化上下文,合并上下文的便利性仍然存在于通知安排中。

编辑 2:子/父上下文

这是来自Correct implementation of parent/child NSManagedObjectContext 的父/子示例:

- (void)saveContexts {
    [childContext performBlock:^{
        NSError *childError = nil;
        if ([childContext save:&childError) {
            [parentContext performBlock:^{
                NSError *parentError = nil;
                if (![parentContext save:&parentError]) {
                    NSLog(@"Error saving parent");
                }
            }];
        } else {
            NSLog(@"Error saving child");
        }
    }];
}

在我们的例子中,初始化子上下文时,将其父上下文设置为您的 UI 上下文:

[backgroundManagedObjectContext setParentContext:_managedObjectContext];

【讨论】:

  • 感谢您的详细解答!这是有道理的,但我相信我在某处读到 NSPrivateQueueConcurrencyTypeNSMainQueueConcurrencyType 应该替换它。对于我的情况,使用您的示例代码或使用这些代码会更好吗?
  • 另外,如果我有一个包含所有其他实体(通过关系)的实体,我可以在我的 AppDelegate(self.appDelegate.userself.appDelegate.backgroundUser)中创建它的两个实例,以便我所有的 ViewControllers 都可以访问给这个用户(特别是因为我也在使用NSXMLParsers?并确保只在各自的线程中使用每个。
  • 我认为拥有两个全球版本的用户确实不是一个很好的理由。当您想在后台使用当前用户时,使用NSManagedObject *threadSafeManagedObject = [backgroundManagedObjectContext objectWithID:self.currentManagedObject.objectID]; 注册NSManagedObjectContextDidSaveNotification 进行所需的更改,然后保存并将更改合并到您的主上下文中。恕我直言,维护两个全局版本令人头疼,不值得付出努力。
  • 请注意,在整个应用程序中维护任何对象的状态已经是一项挑战。保持两个甚至更多。在两个单独的线程上维护两个是自找麻烦。即使是最有经验的多线程程序员也难以确保对象是线程安全的。
  • 所以基本上我应该只存储用户 (NSManagedObject) 的 ID,然后每次都根据我所在的线程获取它。此外,我在某处阅读了关于将一个设置为父级的内容,这应该自动无需注册通知即可更新。这是一条更简单的路线还是不正确?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多