4

我正在尝试使用 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...),然后方法返回并释放字典。然后应该释放块。但是,使用仪器,我看到了泄漏

泄漏

泄漏2

泄漏3

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

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

同样的结果。

我找到了这篇文章,但答案指向另一个问题: NSMutableArray 中的块泄漏(ARC)

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

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

至:

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

泄漏消失。

我知道,在非 ARC 下,在传递块文字的引用之前,我必须复制它(当声明文字时,它在堆栈上,所以我需要在传递到函数范围之外之前将它复制到堆已声明)......但使用ARC我不应该关心它。有什么迹象吗?从 5.0 到 6.1 的所有版本都会发生这种情况。

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

第一:我读错了仪器信息吗?我不认为,泄漏是真实的,而不是我的错误。看这张图……执行该方法 20 次后:

仪器泄漏

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

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 不添加副本本身。如果您不这样做,则集合(数组、字典..)会在堆栈分配对象上发送保留 - 它什么也不做。当方法结束时,块超出范围并变为无效。使用它时,您可能会收到错误的访问权限。但请注意:这不是我的情况,我遇到了不同的问题

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,然后块向“僵尸”对象发送保留消息(出现意外行为,它可能会泄漏块、崩溃、ecc ecc)

4

2 回答 2

3

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

通常,如果您将块分配给超出当前范围的变量,编译器将能够自动将该块复制到堆中。这意味着当你掉出范围时,你仍然有块在附近徘徊。同样,块参数也是如此。编译器知道它需要复制这些参数,因此这样做了。

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

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

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

于 2013-02-05T10:12:15.950 回答
2

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

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

这记录在 Apple 的Transitioning to ARC文档中,在该文档中搜索“How do blocks work in ARC”。

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

NSObject *object = [[NSObject alloc] init];
void (^aBlock)() = ^{
    NSLog(@"%@", object);
};
[object release];
aBlock(); // undefined behavior. Crashes on my iPhone.
于 2013-02-05T11:49:56.073 回答