在NSDictionary中,块被释放(ARC)

16

我试图通过一个方法将传递给我的类的Block保留下来,以便稍后调用。然而,我遇到了问题,无法维护对它的引用。

我想到的显而易见的方法是将它添加到一个ivar集合中,所有这些集合都应该维护对其内容的强引用。但是当我尝试将其取回时,它为nil。

代码非常简单:

typedef void (^DataControllerCallback)(id rslt);

@interface DataController : NSObject {
    NSMutableArray* queue;
}
- (void) addBlock:(DataControllerCallback)callback;
- (void) functionToBeCalledLater;
@end

@implementation DataController

- (id) init {
    self = [super init];
    if (self != nil) {        
        queue = [NSMutableArray new];
    }
    return self;
}

- (void) addBlock:(DataControllerCallback)callback {
    NSDictionary* toAdd = [NSDictionary dictionaryWithObjectsAndKeys:
        [callback copy], @"callback",
        @"some other data", @"data", nil];
    [queue addObject:toAdd];
}

- (void) functionToBeCalledLater {
    NSDictionary* dict = [queue lastObject];
    NSLog(@"%@", [dict objectForKey:@"data"]; //works
    DataControllerCallback callback = [dict objectForKey:@"callback"]; //this is nil
    callback(@"an arguemnt"); //EXC_BAD_ACCESS
}

发生了什么?


更新:我已经尝试了[callback copy]和只插入callback到字典中,两者都不起作用。


更新2:如果我将代码块放入NSMutableSet中,只要我调用copy,就可以正常工作。它很棒。但是如果它在NSDictionary中,就不行。

我实际测试过了,在创建NSDict之后立即设置断点,回调函数从未被插入。描述清楚地显示“1个键值对”,而不是两个。

我目前正在使用一个专门的类来解决这个问题。 callback属性被声明为strong; 我甚至不需要使用copy

然而,问题仍然存在:为什么会发生这种情况?为什么NSDictionary不会存储Block?这是否与我针对iOS 4.3并且ARC必须作为静态库构建有关?


更新3:女士们先生们:我是个白痴。

我在这里展示的代码显然是实际代码的简化版本; 尤其是,它省略了一些键/值对。

如果您使用[NSDictionary dictionaryWithObjectsAndKeys:]存储在NSDictionary中,您最好确保这些值之一不是nil

其中一个是。

如果您错过了这一点,它会导致参数列表的提前终止。我有一个userInfo类型的参数传递到其中一个“添加到队列”的方法中,您当然可以传递“nil”。然后,当我构造字典并将该参数投入其中时,构造函数会认为我已经终止了参数列表。 @"callback"是字典构造器中的最后一个值,它从未被存储。


我使用 [callback copy] 复制粘贴了这段代码,一切都运行良好。 - Firoze Lafeer
functionToBeCalledLater 在运行循环的不同迭代中发生,尽管我认为这并不重要,但也许与 ARC 放置其保留和释放的位置有关。 - Morgan Harris
我在运行循环的不同迭代中也调用了functionToBeCalledLater函数(为此制作了一个小按钮,并且实际上点击了多次)。 - Firoze Lafeer
你能否提供一个完整可编译的示例,以便我们了解你遇到的问题? - Joshua Weinberg
是的,我传递的块不是静态的。这是Xcode构建4D199。有趣。 - Firoze Lafeer
1个回答

33
与普遍误解相反,ARC 不会自动将作为参数传递给方法的 Block 解封。只有在从方法/函数返回 Block 时,它才会自动解封。 换句话说,只有像这样返回 Block 时...
[dict setObject: ^{;} forKey: @"boom"];

如果dict超出其作用域并且你尝试使用该块,则会崩溃(实际上,在这种情况下不会,因为那是一个静态块,但这是编译器的细节,您不能依赖它)。
这在此处记录
“ARC模式下,当您将块通过堆栈传递时,例如在返回中,块“只需工作”。 您不再需要调用Block Copy。但是,当向“下”传递给arrayWithObjects:和执行保留的其他方法时,仍需要使用[^ {} copy]。”
返回值行为可以自动化,因为始终正确返回基于堆的块(并且始终返回基于栈的块是错误的)。 在块作为参数的情况下,无法以非常有效且始终正确的方式自动化行为。
分析器可能应该警告此使用。 如果没有,请报告错误。
编译器不自动处理块作为参数的原因有几个:
- 不必要地将块复制到堆中可能会产生显着的性能损失 - 多次复制块会使该性能损失显著增加。
 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);

如果这意味着四个Block_copy()操作并且aBlock包含大量捕获状态,那将是一个巨大的潜在影响。
• 一天中只有这么多小时,自动处理参数充满了非显而易见的边缘情况。 如果未来可以自动处理此项工作,那么可以在不破坏现有代码的情况下完成,因此可能在未来完成。
即编译器可以生成:
 aBlock = [aBlock copy];
 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);
 [aBlock release];

不仅可以解决块作为参数的问题,而且在所有潜在用途中只会生成一个块的副本。
但问题仍然存在:为什么会发生这种情况?NSDictionary为什么不能存储块?这是否与我针对 iOS 4.3 并因此必须构建 ARC 作为静态库有关?
那么,一些奇怪的事情正在发生。巧合的是,过去一周中我一直在使用基于 ARC 的应用程序中的块作为值,它运行良好。你有一个简单的示例吗?

1
总是正确返回基于堆栈的块,总是错误返回基于堆栈的块?我怀疑其中一个应该说“堆”。 - BJ Homer
在“始终返回基于堆栈的块是正确的(并且始终返回基于堆栈的块是错误的)”中,第一种情况应该是_static_块吗? - user557219
是的——^stack^heap。编辑。已修复。谢谢。需要明确的是——您永远不能依赖于块是静态的还是不静态的……因此,不,返回基于堆栈的块从来都不可以,因此必须始终返回基于堆的块,“始终”意味着“可以在编译器中自动化”。 - bbum
除了作为初始化__strong参数变量的一部分保留之外,我在文档中错过了这一部分。这种情况下它不做同样的帮助,有什么原因吗? - Joshua Weinberg
嘿,尽管这并没有解决我的原始问题,但我已将其标记为已回答/点赞,因为这是一个超级棒的答案,而且我之前并不知道。你应该为此感到自豪,bbum。 - Morgan Harris
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接