高效地进行大规模和连续的Core Data导入

3
问题: 当要插入到Core Data的记录数量不可预见时,如何释放NSManagedObjectContext(我猜是)使用的内存,以便可以有效地使用内存?
这是我的情况: 我有一个蓝牙设备,每0.00125秒(最小间隔,最大情况将是0.002秒)会连续向iOS设备发送十二组整数,然后我应该将这些整数与时间戳一起存储到CoreData中。
数据对象和关联: 当流程开始时,创建一个头记录(NSManagedObject),作为从蓝牙设备检索接收到的数据批次的关键。在整个数据接收期间,该对象作为强属性保留,并在流程结束时从属性中删除(可能设置为nil)。
NSManagedObjectContext的设计: 所有三个ManagedObjectContext都是AppDelegate中存储的单例对象。
以下是创建ManagedObjectContext的代码:
- (NSManagedObjectContext *)masterManagedObjectContext {
    // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
    if (_masterManagedObjectContext != nil) {
        return _masterManagedObjectContext;
    }
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (!coordinator) {
        return nil;
    }
    _masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [_masterManagedObjectContext setPersistentStoreCoordinator:coordinator];
    [_masterManagedObjectContext setUndoManager:nil];
    return _masterManagedObjectContext;
}
-(NSManagedObjectContext*) backgroundManagedObjectContext{
    if(_backgroundManagedObjectContext != nil){
        return _backgroundManagedObjectContext;
    }
    _backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
    [_backgroundManagedObjectContext setUndoManager:nil];
    [_backgroundManagedObjectContext setParentContext:[self masterManagedObjectContext]];
    return _backgroundManagedObjectContext;
}
-(NSManagedObjectContext*) mainManagedObjectContext{
    if(_mainManagedObjectContext !=nil){
        return _mainManagedObjectContext;
    }
    _mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [_mainManagedObjectContext setUndoManager:nil];
    [_mainManagedObjectContext setParentContext:[self masterManagedObjectContext]];
    return _mainManagedObjectContext;
}

导入过程在backgroundManagedObjectContext中处理。使用以下代码创建并存储Header:

_header = [NSEntityDescription insertNewObjectForEntityForName:@"Header" inManagedObjectContext:_backgroundManagedObjectContext];
_header.startTime = [NSDate date];
NSError *error;
BOOL success = [_backgroundManagedObjectContext save:&error];

当蓝牙设备触发该方法时,使用以下代码创建和存储接收到的数据:

@autoreleasepool {    
    ReceivedData* data = [NSEntityDescription insertNewObjectForEntityForName:@"ReceivedData" inManagedObjectContext:_backgroundManagedObjectContext];
    //Data is set here
    [_header addFk_header_many_dataObject:data];
    currentCount ++;
    if(currentCount >=1000){
        currentCount = 0;
        NSError *error;
        BOOL success = [_backgroundManagedObjectContext save:&error];
    }
}

接收到的数据将会按照每1000个数据存储到 managedObjectContext 中。

如果我停止这个进程,所占用的内存将会增加一倍,并且持续到我完全终止应用程序。

处理进程结束的代码如下:

_header.endTime = [NSDate date];
_header = nil;
NSError *error;
BOOL success = [_backgroundManagedObjectContext save:&error];
[_masterManagedObjectContext performBlock:^{
   NSError* mastererror;
   BOOL mastersuccess = [_masterManagedObjectContext save:&mastererror];
}];

问题:

正如苹果的 Core Data Performance 所述,使用 NSManagedObjectContext 的 reset 方法将删除与上下文相关的所有托管对象,重新开始就像您刚创建它一样。

据我理解,这意味着我只能在整个进程结束时调用此方法。我试过在 _backgroundManagedObjectContext 和 _masterManagedObjectContext 保存后添加重置功能。但是,内存仍然没有改变。

内存使用说明:

对于每0.002秒接收到的数据,将保存1000条记录到背景管理上下文中增加0.5MB内存。因此,应用程序在8分钟的处理时间内将消耗大约150 MB的内存,并且当进程在该时间终止时,内存将增加到320MB,并保持内存使用量为220MB左右。

问题:当要插入到 Core Data 中的记录数不可预见时,如何释放 NSManagedObjectContext(我猜测)使用的内存,以便有效地使用内存?

很抱歉,由于我很新手 iOS,可能会有一些错误。在发布问题之前,我已经尽力搜索了。

非常感谢您的帮助。

备注:

我在不超过10分钟的处理时间内尝试了上述情况。然而,这种实现应该已经扩展到超过1小时的处理时间。我仍然不知道如何处理这种情况。

编辑1: 修改了代码以显示ReceivedData和Header之间的关系。编辑2: 根据 @flexaddicted 提供的标准更新了代码。


首先要做的是更改后台MOC的工作方式。在您的情况下,它应该直接连接到persistentStoreCoordinator而不是父主MOC,因为这会导致不必要的内存使用 - 对于您来说,所有数据都应直接进入存储(sqlite),最终UI应该被通知有关更改和更新需求。在您的代码中,当您保存后台MOC时,所有更改都需要移动到主MOC,并且在后台MOC上执行重置将重置(可能)已经干净的MOC,并且所有内存仍由主MOC使用。您没有写明在哪个MOC上调用重置。 - thom_ek
感谢您的评论。对于第一个评论,我之前尝试过直接连接到persistentStoreCoordinator,但性能不尽如人意。在问题部分中,我也记录了我调用reset的MOC。实际上,我两种方法都尝试了。 - Frank Fung
你是否已经尝试在 if(currentCount >=1000){ 检查内,在 save 调用之后,调用上下文的重置? - Lorenzo B
不会,在那个时候调用重置函数会使头对象失效吗?让我试一下,测试后再回复。 - Frank Fung
在这种情况下,是的。请看我的回答。希望能有所帮助。 - Lorenzo B
你能否展示一下在哪里设置了ReceiveDataHeader之间的关系? - Lorenzo B
1个回答

0

这只是我的建议,也许有人会有不同的方法。

在这种情况下,我会消除BackgroundManagedObjectContext,只留下MasterManagedObjectContext(作为主要上级)。由于您需要低内存配置文件,因此应切换到允许您控制应用程序内存占用的机制。因此,我会创建一种缓冲区,开始收集接收数据。当缓冲区达到其限制时,我会将接收数据移动到MasterManagedObjectContext中,以便将它们保存到持久存储中。在这里,缓冲区的限制(在我看来是结构体向量或对象数组)应该根据应用程序性能进行调整。通过这种方式,您可以直接控制创建的对象。因此,每当您完成一批导入数据时(其中一批是该向量/数组的限制),您可以将它们丢弃。

否则,您可以尝试以下方法。

@autoreleasepool {    

    NSMutableArray *temporary = [NSMutableArray array];

    ReceivedData* data = [NSEntityDescription insertNewObjectForEntityForName:@"ReceivedData" inManagedObjectContext:_backgroundManagedObjectContext];
    // Data is set here

    // Let temporary to hold a reference of the data object 
    [temporary addObject:data];
    currentCount ++;
    if(currentCount >=1000){
        currentCount = 0;
        NSError *error;
        BOOL success = [_backgroundManagedObjectContext save:&error];

        for(NSManagedObject *object in temporary) {
            [_backgroundManagedObjectContext refreshObject:object mergeChanges:NO];
        }
        [temporary removeAllObjects];
    }
}

更新 1

您能否展示一下您在哪里设置了ReceiveDataHeader之间的关系?我问这个是因为您可以更改设置这两个实体之间关系的时间。

根据您修改后的代码。

@autoreleasepool {    
    receivedData* data = [NSEntityDescription insertNewObjectForEntityForName:@"ReceivedData" inManagedObjectContext:_backgroundManagedObjectContext];
    //Data is set here

    [_header addFk_header_many_dataObject:data];
    currentCount ++;
    if(currentCount >=1000){
        currentCount = 0;
        NSError *error;
        BOOL success = [_backgroundManagedObjectContext save:&error];
    }
}

如果您能够将此关联推迟到主队列上(我猜您需要将属性设置为可选项),则可以按照以下方式操作:
@autoreleasepool {    
    ReceivedData* data = [NSEntityDescription insertNewObjectForEntityForName:@"ReceivedData" inManagedObjectContext:_backgroundManagedObjectContext];
    // Data is set here

    // Move it later
    //[_header addFk_header_many_dataObject:data];

    currentCount ++;
    if(currentCount >=1000){
        currentCount = 0;
        NSError *error;
        BOOL success = [_backgroundManagedObjectContext save:&error];
        [_backgroundManagedObjectContext reset];
    }
}

附言:receiveData *data = ... 应该改为 ReceiveData *data = ...。换句话说,类名应该以大写字母开头。


谢谢,你的方法似乎和@thom-ek一样。我需要一些时间来尝试这种方法。 - Frank Fung
谢谢您的更新。我正在寻找一种方法,可以推迟在互联网上设置关系。 - Frank Fung
经过一些搜索,我可以将“在主队列上推迟此关联”解释为在处理过程中创建两个独立的对象,并在处理过程终止时设置它们之间的关系。 - Frank Fung
当然可以。在您的后台上下文中创建接收到的数据,并在主机头中添加@FrankFung。 - Lorenzo B
好的,我现在明白你的意思了。感谢你的帮助。所有这些想法都应该清楚了。实际上,对于我来说,我可以采用另一种方法处理数据,因为标题同时具有开始时间和结束时间。然后我可以获取时间范围内的数据。虽然这不是最好的方法,即我完全打破了关系。 - Frank Fung

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接