Objective c 循环和方便的方法是否会导致ARC内存峰值?

Objective c 循环和方便的方法是否会导致ARC内存峰值?,objective-c,memory-management,nsstring,automatic-ref-counting,instruments,Objective C,Memory Management,Nsstring,Automatic Ref Counting,Instruments,我正在使用ARC,在修改循环中的字符串时看到一些奇怪的行为 在我的情况下,我使用NSXMLParser委托回调循环,但是我使用一个演示项目和示例代码看到了相同的行为和症状,该示例代码只是修改了一些NSString对象 您只需取消注释主视图控制器的viewDidLoad方法中的四个方法调用之一,即可测试不同的行为 为了简单起见,这里有一个简单的循环,我将它固定在一个空的单视图应用程序中。我将此代码直接粘贴到viewDidLoad方法中。它在视图出现之前运行,因此在循环结束之前屏幕是黑色的 NSSt

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

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

您只需取消注释主视图控制器的
viewDidLoad
方法中的四个方法调用之一,即可测试不同的行为

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

NSString *text;

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

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

    if (text) {
        text = newText;
    }else{
        text = @"";
    }
}
这段代码的性能似乎要好得多,但仍然会崩溃。下面是它的样子:

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

NSString *text;

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

    if (text) {
        text = [text stringByAppendingString:@" Hello"];
    }else{
        text = @"";
    }
}
并查看内存使用率图表:

开始时的短尖峰是循环产生的内存使用量。还记得我在viewDidLoad中运行时注意到一个看似无关的事实,即在循环处理过程中屏幕是黑色的吗?在该峰值之后,视图立即出现。因此,在这种情况下,NSMutableString不仅可以更有效地处理内存,而且速度也更快。迷人

现在,回到我的实际场景。。。我正在使用
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
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
  if((currentProperty is EqualToString:@"firstname"]){
    self.workingPerson.firstname = [self.workingPerson.firstname stringByAppendingString:string]; 
  }
}
现在,在我的NSXMLParser委托中,我将继续循环使用我的XML,并跟踪当前元素(我不需要完整的层次结构表示,因为我的数据非常扁平,它是MSSQL数据库作为XML的转储),然后在
foundCharacters
方法中,我将运行如下内容:

<person>
<firstname>John</firstname>
<lastname>Doe</lastname>
</person>
@interface Person : NSObject

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

@end
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
  if((currentProperty is EqualToString:@"firstname"]){
    self.workingPerson.firstname = [self.workingPerson.firstname stringByAppendingString:string]; 
  }
}
此代码与第一个代码非常相似。我正在使用
NSXMLParser
有效地循环XML,因此如果我要记录所有方法调用,我会看到如下内容:

<person>
<firstname>John</firstname>
<lastname>Doe</lastname>
</person>
@interface Person : NSObject

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

@end
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
  if((currentProperty is EqualToString:@"firstname"]){
    self.workingPerson.firstname = [self.workingPerson.firstname stringByAppendingString:string]; 
  }
}
parserDidStartDocument: 解析器:didStartElement:namespaceURI:qualifiedName:attributes: 解析器:查找字符: 解析器:didStartElement:namespaceURI:qualifiedName: 解析器:didStartElement:namespaceURI:qualifiedName:attributes: 解析器:查找字符: 解析器:didStartElement:namespaceURI:qualifiedName: 解析器:didStartElement:namespaceURI:qualifiedName:attributes: 解析器:查找字符: 解析器:didStartElement:namespaceURI:qualifiedName: parserDidEndDocument:

看到模式了吗?这是一个循环。请注意,还可以对
解析器: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];
            }
        }
    }
以及分配跟踪:

NSMutableString似乎对自动释放池免疫。我不知道为什么,但乍一看,我会把这与我们之前看到的联系起来,
NSMutableString
可以自己处理大约一百万次迭代,而
NSString
不能


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

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

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

NSMutableString *text;

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

        @autoreleasepool {
            if (text) {
                [text appendString:@" Hello"];
            }else{
                text = [@"" mutableCopy];
            }
        }
    }
for (...) {
    @autoreleasepool {
        ... your test code here ....
    }
}

在查找与内存相关的bug时,应该注意,@“”和@“Hello”将是不朽对象。您可以将其视为常量,但对象除外。在整个过程中,内存中将有一个,而且只有一个,该对象的实例

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

在您使用@autoreleasepool和NSMutableString的示例中,该池实际上没有做多少工作。循环中唯一的致命对象是@“”的可变副本,但它只会使用一次。另一种情况只是一个指向持久对象(NSMutableString)的objc_msgSend,它只引用一个不朽对象和一个选择器


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

初始测试表明,即使使用单独的autorelease pool也不能阻止崩溃。对象必须去某个地方,即使它是第二个自动释放池,它们仍然在那里,直到运行循环结束,此时自动释放池可以耗尽。