高效持续地向核心数据导入海量数据

Extremely Massive and Continuous Import to Core Data Efficiently

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

这是我的案例: 我有一个蓝牙设备,每 0.00125 秒(最小间隔,最大情况为 0.002 秒)连续向 iOS 设备发送十二组整数,然后我应该将这些整数与时间戳一起存储到 CoreData 中。

数据Objects和关联:

进程启动时,将创建一个 header 记录(NSManagedObject)作为键,以检索从蓝牙设备接收到的批量数据。 object在整个数据接收期间保持强属性,并在过程结束时从属性(可能设置为nil)中删除。

NSManagedObjectContext 的设计:

三个 ManagedObjectContext 都是单例 object 存储在 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 by Apple 所述, 使用 NSManagedObjectContext 的重置方法将删除所有与上下文关联的托管 objects,并且 "start over" 就像您刚刚创建它一样.

以我的理解,这意味着我只能在整个过程结束时调用此方法。我试图在保存 _backgroundManagedObjectContext 和 _masterManagedObjectContext 之后添加重置功能。但是,内存保持不变。

内存使用说明 对于每 0.002 秒接收一次数据的情况,每 1000 条记录增加 0.5MB 内存保存到 backgroundManagedObjectContext。因此,该应用程序将在 8 分钟的处理时间内消耗大约 150 MB,当进程终止时内存将增加到 320MB,并将保留大约 220MB 的内存使用量。

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

对于一些愚蠢的错误,我很抱歉,因为我在 iOS 中还很陌生。在发布问题之前,我已经尽力搜索了。

感谢您的帮助。 非常感谢。

备注 我已经在不超过 10 分钟的处理时间内尝试了上述案例。但是,实施应该已经延长到 1 小时以上处理时间的情况。我还不知道如何处理这种情况。

编辑 1 修改了显示 ReceivedData 和 Header 关系的代码 编辑 2 更新了@flexaddicted

提到的标准的代码

只是我的建议。也许有人可以有不同的方法。

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

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

@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];
    }
}

P.S。 receiveData *data = ... 应该是 ReceiveData *data = ...。换句话说,类 应该以大写字母开头。