【问题标题】:How to make changes to Core Data objects from different threads without having to save after every change如何从不同的线程对 Core Data 对象进行更改,而无需在每次更改后保存
【发布时间】:2011-10-11 18:24:58
【问题描述】:

我仔细阅读了 SO 上的所有相关线程,但仍然对如何从多个线程更改核心数据对象而无需在每次更改后进行保存感到困惑。

我正在开发一个不断与服务器对话的应用程序。该应用程序使用 Core Data 进行存储,NSFetchedResultsController 在一些视图控制器中用于从持久存储中获取数据。通常当用户执行一个动作时,会触发一个网络请求。在发送网络请求之前,通常应该对相关的 Core Data 对象进行一些更改,而在服务器响应时,会对这些 Core Data 对象进行更多的更改。

最初所有的核心数据操作都是在同一个NSManagedObjectContext 的主线程上完成的。一切都很好,只是当网络流量很高时,应用程序可能会在几秒钟内变得无响应。显然这是不可接受的,所以我考虑将一些 Core Data 操作移动到后台运行。

我尝试的第一种方法是创建一个 NSOperation 对象来处理每个网络响应。在 NSOperation 对象的 main 方法中,我设置了一个专门的 MOC,进行一些更改,最后提交更改。

- (void)main
{
    @try {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

        // Create a dedicated MOC for this NSOperation
        NSManagedObjectContext * context = [[NSManagedObjectContext alloc] init];
        [context setPersistentStoreCoordinator:[APP_DELEGATE persistentStoreCoordinator]];

        // Make change to Core Data objects
        // ...

        // Commit the changes
        NSError *error = nil;
        if ([context hasChanges] && ![context save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        }

        // Release the MOC
        [context release];

        // Drain the pool
        [pool drain];
    }
    @catch (NSException *exception) {
        // Important that we don't rethrow exception here
        NSLog(@"Exception: %@", exception);
    }
}

主线程上的MOC注册为NSManagedObjectContextDidSaveNotification

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundContextDidSave:)
                                             name:NSManagedObjectContextDidSaveNotification
                                           object:nil];

所以当后台上下文提交更改时,会通知主 MOC,然后将更改合并:

- (void)backgroundContextDidSave:(NSNotification *)notification
{
    // Make sure we're on the main thread when updating the main context
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(backgroundContextDidSave:) withObject:notification waitUntilDone:NO];
        return;
    }

    // Merge the changes into the main context
    [[self managedObjectContext] mergeChangesFromContextDidSaveNotification:notification];
}

但是,正如我之前提到的,我还需要从主 MOC 对 Core Data 对象进行更改。每个更改通常非常小(例如更新对象中的一个实例变量),但可能有很多。所以我真的不想在每次更改后都保存主 MOC。但是,如果我不这样做,我会在将更改从后台 MOC 合并到主 MOC 时遇到问题。由于两个 MOC 都有未保存的更改,因此会发生合并冲突。设置合并策略也无济于事,因为我想保留两个 MOC 的更改。

一种可能性是也使用NSManagedObjectContextDidSaveNotification 注册后台MOC,但这种方法对我来说听起来像是糟糕的设计。而且我仍然需要在每次更改后保存主 MOC。

我尝试的第二种方法是从在永久后台线程上运行的专用后台上下文中进行所有核心数据更改。

- (NSThread *)backgroundThread
{
    if (backgroundThread_ == nil) {
        backgroundThread_ = [[NSThread alloc] initWithTarget:self selector:@selector(backgroundThreadMain) object:nil];
        // Actually start the thread
        [backgroundThread_ start];
    }
    return backgroundThread_;
}

// Entry point of the background thread
- (void)backgroundThreadMain
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    // We can't run the runloop unless it has an associated input source or a timer, so we'll just create a timer that will never fire.
    [NSTimer scheduledTimerWithTimeInterval:DBL_MAX target:self selector:@selector(ignore) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] run];

    // Create a dedicated NSManagedObjectContext for this thread.
    backgroundContext_ = [[NSManagedObjectContext alloc] init];
    [backgroundContext_ setPersistentStoreCoordinator:[self persistentStoreCoordinator]];

    [pool drain];
}

因此,每当我需要从主线程更改 Core Data 时,我必须从主线程获取 objectID,并传递给后台线程来执行更改。当后台上下文保存时,更改将被合并回主 MOC。

- (void)addProduct:(Product *)product toCatalog:(Catalog *)catalog;

将更改为:

- (void)addProduct:(NSManagedObjectID *)productObjectId toCatalog:(NSManagedObjectID *)catalogObjectId
{
    NSArray * params = [NSArray dictionaryWithObjects:productObjectId, catalogObjectId, nil];
    [self performSelector:(addProductToCatalogInBackground:) onThread:backgroundThread_ withObject:params waitUntilDone:NO];
}

但这看起来如此复杂和丑陋。编写这样的代码似乎首先否定了使用 Core Data 的用处。此外,我仍然需要在每次更改后保存 MOC,因为如果不先将新对象保存到数据存储区,我无法获取新对象的 objectId。

我觉得我在这里遗漏了一些东西。我真的希望有人能对此有所了解。谢谢。

【问题讨论】:

    标签: iphone multithreading core-data


    【解决方案1】:

    NSManagedObjectContext 只是一个便签本。保存它的行为会将一个便笺簿中的更改向下移动到NSPersistentStoreCoordinator,并可能向下移动到磁盘。一个 MOC 可以了解另一个 MOC 更改的唯一方法是通过NSPersistentStoreCoordinator。因此需要保存。但是,在 iOS 的下一个版本中,节省的成本要低很多。

    如果您必须符合 iOS4 或更低标准,那么保存是唯一的选择。但是,您可以根据应用程序的设计加载保存并减少执行频率。如果您正在导入数据,则在导入完成时或在导入内的逻辑单元中保存。每次输入都不需要保存,太浪费了。

    顺便说一句,我建议使用 NSOperation 实例而不是直接使用 NSThread 实例。它们更易于使用,并且性能更好。

    此外,您不需要将 Objective-C 代码包装在 try/catch 块中。很少有事情会抛出异常;尤其是在 iOS 上。

    最后,我建议查看my post on CIMGF 关于在后台线程上导入的信息。

    【讨论】:

    • 嗨 Marcus,我浏览了你的帖子并在我的应用程序中做了类似的方法,但有时我的 UI 在尝试合并更改时会在几秒钟内无响应,我不知道如何避免这种情况,因为合并主线程正在发生变化
    • 合并到主线程会导致主线程在合并过程中阻塞。避免这种情况的方法(iOS 5 之前)是保存在较小的块中,以便主线程有时间做其他事情。您希望保存时间短,以便人类无法察觉。
    • 是的......目前我的保存操作在 15 条记录的块中工作,超过 1000 条记录,我观察到 UI 无响应性有所改善,但仍然存在几秒钟,我想我会用更少的时间尝试它每个块的记录以检查它是否有所改善..
    • 太短了,你的频率会上升,你又会遇到同样的问题。这是一件很难处理的事情,这就是为什么它在 iOS4 中得到了解决。您可能会考虑的另一个选择是根本不进行合并。保存完成后只需重置主上下文并强制它自行刷新。并非适用于所有应用,但需要考虑。
    • 感谢您的提示..我可以通过每次合并保留 15 条记录来使其正常工作。但仍处于测试阶段,因为我需要跨多个操作系统版本进行验证。在我的情况下,重置也不起作用,因为用户在整个应用程序中访问数据库中的数据,并且在重置上下文时可能会导致 UI 无响应..
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多