Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Block gets released whilst in NSDictionary (ARC)

I'm trying to retain a reference to a Block that's been passed in to my class by a method, to call at a later time. I'm having trouble, however, maintaining a reference to it.

The obvious way, I thought, was to add it to an ivar collection, all of which are supposed to maintain strong references to their contents. But when I try to pull it back out, it's nil.

The code is pretty simple:

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
}

What's happening?


Update: I've tried it with [callback copy] and just callback inserting into the dictionary, neither works.


Update 2: If I just stick my block into an NSMutableSet, as long as I call copy, I'm fine. It works great. But if it's in an NSDictionary, it doesn't.

I've actually tested it by putting a breakpoint right after the NSDict is created and the callback never gets inserted. The description reads clearly "1 key-value pair", not two.

I'm currently getting around this with a specialised class that just acts as a container. The callback property is declared as strong; I don't even need to use copy.

The question still stands, though: why is this happening? Why won't an NSDictionary store a Block? Does it have something to do with the fact that I'm targeting iOS 4.3 and thus ARC must be built in as a static library?


Update 3: Ladies and gentleman: I am an idiot.

The code I presented here was obviously a simplified version of the actual code; most particularly, it was leaving some key/value pairs out of the dictionary.

If you're storing a value in an NSDictionary using [NSDictionary dictionaryWithObjectsAndKeys:], you had better be damn sure one of those values isn't nil.

One of them was.

ICYMI, it was causing an early termination of the argument list. I had a userInfo-type argument being passed into one of the "add to queue" methods, and you could, of course, pass in "nil". Then when I constructed the dictionary, chucking in that argument caused the constructor to think I had terminated the argument list. @"callback" was the last value in the dictionary constructor and it was never being stored.

like image 961
Morgan Harris Avatar asked Nov 11 '11 05:11

Morgan Harris


1 Answers

Contrary to popular mis-conception, ARC does not automatically de-stackify Blocks passed as arguments to methods. It only de-stackify's automatically when a block is returned from a method/function.

I.e. this....

[dict setObject: ^{;} forKey: @"boom"];

... will crash if dict survives beyond the scope and you attempt to use the block (actually, it won't in this case because that is a static block, but that is a compiler detail that you can't rely on).

This is documented here:

How do blocks work in ARC?

Blocks “just work” when you pass blocks up the stack in ARC mode, such as in a return. You don’t have to call Block Copy any more. You still need to use [^{} copy] when passing “down” the stack into arrayWithObjects: and other methods that do a retain.

The return value behavior could be automated because it is always correct to return a heap based block (and always an error to return a stack based block). In the case of a block-as-an-argument, it is impossible to automate the behavior in a way that would be both very efficient and always correct.

The analyzer likely should have warned about this use. If it didn't, file a bug.

(I derped a stack when I meant a heap. Sorry about that.)


The compiler doesn't automate blocks-as-parameters for a few reasons:

  • unnecessarily copying a block to the heap can be a significant performance penalty
  • multiple-copies of a block can multiply that performance penalty significantly.

I.e.:

 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);

If that were to imply four Block_copy() operations and aBlock contained a significant quantity of captured state, that'd be a huge potential hit.

• There are only so many hours in the day and automating the handling of parameters is rife with non-obvious edge cases. If this were handled automatically in the future, it could be done without breaking existing code and, thus, maybe it will be done in the future.

I.e. the compiler could generate:

 aBlock = [aBlock copy];
 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);
 doSomethingSynchronous(aBlock);
 [aBlock release];

Not only would this fix the problem of a block-as-param, but it would also only produce one copy of the block across all potential uses.


The question still stands, though: why is this happening? Why won't an NSDictionary store a Block? Does it have something to do with the fact that I'm targeting iOS 4.3 and thus ARC must be built in as a static library?

Something bizarre is going on, then. Coincidentally, I've been using blocks-as-values in an ARC based application in the last week and it is working fine.

Do you have a minimal example handy?

like image 138
bbum Avatar answered Oct 23 '22 02:10

bbum