启用ARC后,iOS设备崩溃

7

我遇到了一个让我完全不知所措的问题。我会用代码示例来说明:

@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

当启用ARC并在设备上运行时,以上代码在[self setArray:mutable]行崩溃。该代码在模拟器上不会崩溃,禁用ARC的设备也不会崩溃。使用NSZombieEnabled指示setter试图保留已释放的数组。
如果注释掉第二个[mutable addObject:obj]调用,则不会崩溃(但是这段代码首先就没有执行)。
我已经将演示此崩溃的项目上传到Github上aidansteele/arc-crash。 我正在使用Xcode 4.5.2。 它似乎不会发生在Xcode 4.6上,但那仍处于开发人员预览阶段。 我做错了什么?

针对this问题的解答(为了让我有更多的空间,放在问题中),我认为问题不在-[NSArray enumerateObjectsUsingBlock:]方法内,因为如果我将该方法调用更改为使用以下-[NSArray(Functional) each:]调用,问题仍然存在。

@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

@sergio 我已经编辑了问题,以澄清它发生在设置属性时。 - Aidan Steele
@MarkThalman,我不确定你在说哪个后台线程... - Rad'Val
1
如果你发现你的代码在从Xcode 4.5.2到4.6的版本中表现不同,那么很可能是一个Xcode的bug。请提交反馈。 - NSResponder
可能是有效的,但这不是重点,它也应该能够与array一起使用。 - Rad'Val
2
也可以使用NSMutableArray *mutable = [@[] mutableCopy];或者其他传统增加保留计数的方法,效果同样良好。 - Rad'Val
显示剩余6条评论
3个回答

3
由于这个问题只出现在设备(ARM代码)上,并且在发布版本(优化代码)中,我非常怀疑您已经发现了Clang编译器在ARC、块和autorelease方面的优化器中存在一个错误。请在Radar中提出一个带有样例项目的错误报告。
如果您用enumerateObjectsUsingBlock替换它,则可以...
for (id n in items)
{
   [mutable addObject:n];
}

你的崩溃将会消失。
修复问题的其他代码更改:
替换:
[NSMutableArray array];

使用

[NSMutableArray new];

或者

[[NSMutableArray alloc] init];

另外,顺带提一下,您不应该将一个NSMutableArray存储在一个NSArray属性中。在将其分配给属性之前,您应该将NSMutableArray转换为NSArray。例如:
self.array = [NSArray arrayWithArray:mutable];

请注意,这并不能解决崩溃问题,只是更好的代码。
希望这有所帮助。

我认为这是一个非常适合提交radar的好候选人,特别是他已经有了一个样例项目。 - Kendall Helmstetter Gelner
1
"[NSArray arrayWithArray:mutable]"听起来像是一个非常绕弯子的说法,可以用"[mutable copy]"来代替。更好的做法是,使用copy语义来声明array属性。 - Aidan Steele
此外,如果有人希望跟踪其进展情况,它已经作为雷达[#12905972](http://openradar.appspot.com/12905972)提交。 - Aidan Steele
如果在Xcode 4.6中没有出现这个问题,我想知道这个radar实际上会有多少关注度。苹果不太可能回头对Xcode 4.5进行更改。 - StilesCrisis
你能否详细说明一下为什么你觉得 [NSMutableArray new]; 或者 [[NSMutableArray alloc] init];[NSMutableArray array]; 更好?这是个人偏好还是有文档记录的优势? - Axeva

2
我认为答案可能在于变量被自动释放,而块使用了这个自动释放的变量。
从Clang文档关于__autoreleasing对象存储期的部分来看:
如果一个程序声明了一个非自动存储期的__autoreleasing对象,则程序是不合法的。如果一个程序在块中捕获了一个__autoreleasing对象,除非通过引用,在C++11 lambda中也是如此,则程序是不合法的。
那么如何测试是否存在这个问题?
首先,我们需要确定捕获可变数组的块是否真的是问题的来源。注释掉第一个块中可变数组的使用,并在枚举得到的值上使用NSLog进行调试。
[items enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        //[mutable addObject:obj];
        NSLog(@"item is %@",obj);
    }];

这修复了崩溃。如果我们只是以不会导致变异的方式引用可变数组,会怎样呢?(以确保变异不是问题)
[items enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        //[mutable addObject:obj];
        NSLog(@"Mutable array is %@",mutable);
    }];

那仍然会崩溃,所以我们可以判断在块中引用自动释放的可变数组是有问题的。另外,正确使用arrayWithCapacity来容纳所有值也会导致崩溃。
那么,如果问题是块捕获了一个自动释放的对象,我们该如何解决这个问题呢?
我们可以将变量设为strong,这样ARC就必须释放它:
   NSMutableArray *mutable = [NSMutableArray array];

那就解决了崩溃问题,而当方法退出时ARC会适当地释放该变量。但是我并不完全确定这就是全部 - 只要在该方法的任何位置引入这个简单的块也可以解决崩溃问题。
  void (^useMute)();

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

即使它从未被使用,它也会导致可变数组被保留并阻止早期释放。因此,似乎真正的错误在于enumerateUsingBlock和自动释放池之间的交互。另外,更值得一提的是,解决该问题的另一种方法是使用普通枚举而不是块枚举。
   for (id obj in items )
      {
          [mutable addObject:obj];
      }

有时候,除非你有很好的理由使用更高级的方法,否则最好使用简单的机制来做事情。对于一个意图进行直接同步代码执行的数组元素循环,如果你不需要访问块传递给你的其他参数,为什么要使用块呢?你甚至可以使用C语言的continue和stop结构更好地控制循环,而块循环只允许完全停止枚举。

我应该指出,我只是为了举例而使用-[NSArray enumerateObjectsUsingBlock:]。这个问题发生在一个更大的项目中,但我在这里将其缩小到最小的示例以重现该问题。对于没有在问题中明确说明并让你白忙一场,我深感抱歉! - Aidan Steele
值得注意的是,在第二个代码块(即从未被调用的代码块)中注释掉mutable的使用也可以消除这个崩溃问题。 - Aidan Steele
我认为这可能是在一个更大的项目中出现的问题,但似乎可以采取修复建议来帮助解决实际问题...评论掉未被调用的代码会引起任何问题都很有趣。一定要提交一个报告。 - Kendall Helmstetter Gelner

0

我认为你的SDK版本中的enumerateObjectsUsingBlock方法存在问题。也许你应该阅读新SDK的发布说明或旧版本的已知问题以了解更多信息。所以,问题在于当你调用enumerateObjectsUsingBlock方法时,你的保留计数器一切正常。但是在该方法退出后,你的指针指向一些垃圾。

修复它的一种方法是要对你的集合负责,并承诺在enumerateObjectsUsingBlock方法退出之前不会处理它。虽然这是一个解决方案,但它并没有解决核心问题,就像我上面说的那样,我认为问题在于enumerateObjectsUsingBlock方法。这里有一个可行的代码(解决方案)。

#import "Crasher.h"

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

@implementation Crasher

- (void)crash;
{
  __block 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];
    }];
  }

    NSLog(@"%@", mutable);

  [self setArray:mutable];
}

@end

我在问题正文中回复了你的答案。这个评论区域并没有提供足够的空间来进行漂亮格式的回答。 :) - Aidan Steele
我明白了,那么这一定与块有关。尝试创建一个新项目,从模板开始,不修改任何设置,看看是否相同的代码会导致相同的问题。 - Rad'Val
有一件事是肯定的,只要你在代码中使用了一个块,引用计数就会中断。我提出的解决方案仍然有效,但由于它似乎比预期的要复杂,你应该找到根本问题。 - Rad'Val
问题正文中提到的示例项目确实是一个全新的项目,除了这段代码之外没有添加任何内容。不幸的是,它仍然存在同样的问题。 - Aidan Steele

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