使用块,集合和ARC时泄漏

时间:2013-02-05 09:33:41

标签: ios objective-c automatic-ref-counting

我正在尝试使用ARC来理解为什么此代码泄漏:

- (IBAction)block2:(id)sender {
    NSMutableString *aString = [[NSMutableString alloc] init];

    void (^aBlock)() = ^{
        NSMutableString __unused *anotherString = aString;
    };

    NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObject:aBlock forKey:@"Key"];

}

正如你所看到的,我在一个集合中放了一个块(NSMutableDictionary,但是如果我使用NSDictionary,NSArray ecc就是一样的话),那么该方法返回并且字典被释放。然后应该释放该块。但是,使用仪器,我看到了泄漏

leaks

leaks2

leaks3

“只是为了确定”该块没有其他引用,我在方法的末尾添加了这一行:

[dict setObject:[NSNull null] forKey:@"Key"];

同样的结果。

我发现了这篇文章,但答案指出了另一个问题: Blocks inside NSMutableArray leaking (ARC)

然后,这是魔术: 如果我改变这一行:

NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObject:aBlock forKey:@"Key"];

为:

NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObject:[aBlock copy] forKey:@"Key"];
泄漏消失了。

我知道,在非ARC之下,在传递块文字的引用之前,我必须复制它(当声明为文字时,它在堆栈上,所以我需要将它复制到堆中,然后才能超出范围声明的函数)...但是使用ARC我不应该关心它。 任何迹象? 这种情况发生在从5.0到6.1的所有版本中。

编辑:我做了一些测试,试图了解我是做错了什么或是否有一些错误......

第一:我读错了乐器信息吗? 我不认为,泄漏是真实的,而不是我的错误。在执行该方法20次后,请查看此图像:

instruments leak

第二:如果我尝试在非弧形环境中做同样的事情会发生什么? 这增加了一些奇怪的行为:

NON-ARC环境中的相同功能:

- (IBAction)block2:(id)sender {
    NSMutableString *aString = [[NSMutableString alloc] init];

    void (^aBlock)() = ^{
        NSMutableString __unused *anotherString = aString;
    };

    [aString release];

    NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObject:[[aBlock copy] autorelease] forKey:@"Key"];
}

使用之前的非弧实现,我只有块的泄漏(不是字符串) 更改实现以在可变字符串上使用自动释放声明解决泄漏!我不明白为什么,我不确定它是否与主要职位问题有关

// version without leak
- (IBAction)block2:(id)sender {
    NSMutableString *aString = [[[NSMutableString alloc] init] autorelease];

    void (^aBlock)() = ^{
        NSMutableString __unused *anotherString = aString;
    };

    NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObject:[[aBlock copy] autorelease] forKey:@"Key"];
}

结论

经过各种答案和进一步调查,我理解了一些事情:

1- Apple文档说当您将块传递给集合时,必须使用[^ {} copy]。这是因为ARC不会添加副本本身。如果不这样做,集合(数组,字典..)会在STACK ALLOCATED OBJECT上发送一个retain - 它什么都不做。当方法结束时,块超出范围并变为无效。使用它时,您可能会收到错误的访问权限。但请注意:这不是我的情况,我遇到了不同的问题

2-我遇到的问题是不同的:块被过度保留(相反的问题 - >块仍然存在,即使它不存在)。为什么? 我发现了这个:在我的例子中,我正在使用这段代码

void (^aBlock)() = ^{
    NSMutableString __unused *anotherString = aString;
};

此代码在NON-ARC下,将引用(aBlock)存储到文字块。该块在堆栈上分配,因此如果您使用NSLog(@“%p”,aBlock) - >你会看到一个堆栈内存地址

但是,这是“奇怪的”(我在Apple文档中没有找到任何指示),如果你在ARC和NSLog aBlock地址下使用相同的代码,你会看到它现在在HEAP上! 出于这个原因,行为是不同的(没有坏的访问)

所以,不正确但行为不同:

// this causes a leak
- (IBAction)block2:(id)sender {
    NSMutableString *aString = [[NSMutableString alloc] init];

    void (^aBlock)() = ^{
        NSMutableString __unused *anotherString = aString;
    };

    NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObject:aBlock forKey:@"Key"];

}

// this would cause a bad access trying to retrieve the block from the returned dictionary
- (NSMutableDictionary *)block2:(id)sender {
    NSMutableString *aString = [[NSMutableString alloc] init];

    return [NSMutableDictionary dictionaryWithObject:^{
        NSMutableString __unused *anotherString = aString;
    } forKey:@"Key"];

}

3 - 关于我在NON-ARC下的最后一次测试,我认为发布的地方是错误的。我在使用copy-autorelease将块添加到字典之前释放了字符串。 该块自动保留块内引用的变量,但保留消息在复制时发送,而不是在声明处发送。所以,如果我在复制块之前释放aString ,它的保留计数变为0,然后该块向“zombie”对象发送一条保留消息(具有意外行为,它可能泄漏块,崩溃,ecc ecc)

2 个答案:

答案 0 :(得分:3)

请参阅此问题以供参考iOS 5 Blocks ARC bridged cast;它展示了Block和ARC的噩梦。

通常,如果将块分配给超出当前范围的变量,编译器将自动将块复制到堆中。这意味着当你超出范围时,你仍然可以使用这个块。同样,块参数也是如此。编译器知道它需要对这些参数进行复制,因此会这样做。

NSArray等类的问题在于,他们通常不需要复制对象以保持正确;通常他们只保留对象。虽然超出范围的对象是语言的一部分(因此它会复制),但将其保留在像NSArray这样的对象中是应用程序级操作。因此,编译器还不够聪明,无法确定块需要复制(块毕竟是标准的Obj-C对象,它认为它需要做的就是保留它)。类似的徒劳,这就是为什么持有块的任何属性都需要指定copy关键字。属性方法的自动合成不知道存储了一个块,并且在设置时需要给它一个微调来复制它们。

这说明了为什么当你在你的块上使用- copy时,整个事情都有效,你正在做编译器应该做的事情,但是这样做并不聪明...... Apple甚至推荐这种技术其Transitioning to ARC文档,请参阅常见问题解答。

Bootnote:如果您想知道我为什么要保留,即使您正在使用ARC,这就是ARC在幕后所做的事情。内存管理模型仍然和以前一样,但现在系统有责任根据命名和约定为我们管理它,而以前开发人员有责任正确管理内存。只是对于块,系统无法完全管理它,因此开发人员需要不时介入。

答案 1 :(得分:2)

由于性能原因,块在堆栈上开始生命。如果它们的存活时间比堆栈周期长,则必须将它们复制到堆中。

在MRR中,你必须自己复制。如果您将一个块传递给堆栈(即从方法返回),ARC会自动为您执行此操作。但是如果在堆栈中传递一个块(例如,将其存储在NSMutableDictionaryNSMutableArray中),则必须自己复制它。

Apple的Transitioning to ARC文档中记录了这一点,在该文档中搜索“块如何在ARC中工作”。

对于非ARC示例(正如您在结论中所写的那样),块的copy应该在发布aString之前发生,因为在复制块时保留aString 。否则,您的代码将显示未定义的行为,甚至可能会崩溃。以下是一些演示Non-ARC问题的代码:

NSObject *object = [[NSObject alloc] init];
void (^aBlock)() = ^{
    NSLog(@"%@", object);
};
[object release];
aBlock(); // undefined behavior. Crashes on my iPhone.