【问题标题】:Leak when using blocks, collections and ARC使用块、集合和 ARC 时泄漏
【发布时间】:2013-01-20 04:19:58
【问题描述】:

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

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

[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 次后:

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

在非 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 文档说,当您将块传递给集合时,您必须使用 [^{} 复制]。这是因为 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)

【问题讨论】:

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


    【解决方案1】:

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

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

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

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

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

    【讨论】:

    • 我做了同样的假设,但最后我认为问题不在于:NSArray(例如)向集合内的对象发送保留消息。堆栈上对象的保留消息不会增加保留计数,因此当对象(块)超出范围时,它会被销毁。这就是为什么您应该在非弧中使用复制(将块添加到数组)的原因 --> 如果不这样做,您将有一个指向堆栈上对象的指针(可能超出范围)。在这种情况下,我有相反的问题,泄漏,“过度保留”的对象。我的假设有问题吗?
    • 它是否泄漏了;在您的图像中,它表明它在哪里泄漏?该块最后的保留计数为 0,这不是泄漏,还是我遗漏了什么?
    • 这是我不明白的另一件事。块的refct为0,但是instruments显示对象是存活的--> 可以看到,即使是可变字符串也被存活块保留(在这种情况下,字符串的保留计数为1)
    • 你的意思是持续计数吗?这并不是存在泄漏的直接迹象。系统可能会代表您缓存一些东西,您需要通过几个循环运行应用程序以查看它们是否正在累积。如果是,那么就是泄漏(有关一些信息,请参阅developer.apple.com/library/mac/#documentation/developertools/…)。虽然我在您的示例中看不到任何泄漏,但有一个特定的 Leaks 仪器可以非常清楚地标记任何泄漏,因此您可以尝试一下。
    • 你也得想一想,什么是过度保留对象导致泄漏?我什么也没看到,我认为这是对 Instruments 提供的信息的误解。
    【解决方案2】:

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

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

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

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

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

    【讨论】:

    • 哦...这很尴尬...我已经打开该文档很多次了,但从未看过常见问题解答部分:-(
    • ;) 这可能会发生。至于您的非 ARC 示例,我现在将添加答案!
    • 谢谢,如果你知道的话,我还有一个问题......文档说“你必须做”,但没有详细说明(这是我想知道的 :-) .. .如果ARC不自己添加副本(将块添加到数组中),我不应该有不同的行为吗?我将保留发送到堆栈对象->该对象并未真正保留或复制......所以当方法返回时它应该“弹出”堆栈。数组中的引用应该是无效的并且不应该发生泄漏......
    • 哎呀,对不起,关于非弧形部分,我在答案中使用剪切粘贴犯了一个错误。我更正了代码,最后一行有一个copy-autorelease,不仅是一个副本,而且还在泄漏。立即尝试代码
    • 这里发生的自动复制是未记录的行为。我不会依赖它——你看到的泄漏是一个很好的迹象,表明它不应该被依赖。
    猜你喜欢
    • 2012-02-07
    • 2012-05-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-09
    • 2012-04-03
    相关资源
    最近更新 更多