循环和便利方法是不是会导致 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 出现内存峰值?的主要内容,如果未能解决你的问题,请参考以下文章

ios之block循环引用

ARC 占用大量内存

iOS 内存管理

循环内的 Objective-C 循环和 @autoreleasepool 的 ARC 内存问题

objective-c启用ARC时的内存管理 (循环引用)

block-循环引用