循环和便利方法是不是会导致 ARC 出现内存峰值?
Posted
技术标签:
【中文标题】循环和便利方法是不是会导致 ARC 出现内存峰值?【英文标题】:Do loops and convenience methods cause memory peaks with ARC?循环和便利方法是否会导致 ARC 出现内存峰值? 【发布时间】:2012-08-06 18:36:18 【问题描述】:我正在使用 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 中的循环方式,运行分配工具:
看到了吗?逐渐稳定的内存使用,直到一大堆内存警告,然后应用程序自然而然地死掉。
接下来,我尝试了一些不同的方法。我使用了NSMutableString
的一个实例,如下所示:
NSMutableString *text;
for (NSInteger i = 0; i < 600000000; i++)
if (text)
[text appendString:@" Hello"];
else
text = [@"" mutableCopy];
此代码似乎执行得更好,但仍然崩溃。看起来是这样的:
接下来,我在一个较小的数据集上进行了尝试,以查看任一循环是否能够在构建过程中存活足够长的时间以完成。这是NSString
版本:
NSString *text;
for (NSInteger i = 0; i < 1000000; i++)
if (text)
text = [text stringByAppendingString:@" Hello"];
else
text = @"";
它也崩溃了,生成的内存图看起来与使用以下代码生成的第一个相似:
使用NSMutableString
,同样的百万次迭代循环不仅成功,而且在更短的时间内完成。代码如下:
NSMutableString *text;
for (NSInteger i = 0; i < 1000000; i++)
if (text)
[text appendString:@" Hello"];
else
text = [@"" mutableCopy];
看看内存使用图:
一开始的短暂峰值是循环引起的内存使用。还记得我注意到在处理循环期间屏幕是黑色的看似无关的事实,因为我在 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:属性: 解析器:找到字符: 解析器:didStartElement:namespaceURI:qualifiedName: 解析器:didStartElement:namespaceURI:qualifiedName:属性: 解析器:找到字符: 解析器:didStartElement:namespaceURI:qualifiedName: 解析器:didStartElement:namespaceURI:qualifiedName:属性: 解析器:找到字符: 解析器:didStartElement:namespaceURI:qualifiedName: parserDidEndDocument:
看到模式了吗?这是一个循环。请注意,也可以对parser:foundCharacters:
进行多次连续调用,这就是我们将属性附加到先前值的原因。
总结一下,这里有两个问题。首先,任何类型的循环中积累的内存似乎都会使应用程序崩溃。其次,将NSMutableString
与属性一起使用并不那么优雅,我什至不确定它是否按预期工作。
一般来说,有没有办法在使用 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
可以自己处理大约一百万次迭代,而NSString
不能。
那么,解决这个问题的正确方法是什么?
【问题讨论】:
在每种情况下,您都在使用未初始化的局部变量。你没有收到编译器警告吗? @NicholasRiley 不,没有警告。 使用 NSString 技术,创建 newText 每次循环分配一个新的且越来越大的 NSString,然后自动释放旧的。追加到 NSMutableString 不会分配新的 NSString,所以没有什么可以释放的。它按预期工作。 在循环的第一次迭代中,在初始化之前,您使用了一次变量。我确实收到了编译器警告: bookworm% clang -Weverything -framework Foundation foo.m foo.m:10:11: 警告:在此处使用时变量“文本”可能未初始化 [-Wconditional-uninitialized] if (text) ^ ~~~ foo.m:5:24: 注意:初始化变量 'text' 来消除这个警告 NSMutableString *text; ^ = nil 生成 1 个警告。 为了更清楚 [text stringByAppendingString:@"Hello"] 每次都会创建一个新的 NSString。 [文本 appendString:@"Hello"] 没有。 【参考方案1】:您正在用大量自动释放的对象污染自动释放池。
用自动释放池包围循环的内部部分:
for (...)
@autoreleasepool
... your test code here ....
【讨论】:
初始测试表明,即使使用单独的自动释放池也无法阻止崩溃。对象必须去某处,即使它是第二个自动释放池,它们仍然存在,直到运行循环结束,当自动释放池可以耗尽时。就 NSXMLParser 而言,这并不能解决字符串情况下的任何问题,它可能需要测试。 实际上,这并不完全正确。 NSMutableString 和 NSString 之间存在行为差异。请查看我的编辑。 Hrm -- 好的 -- 我怀疑这是一个与微基准和自动释放池的实现相交的病态案例。在生产代码中附加无数这样的字符串将是非常不典型的。并不是说爆炸是正确的行为,而只是你遇到了极端情况。让我看看源代码。 看起来这有助于解决我的记忆问题,但行为差异可能是也可能不是附带问题。 尝试在 Instruments 中开启“仅跟踪实时分配”。我刚刚在 OS X 和 ios 模拟器(但不是设备)上进行了测试,无法重现永久分配内存的增长;也没有高峰。【参考方案2】:在寻找与内存相关的错误时,您应该注意@"" 和@"Hello" 将是不朽的对象。您可以将其视为 const,但对于对象。在整个内存中,该对象的实例将只有一个,而且只有一个。
正如@bbum 指出的那样,并且您已验证,@autoreleasepool 是循环处理此问题的正确方法。
在您使用 @autoreleasepool 和 NSMutableString 的示例中,池并没有真正做太多事情。循环内唯一的致命对象是你的 @"" 的 mutableCopy,但它只会被使用一次。另一种情况只是一个 objc_msgSend 到一个持久对象(NSMutableString),它只引用一个不朽的对象和一个选择器。
我只能假设内存建立在 Apple 的 NSMutableString 实现内部,但我想知道为什么你会在 @autoreleasepool 中看到它,而不是在它不存在时看到它。
【讨论】:
我也想知道。不过,我无法重现相同的内存增长。 @bbum - 我添加了我的demo project to GitHub。你可以下载试试看。以上是关于循环和便利方法是不是会导致 ARC 出现内存峰值?的主要内容,如果未能解决你的问题,请参考以下文章