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

[self setArray:mutable]启用 ARC 并在设备上运行时,上述代码在线崩溃。该代码永远不会在模拟器上崩溃,也不会在禁用 ARC 的设备上崩溃。UsingNSZombieEnabled表示 setter 试图保留一个已经释放的数组。

[mutable addObject:obj]如果第二次调用被注释掉,它不会崩溃(但这段代码从不首先执行)。

我已将演示此崩溃的项目上传到 Github 到aidansteele/arc-crash. 我正在使用 Xcode 4.5.2。它似乎不会出现在 Xcode 4.6 上,但仍处于开发者预览版中。我究竟做错了什么?


解决这个答案(在问题中以便我有更多空间),我不认为问题出在问题之内-[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
4

3 回答 3

3

因为这个问题只发生在设备上(ARM 代码),并且在发布版本(优化代码)中,我非常怀疑你在 Clang 编译器的优化器中发现了关于 ARC 和块和自动释放的错误。使用您的示例项目作为附件在 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];

请注意,这不会修复崩溃。这只是更好的代码。

希望这可以帮助。

于 2012-12-19T06:58:13.103 回答
2

我认为答案可能在于自动释放的变量,以及使用这个自动释放变量的块。

来自关于__autoreleasing 对象的存储持续时间的 Clang 文档:

如果程序声明了一个非自动存储持续时间的 __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 进行比块循环更多的控制,后者只允许完全停止枚举。

于 2012-12-19T07:10:09.517 回答
0

我认为 enumerateObjectsUsingBlock您的 SDK 版本的方法存在问题。也许您应该阅读新 SDK 的发行说明或旧 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
于 2012-12-19T00:46:43.957 回答