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

Mos*_*she 13 memory-management objective-c instruments nsstring automatic-ref-counting

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

在我的情况下,我使用NSXMLParser委托回调循环,但我看到使用演示项目和简单修改某些NSString对象的示例代码的相同的确切行为和症状.

您可以从GitHub下载演示项目,只需在主视图控制器的viewDidLoad方法中取消注释四个方法调用之一,即可测试不同的行为.

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

NSString *text;

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

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

    if (text) {
        text = newText;
    }else{
        text = @"";
    }
}
Run Code Online (Sandbox Code Playgroud)

以下代码在循环完成之前还会保留内存:

NSString *text;

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

    if (text) {
        text = [text stringByAppendingString:@" Hello"];
    }else{
        text = @"";
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是这两个循环在Instruments中循环的方式,运行Allocations工具:

仪器分析重复字符串操作

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

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

NSMutableString *text;

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

    if (text) {
        [text appendString:@" Hello"];
    }else{
        text = [@"" mutableCopy];
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码似乎表现得更好,但仍然崩溃.这是看起来像:

NSMutableStrings是配置文件

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

NSString *text;

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

    if (text) {
        text = [text stringByAppendingString:@" Hello"];
    }else{
        text = @"";
    }
}
Run Code Online (Sandbox Code Playgroud)

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

NSString再次崩溃

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

NSMutableString *text;

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

    if (text) {
        [text appendString:@" Hello"];
    }else{
        text = [@"" mutableCopy];
    }
}
Run Code Online (Sandbox Code Playgroud)

并查看内存使用情况图:

NSMutableStrings似乎适用于较小的数据集

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

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

<person>
<firstname>John</firstname>
<lastname>Doe</lastname>
</person>
Run Code Online (Sandbox Code Playgroud)

我的对象看起来像这样:

@interface Person : NSObject

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

@end
Run Code Online (Sandbox Code Playgroud)

现在,在我的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]; 
  }
}
Run Code Online (Sandbox Code Playgroud)

这段代码与第一段代码非常相似.我正在使用XML实际循环NSXMLParser,所以如果我要记录所有的方法调用,我会看到这样的事情:

parserDidStartDocument:parser:didStartElement:namespaceURI:qualifiedName:attributes:parser:foundCharacters:parser:didStartElement:namespaceURI:qualifiedName:parser:didStartElement:namespaceURI:qualifiedName:attributes:parser:foundCharacters:parser:didStartElement:namespaceURI:qualifiedName:parser:didStartElement: namespaceURI:qualifiedName:attributes:parser:foundCharacters:parser: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];
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

分配跟踪如下所示:

在此输入图像描述

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

NSMutableString *text;

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

        @autoreleasepool {
            if (text) {
                [text appendString:@" Hello"];
            }else{
                text = [@"" mutableCopy];
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

分配跟踪:

NSMutableString显然不受自动释放池的影响

看起来NSMutableString显然不受自动释放池的影响.我不确定为什么,但是最初猜测,我将这与我们之前看到的相结合,它NSMutableString可以自己处理大约一百万次迭代,而NSString不能.

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

bbu*_*bum 11

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

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

for (...) {
    @autoreleasepool {
        ... your test code here ....
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 尝试在乐器中启用"仅跟踪实时分配".我刚刚测试了OS X和iOS模拟器(但不是设备),并且无法重现永久分配内存的增长; 也没有高峰. (3认同)