【问题标题】:stringByAppendingString causing system to run out of memory for applicationsstringByAppendingString 导致系统内存不足的应用程序
【发布时间】:2013-12-03 14:30:27
【问题描述】:

当我在“大”(37000 行)文本文件上运行以下代码时,为什么我的系统告诉我应用程序的内存不足?

-(void) writeToFile: (NSString*)filePath withSeparator:(NSString*) fieldSep{
NSString* completeFile = [[[NSString alloc] initWithString:@""] autorelease];
for(int i=0;i<[self numberOfRows];i++){
    printf("im at line number... %i of %i\n",i,[self numberOfRows]);
    for(int j=0;j<[self numberOfColumns];j++){
        completeFile = [completeFile stringByAppendingString:[self objectInRow:i column:j]];

        if(j<[self numberOfColumns]-1){
            //separator for all columns except last one
            completeFile = [completeFile stringByAppendingString:fieldSep];
        }
    }
        completeFile = [completeFile stringByAppendingString:@"\n"];
}
NSError *error = nil;
[completeFile writeToFile:filePath atomically:NO
                 encoding:NSStringEncodingConversionAllowLossy error:&error];
if(error){
    NSLog(@"Error writing file at %@\n%@",
          filePath, [error localizedFailureReason]);
}

}

出于调试原因,我添加了 printf,前 4000 行似乎立即发生,然后慢慢变慢...我的文件包含超过 37000 行类似于这些:

1893-11-6   136 194 165

【问题讨论】:

  • 好吧,不是斯波尔斯基的例子; NSString 不像 strcat 那样工作。 :)
  • @StevenFisher:这里每次附加一个字符串时都会创建一个副本,而不是在 Spolsky 的 strcat 示例中线性搜索 0。但是原理是一样的——追加是O(n),所以构建字符串是O(n^2)。
  • 我应该更清楚。问题并不完全相同,但肯定是相同的原理在起作用。 :)
  • 感谢@RussellZahniser,我已经阅读了维基百科的文章,现在我感到很惭愧。哦,好吧,现在学习比编写大量代码更好! :)

标签: objective-c


【解决方案1】:

当您使用工厂方法分配对象时,这些对象会被添加到自动释放池中。仅当您的事件循环运行时,在您的 IBAction 返回之后,自动释放池才会被耗尽。

这里的诀窍是将循环的内容放在它自己的自动释放池中。

但让我们先解决最大的问题。您应该在此处使用一个 NSMutableString 类,这将大大减少您需要创建的对象数量。

我们将 completeFile 切换为 NSMutableString,使用工厂方法构造,然后附加到它:

-(void) writeToFile: (NSString*)filePath withSeparator:(NSString*) fieldSep{
    NSMutableString* completeFile = [NSMutableString string];
    for(int i=0;i<[self numberOfRows];i++){
        printf("im at line number... %i of %i\n",i,[self numberOfRows]);
        for(int j=0;j<[self numberOfColumns];j++){
            [completeFile appendString:[self objectInRow:i column:j]];

            if(j<[self numberOfColumns]-1){
                //separator for all columns except last one
                completeFile appendString:fieldSep];
            }
        }
            [completeFile appendString:@"\n"];
    }
    NSError *error = nil;
    [completeFile writeToFile:filePath atomically:NO
                     encoding:NSStringEncodingConversionAllowLossy error:&error];
    if(error){
        NSLog(@"Error writing file at %@\n%@",
              filePath, [error localizedFailureReason]);
    }
}

不过,这会带来另一个问题。看到[self objectInRow:i column:j]了吗?它仍然(可能)是一个自动释放的对象。这不会得到清理。

根据数据的大小,我们可能使您的代码运行时不会崩溃,但这是何时崩溃而不是如果的问题。

为了解决这个问题,我们需要引入自动释放池。让我们每行每列做一个。这可能看起来有点过分(事实上,在这种情况下,因为我们已经在外部循环中消除了 autoreleasepool 的使用),但 autoreleasepool 非常便宜。如果您要对大量数据进行循环,这是一种很好的做法。

您可以用 @autorelease 块替换每个 for 块,例如:

for(int i=0;i<[self numberOfRows];i++){

与:

for(int i=0;i<[self numberOfRows];i++) @autoreleasepool {

这给了我们这个代码:

-(void) writeToFile: (NSString*)filePath withSeparator:(NSString*) fieldSep{
    NSMutableString* completeFile = [NSMutableString string];
    for(int i=0;i<[self numberOfRows];i++) @autoreleasepool {
        printf("im at line number... %i of %i\n",i,[self numberOfRows]);
        for(int j=0;j<[self numberOfColumns];j++) @autoreleasepool {
            [completeFile appendString:[self objectInRow:i column:j]];

            if(j<[self numberOfColumns]-1){
                //separator for all columns except last one
                completeFile appendString:fieldSep];
            }
        }
            [completeFile appendString:@"\n"];
    }
    NSError *error = nil;
    [completeFile writeToFile:filePath atomically:NO
                     encoding:NSStringEncodingConversionAllowLossy error:&error];
    if(error){
        NSLog(@"Error writing file at %@\n%@",
              filePath, [error localizedFailureReason]);
    }
}

不过,最后一点。您在这里的错误检查是不安全的。 成功时这样传入的错误指针会发生什么情况没有定义。

    [completeFile writeToFile:filePath atomically:NO
                     encoding:NSStringEncodingConversionAllowLossy error:&error];
    if(error){
        NSLog(@"Error writing file at %@\n%@",
              filePath, [error localizedFailureReason]);
    }

相反,你想要这个:

    BOOL ok = [completeFile writeToFile:filePath atomically:NO
                     encoding:NSStringEncodingConversionAllowLossy error:&error];
    if(!ok){
        NSLog(@"Error writing file at %@\n%@",
              filePath, [error localizedFailureReason]);
    }

那么,这应该做你想做的。

【讨论】:

  • 此处未使用 ARC。看到对autorelease的调用了吗?
  • 当然。我认为也应该使用 NSMutableString。 :)
  • @LeoNatan 你误解了那篇文章。请参阅stackoverflow.com/questions/7825976/… 了解更多信息。
  • 一般来说,我认为假设您将获得objc_retainAutoreleasedReturnValue 优化(以及它所暗示的一切)是错误的。有很多东西可以把它扔掉。最好只是想清楚自己想做什么,然后用代码清楚地表达出来。
  • 当然,切换到可变字符串。但是你不能在这里完全避免自动释放池。这是何时会失败的问题,而不是if:例如,[self objectInRow:i column:j] 将继续返回一个自动关联的对象。最终,您将需要一个游泳池。
【解决方案2】:

问题是每次调用stringByAppendingString: 都会创建一个新的自动释放NSString 对象。但是,由于该方法在循环中继续,因此 autorelease 没有机会释放这些对象。

您可以通过在内部循环周围添加一个自动释放池来解决它,如下所示:

for(int i=0;i<[self numberOfRows];i++){
    printf("im at line number... %i of %i\n",i,[self numberOfRows]);
    @autoreleasepool {
        for(int j=0;j<[self numberOfColumns];j++){
            completeFile = [completeFile stringByAppendingString:[self objectInRow:i column:j]];

            if(j<[self numberOfColumns]-1){
                //separator for all columns except last one
                completeFile = [completeFile stringByAppendingString:fieldSep];
            }
        }
    }
        completeFile = [completeFile stringByAppendingString:@"\n"];
}

【讨论】:

  • 感谢您的回答!
【解决方案3】:

您应该使用NSMutableString 附加这些字符串。

【讨论】:

  • 这确实修复了它,我更改为 NSMutableString 并使用 completeFile appendString 而不是 stringByAppendingString,它几乎立即完成了所有 37000 行。谢谢大家的回复!
  • 是的。 NSMutableString 非常有效。我不确切知道调整大小的算法是什么,但它肯定比在每个追加上重新分配更聪明。 :)
猜你喜欢
  • 2011-04-29
  • 1970-01-01
  • 2013-06-15
  • 1970-01-01
  • 1970-01-01
  • 2012-07-03
  • 2018-12-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多