Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS crash on device with ARC enabled

I've hit a problem that has me completely stumped. I'll illustrate with a code sample:

@interface Crasher ()
@property (nonatomic, strong) NSArray *array;
@end

@implementation Crasher

- (void)crash;
{
  NSMutableArray *mutable = [NSMutableArray array];
  NSArray *items = @[@0, @1, @2, @3];

  if ([@YES boolValue])
  {
    [items enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
      [mutable addObject:obj];
    }];
  }
  else
  {
    [items enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
      [mutable addObject:obj];
    }];
  }

  [self setArray:mutable];
}

@end

The above code crashes on the [self setArray:mutable] line when ARC is enabled and it is run on a device. The code never crashes on the simulator nor does it crash on a device with ARC disabled. Using NSZombieEnabled indicates that the setter is attempting to retain an already-deallocated array.

It does not crash if the second [mutable addObject:obj] call is commented out (but this code is never executed in the first place).

I've uploaded a project that demonstrates this crash to Github to aidansteele/arc-crash. I am using Xcode 4.5.2. It seems to not occur on Xcode 4.6, but that is still in developer preview. What am I doing wrong?


Addressing this answer (in the question so that I have a bit more space), I don't believe the problem lies within -[NSArray enumerateObjectsUsingBlock:] as the problem persists if I change that method call to use the following -[NSArray(Functional) each:] call.

@interface NSArray (Functional)
- (void)each:(void (^)(id obj))action;
@end

@implementation NSArray (Functional)

- (void)each:(void (^)(id))action;
{
  for (NSUInteger idx = 0; idx < [self count]; idx++)
  {
    action([self objectAtIndex:idx]);
  }
}

@end
like image 642
Sedate Alien Avatar asked Dec 18 '12 23:12

Sedate Alien


2 Answers

Because this problem only occurs on the device (ARM code), and in a Release build (optimised code) I very much suspect you have found a bug in the Clang compiler's optimiser with respect to ARC and blocks and autorelease. Raise a bug in Radar with your sample project as an attachment.

If you replace the enumerateObjectsUsingBlock with

for (id n in items)
{
   [mutable addObject:n];
}

you're crash will go away.

Other changes to the code that fix the problem:

Replace:

[NSMutableArray array];

with

[NSMutableArray new];

or

[[NSMutableArray alloc] init];

Also, as an aside, you shouldn't be storing an NSMutableArray in a NSArray property. You should be converting the NSMutableArray to an NSArray before assigning it to the property. Eg:

self.array = [NSArray arrayWithArray:mutable];

Note that this won't fix the crash. It is just better code.

Hope this helps.

like image 133
orj Avatar answered Nov 06 '22 08:11

orj


I think the answer may lie with the variable being auto-released, and the block making use of this auto-released variable.

From the Clang docs on storage duration of __autoreleasing objects:

A program is ill-formed if it declares an __autoreleasing object of non-automatic storage duration. A program is ill-formed if it captures an __autoreleasing object in a block or, unless by reference, in a C++11 lambda.

So how to test if this is the issue?

First we see if the block capturing the mutable array really is the source of the issue. Comment out the use of the mutable array in the first block (the only block called) and use NSLog on the value found by enumeration instead:

[items enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        //[mutable addObject:obj];
        NSLog(@"item is %@",obj);
    }];

That fixes the crash. What if we simply reference the mutable array in a way that does not cause mutation (to make sure that mutation is not an issue)?

[items enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        //[mutable addObject:obj];
        NSLog(@"Mutable array is %@",mutable);
    }];

That still crashes, so we can tell just referencing the autoreleased mutable array in the block is causing an issue. On a side note, you also get a crash using arrayWithCapacity correctly sized to hold all the values.

So how can we fix this issue, if the problem is a block capturing an autoreleased object?

We can make the variable strong instead, so that ARC has to release it:

   NSMutableArray *mutable = [NSMutableArray array];

That fixes the crash, and ARC properly releases the variable when the method is left.

However I am not wholly sure that is the full story - merely introducing this simple block anywhere in that method also fixes the crash:

  void (^useMute)();

    useMute = ^() {
        NSLog(@"Mutable is %@", mutable);
    };

Even if it is never used, it's causing the mutable array to be retained and preventing an early release also. So it seems like the true error is somewhere with the interaction of enumerateUsingBlock and the auto-release pool.

On more of a side note, what also fixes the issue is to use normal enumeration instead of block enumeration:

   for (id obj in items )
      {
          [mutable addObject:obj];
      }

Sometimes it's better to use the simpler mechanisms to do things unless you have a great reason for using the fancier methods. For a loop over array elements that is intended to be straight-forward synchronous code execution, why use a block if you don't need access to the other parameters the block passes you? You even have more control using the C constructs continue and stop than the block loop, which only allows for stopping the enumeration entirely.

like image 36
Kendall Helmstetter Gelner Avatar answered Nov 06 '22 09:11

Kendall Helmstetter Gelner