循环和便利方法会导致ARC的内存峰值吗?

时间:2012-08-09 13:54:50

标签: objective-c memory-management nsstring automatic-ref-counting instruments

我正在使用ARC并在循环中修改字符串时看到一些奇怪的行为。

在我的情况下,我使用NSXMLParser委托回调进行循环,但是我使用演示项目和示例代码看到了相同的确切行为和症状,这些代码只修改了一些NSString个对象。

您可以download the demo project from GitHub,取消注释主视图控制器的viewDidLoad方法中的四个方法调用之一,以测试不同的行为。

为简单起见,这是一个简单的循环,我已经陷入空单视图应用程序。我将此代码直接粘贴到viewDidLoad方法中。它在视图出现之前运行,因此屏幕为黑色,直到循环结束。

NSString *text;

for (NSInteger i = 0; i < 600000000; i++) {

    NSString *newText = [text stringByAppendingString:@" Hello"];

    if (text) {
        text = newText;
    }else{
        text = @"";
    }
}

以下代码在循环完成之前也会继续占用内存:

NSString *text;

for (NSInteger i = 0; i < 600000000; i++) {

    if (text) {
        text = [text stringByAppendingString:@" Hello"];
    }else{
        text = @"";
    }
}

这里有两个循环在工具中循环,分配工具在运行:

Instruments profiling repeating string manipulation

请参阅?逐渐稳定的内存使用,直到一大堆内存警告,然后应用程序自然死亡。

接下来,我尝试了一些不同的东西。我使用NSMutableString的实例,如下所示:

NSMutableString *text;

for (NSInteger i = 0; i < 600000000; i++) {

    if (text) {
        [text appendString:@" Hello"];
    }else{
        text = [@"" mutableCopy];
    }
}

此代码似乎执行得更好,但仍然崩溃。这是什么样的:

NSMutableStrings being profiles instead

接下来,我在一个较小的数据集上尝试了这个,看看是否有一个循环可以在构建期间存活足够长的时间来完成。这是NSString版本:

NSString *text;

for (NSInteger i = 0; i < 1000000; i++) {

    if (text) {
        text = [text stringByAppendingString:@" Hello"];
    }else{
        text = @"";
    }
}

它也会崩溃,结果内存图看起来与使用此代码生成的第一个相似:

NSString crashes again

使用NSMutableString,相同的百万次迭代循环不仅可以成功,而且可以在更短的时间内完成。这是代码:

NSMutableString *text;

for (NSInteger i = 0; i < 1000000; i++) {

    if (text) {
        [text appendString:@" Hello"];
    }else{
        text = [@"" mutableCopy];
    }
}

并查看内存使用情况图表:

NSMutableStrings seem to work with smaller datasets

开头的短峰值是循环引起的内存使用量。还记得当我注意到在处理循环期间屏幕是黑色的看似无关的事实,因为我在viewDidLoad中运行它吗?在该峰值之后,立即出现视图。所以看起来不仅NSMutableStrings在这种情况下更有效地处理内存,而且它们也更快。迷人。

现在,回到我的实际情况......我使用NSXMLParser来解析API调用的结果。我创建了Objective-C对象以匹配我的XML响应结构。因此,例如,考虑一个类似于此的XML响应:

<person>
<firstname>John</firstname>
<lastname>Doe</lastname>
</person>

我的对象看起来像这样:

@interface Person : NSObject

@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;

@end

现在,在我的NSXMLParser委托中,我将继续循环浏览XML,并且我会跟踪当前元素(因为我的数据,我不需要完整的层次结构表示相当平坦,它是一个MSSQL数据库转储为XML)然后在foundCharacters方法中,我运行这样的事情:

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
  if((currentProperty is EqualToString:@"firstname"]){
    self.workingPerson.firstname = [self.workingPerson.firstname stringByAppendingString:string]; 
  }
}

此代码与第一个代码非常相似。我使用NSXMLParser有效地循环遍历XML,所以如果我要记录所有方法调用,我会看到类似的内容:

  

parserDidStartDocument:   解析:didStartElement:的namespaceURI:qualifiedName中:属性:   解析:foundCharacters:   解析:didStartElement:的namespaceURI:qualifiedName中:   解析:didStartElement:的namespaceURI:qualifiedName中:属性:   解析:foundCharacters:   解析:didStartElement:的namespaceURI:qualifiedName中:   解析:didStartElement:的namespaceURI:qualifiedName中:属性:   解析:foundCharacters:   解析:didStartElement:的namespaceURI:qualifiedName中:   parserDidEndDocument:

看模式?这是一个循环。请注意,也可以连续多次调用parser:foundCharacters:,这就是我们将属性附加到以前的值的原因。

为了把它包起来,这里有两个问题。首先,在任何类型的循环中建立内存似乎会使应用程序崩溃。其次,使用带有属性的NSMutableString并不是那么优雅,我甚至不确定它是否按预期工作。

一般来说,有没有办法在使用ARC循环字符串时克服这种内存累积?我可以做一些特定于NSXMLParser的东西吗?

修改

初步测试表明即使使用第二个@autoreleasepool{...}似乎也没有解决问题。

当存在时,对象必须在内存中某处,并且它们仍然存在,直到runloop结束,此时自动释放池可以耗尽。

就NSXMLParser而言,这并没有解决字符串情况中的任何问题,因为循环遍布方法调用 - 需要进一步测试。

(注意我称之为记忆峰值,因为从理论上讲,ARC会在某个时刻清理记忆,直到它达到峰值为止。没有任何东西实际上是泄漏,但它具有相同的效果。 )

编辑2:

在循环内部粘贴自动释放池有一些有趣的效果。在附加到NSString对象时,它似乎几乎可以缓解累积:

NSString *text;

for (NSInteger i = 0; i < 600000000; i++) {

        @autoreleasepool {
            if (text) {
                text = [text stringByAppendingString:@" Hello"];
            }else{
                text = [@"" mutableCopy];
            }
        }
    }

分配跟踪如下所示:

enter image description here

我确实注意到随着时间的推移逐渐积累了内存,但它大约是150千字节,而不是之前看到的350兆字节。但是,使用NSMutableString的此代码与没有自动释放池时的行为相同:

NSMutableString *text;

for (NSInteger i = 0; i < 600000000; i++) {

        @autoreleasepool {
            if (text) {
                [text appendString:@" Hello"];
            }else{
                text = [@"" mutableCopy];
            }
        }
    }

分配跟踪:

NSMutableString is apparently immune to the autorelease pool

看起来NSMutableString显然不受自动释放池的影响。我不确定原因,但首先猜测,我将此与我们之前看到的结合起来,NSMutableString可以自己处理大约一百万次迭代,而NSString不能。

那么,解决这个问题的正确方法是什么?

2 个答案:

答案 0 :(得分:11)

您正在使用数吨和大量自动释放的对象来污染自动释放池。

使用自动释放池环绕循环的内部部分:

for (...) {
    @autoreleasepool {
        ... your test code here ....
    }
}

答案 1 :(得分:1)

当您在寻找与内存相关的错误时,您应该注意@“”和@“Hello”将是不朽的对象。您可以将其视为const,但对于对象。整个时间内,这个对象的实例只有一个,而且只有一个。

正如@bbum指出的那样,你验证了,@ autoreleasepool是在循环中处理这个问题的正确方法。

在使用@autoreleasepool和NSMutableString的示例中,池实际上没有做太多。循环中唯一的凡人对象是@“”的mutableCopy,但只能使用一次。另一种情况只是对持久化对象(NSMutableString)的objc_msgSend,它只引用一个永久对象和一个选择器。

我只能假设内存构建在Apple的NSMutableString实现中,尽管我可以想知道为什么你会在@autoreleasepool中看到它,而不是在它缺席的时候。