13

我正在使用 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 = @"";
    }
}

以下代码也一直在消耗内存,直到循环完成:

NSString *text;

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

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

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

分析重复字符串操作的仪器

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

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

NSMutableString *text;

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

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

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

NSMutableStrings 是配置文件

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

NSString *text;

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

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

它也崩溃了,生成的内存图看起来类似于使用以下代码生成的第一个:

NSString 再次崩溃

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

NSMutableString *text;

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

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

并查看内存使用图:

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

一开始的短暂峰值是循环引起的内存使用量。还记得我注意到在处理循环期间屏幕是黑色的看似无关的事实,因为我在 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]; 
  }
}

这段代码很像第一个代码。我正在使用 有效地循环 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:,这就是我们将属性附加到先前值的原因。

总结一下,这里有两个问题。首先,任何类型的循环中积累的内存似乎都会使应用程序崩溃。其次,使用NSMutableStringwith 属性并不是那么优雅,我什至不确定它是否按预期工作。

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

编辑:

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

当对象存在时,对象必须在内存中的某个位置,并且它们仍然存在,直到运行循环结束,此时自动释放池可以耗尽。

就 NSXMLParser 而言,这并不能解决字符串情况中的任何问题,它可能是因为循环分布在方法调用中 - 需要进一步测试。

(请注意,我称之为内存峰值,因为理论上,ARC 会在某个时候清理内存,直到达到峰值之后。实际上没有任何泄漏,但它具有相同的效果。)

编辑2:

将自动释放池粘贴在循环中会产生一些有趣的效果。NSString附加到对象时似乎几乎可以减轻堆积:

NSString *text;

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

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

分配跟踪如下所示:

在此处输入图像描述

我确实注意到随着时间的推移内存逐渐增加,但大约是 150 KB,而不是之前看到的 350 MB。但是,这段代码使用NSMutableString的行为与没有自动释放池的行为相同:

NSMutableString *text;

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

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

分配跟踪:

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

看起来 NSMutableString 显然不受自动释放池的影响。我不知道为什么,但乍一看,我会把它与我们之前看到的联系起来,它NSMutableString可以自己处理大约一百万次迭代,而NSString不能。

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

4

2 回答 2

11

您正在用大量自动释放的对象污染自动释放池。

用自动释放池包围循环的内部部分:

for (...) {
    @autoreleasepool {
        ... your test code here ....
    }
}
于 2012-08-09T13:57:51.137 回答
1

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

正如@bbum 指出的那样,并且您已验证,@autoreleasepool 是循环处理此问题的正确方法。

在您使用 @autoreleasepool 和 NSMutableString 的示例中,池并没有真正做太多事情。循环内唯一的致命对象是你的 @"" 的 mutableCopy,但它只会被使用一次。另一种情况只是一个 objc_msgSend 到一个持久对象(NSMutableString),它只引用一个不朽对象和一个选择器。

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

于 2012-08-09T16:25:23.670 回答