objective-c - 循环和便利方法是否会导致 ARC 出现内存峰值?

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

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

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

您可以 download the demo project from GitHub , 只需在主 View Controller 的 viewDidLoad 中取消注释四个方法调用之一测试不同行为的方法。

为简单起见,这里有一个简单的循环,我将其放入一个空的单 View 应用程序中。我将此代码直接粘贴到viewDidLoad方法。它在 View 出现之前运行,因此在循环完成之前屏幕是黑色的。

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 工具正在运行:

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 中运行它吗?在那个尖峰之后, View 立即出现。因此,在这种情况下,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 委托(delegate)中,我将继续遍历我的 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: 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 with properties 不是那么优雅,我什至不确定它是否按预期工作。

一般来说,有没有办法在使用 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];
            }
        }
    }

分配跟踪如下所示:

enter image description here

我确实注意到内存随着时间的推移逐渐增加,但大约有 150 KB,而不是之前看到的 350 MB。但是,此代码使用 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不能。

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

最佳答案

您正在用成吨的自动释放对象污染自动释放池。

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

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

关于objective-c - 循环和便利方法是否会导致 ARC 出现内存峰值?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11885053/

相关文章:

ios - iOS11中的导航栏问题

c - 系统如何捕获通过 `asm: MOV` 到/从内存映射文件的内存的访问?

node.js - Nodejs 进程是否可以使用比可用物理内存更多的内存(通过使用交换内存)?

objective-c - 包含十六进制的 NSString 转换为等效的 ascii

iphone - 将观察者添加到每个 View 或动态呈现 View

iphone - 为什么没有在标准 Xcode 模板中导入 Foundation.h?

ios - sortedArrayUsingComparators 不按升序对数字进行排序

ios - 获取 NSLocalizedString 中参数的索引

ios - 带有 UINavigationController 的 UISplitViewController 作为详细信息 View : push controller via Storyboard segue

memory-management - 页表: Page fault