【问题标题】:iOS crash on device with ARC enablediOS 在启用 ARC 的设备上崩溃
【发布时间】:2012-12-06 06:18:17
【问题描述】:

我遇到了一个让我完全难过的问题。我会用一个代码示例来说明:

@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:],因为如果我更改该方法调用以使用以下@987654329,问题仍然存在@调用。

@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

【问题讨论】:

  • 崩溃发生在哪一行?
  • 可能是块的问题。如果您使用 ARC,但在前台线程而不是后台线程上进行枚举,会发生什么情况?在枚举完成之前,您可能不想调用 setArray。现在它几乎立即发生。我敢打赌,如果你让 items 数组足够大,它会在模拟器中崩溃。
  • @sergio 我已经编辑了问题以澄清设置属性时会发生这种情况。
  • 如果您发现 Xcode 4.5.2 到 4.6 的代码行为有所不同,那么您很可能是在查看 Xcode 错误。请归档。
  • 也适用于 NSMutableArray *mutable = [@[] mutableCopy]; 或任何其他传统上增加保留计数的方法。

标签: objective-c ios memory-management automatic-ref-counting


【解决方案1】:

因为这个问题只发生在设备上(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];

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

希望这会有所帮助。

【讨论】:

  • 我同意这是一个非常好的提交雷达的候选人,特别是因为他已经有一个示例项目。
  • [NSArray arrayWithArray:mutable] 听起来像是一种非常迂回的说法 [mutable copy]。更好的是,array 属性应该用copy 语义声明。
  • 另外,如果有人想关注它的进展,它会以雷达#12905972 的形式提交。
  • 如果它没有出现在 Xcode 4.6 中,我想知道这个雷达实际上会获得多大的牵引力。 Apple 目前不太可能回过头来对 Xcode 4.5 进行更改。
  • 您能否详细说明为什么您觉得[NSMutableArray new];[[NSMutableArray alloc] init];[NSMutableArray array]; 好?这是个人喜好,还是有记录的优势?
【解决方案2】:

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

来自storage duration of __autoreleasing objects 上的 Clang 文档:

如果一个程序声明了一个 __autoreleasing 对象 非自动存储时间。一个程序是非良构的,如果它捕获 块中的 __autoreleasing 对象,或者,除非通过引用,否则在 C++11 拉姆达。

那么如何测试这是否是问题所在?

首先我们看看捕获可变数组的块是否真的是问题的根源。在第一个块(唯一调用的块)中注释掉可变数组的使用,并在通过枚举找到的值上使用 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 也会导致崩溃。

如果问题是捕获自动释放对象的块,我们该如何解决这个问题?

我们可以改为强变量,这样 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 甚至比 block 循环有更多的控制权,它只允许完全停止枚举。

【讨论】:

  • 我应该注意,我只是使用-[NSArray enumerateObjectsUsingBlock:] 来进行说明。这个问题发生在一个更大的项目中,但我在这里将其简化为重现该问题的最小示例。我很抱歉没有在问题中明确说明这一点并带领您进行疯狂追逐!
  • 另外值得注意的是,在 second 块(即从未调用过的块)中注释掉 mutable 的使用也会导致此崩溃消失。跨度>
  • 我认为这是一个较大项目中的问题,但似乎可以收回有关修复的建议以帮助解决真正的问题......非常有趣的是注释掉代码不是调用会导致任何类型的问题。绝对归档雷达。
【解决方案3】:

我认为您的 SDK 版本的 enumerateObjectsUsingBlock 方法存在问题。也许您应该阅读新 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

【讨论】:

  • 我在问题正文中回复了您的回答。这个评论空间并没有真正为格式良好的答案提供足够的空间。 :)
  • 我明白了,那么它通常与块有关。尝试从模板创建一个新项目,而不修改任何设置,看看相同的代码是否会导致相同的问题。
  • 可以肯定的是,一旦您在代码中使用了块,保留计数就会中断。我建议的解决方法仍然有效,但由于它似乎比预期的要大,你应该找到根本问题。
  • 问题正文中提到的示例项目确实是一个新项目,只是添加了这段代码。不幸的是,它仍然有同样的问题。
猜你喜欢
  • 2015-10-22
  • 2012-08-30
  • 1970-01-01
  • 2019-11-01
  • 2013-06-06
  • 1970-01-01
  • 1970-01-01
  • 2018-03-02
  • 2019-12-30
相关资源
最近更新 更多