ObjC: BAD ACCESS when call blocks ^{} in later fun

2019-02-27 18:17发布

问题:

Following this discussion, I've encountered a bad access issue;

A loop has several steps: a, b, c, ... x, y, z:

-(void)cycle:(float)delta{
[self stepA]
[self stepB]
// etc.
[self stepZ]
}

At some point, step x does the following:

// IRQ is an NSMutableArray
// Self is a reference to the engine running the cycles
[IRQ addObject:^{ NSLog(@"hello! %@", self); } ];

Later, step z is to process all "delayed" calls:

            for (int i = 0; i < [IRQ count]; i++){
                void (^delayedCall)(void) = [IRQ objectAtIndex:i];
                delayedCall();
            }

            [IRQ removeAllObjects];

Result: EXEC_BAD_ACCESS

Now, if step x only adds a plain string with no object reference like follows, step Z works fine:

[IRQ addObject:^{ NSLog(@"hello!"); } ];

Last observation, if a same step both adds blocks to the queue AND iterates over the queue to execute the blocks, then no problem occurs. Like the reference to an object gets "lost" as the step: method is left?

I don't understand much in this area and will need more help!

edit: James, just tried the following to avoid that reference cyle:

NSString *userName = @"James";
[IRQ addObject:^{ NSLog(@"hello %@", userName); } ];

and it also happens. How would your solution apply to this?

Thanks in advance!

回答1:

When you create a block with the ^{} syntax, it's created on the stack. To persist the block for a long period of time (beyond the scope of the function that creates it), you must copy the block into the heap:

void (^ myBlock)(void) = ^ {
    // your block code is here.
};
[IRQ addObject:[[myBlock copy] autorelease]];

If using ARC, skip the -autorelease message.



回答2:

The problem is that block objects are created on the stack. You need to copy blocks to the heap when you expect them to be used after the scope in which they were declared is destroyed, and if the block is not copied for you.

Here you pass an object "down the stack" to a method that is not aware of blocks. Replace

[IRQ addObject:^{ NSLog(@"hello! %@", self); } ];

with

[IRQ addObject:[^{ NSLog(@"hello! %@", self); } copy]];

and the EXC_BAD_ACCESS at this point will go away.

In most cases though, you do not need to copy the block! A couple of examples:

  1. If you return a block from a method ("up the stack"), ARC will automatically copy it.
  2. If you call a method that does not keep the block, the block does not need to be copied, because it stays in scope. Example: the block passed to -[NSArray sortedArrayUsingComparator:].
  3. If you call a method that uses the block later, the method should take the responsible for copying the block, otherwise each and every caller would need to copy the block. All methods/functions from Apple's libraries that I am aware of follow that pattern. Example: the completion block passed to +[UIView animateWithDuration:options:animations:completion:].


回答3:

It seems the object you pass in.. In your examples: self and userName are being prematurely deallocated. This isn't the behaviour I expect from blocks. As in my previous answer, I expected the problem to be because of too much retention!

As a test, could you try:

NSString *userName = [@"James" retain];
[IRQ addObject:^{ NSLog(@"hello %@", userName); } ];

This would be a memory leak, but it would help indicate if the object is being deallocated.

This is caused by a "retain cycle" where the block is retaining self and self is retaining the block.

Try this:

__block typeof(self) blockSafeSelfReference = self;
[IRQ addObject:^{ NSLog(@"hello! %@", blockSafeSelfReference); } ];

If using ARC, use __unsafe_unretained instead of __block