ios - 将 GCD 与 TableView 一起使用会导致间歇性断言

标签 ios objective-c uitableview grand-central-dispatch

我正在使用同步 GCD 队列来控制对 NSArray 的访问,并确保数组在枚举或读取时不会发生变化。这在大多数情况下都很好用,但是,我收到了一个间歇性断言,该断言仅在应用程序后台运行一段时间后才随机发生。

我的代码:

dispatch_async(synchronous_queue, ^{
  // Update the data source and insert the new items
  NSUInteger currentItemsArraySize = originalItems.count;
  [originalItems addObjectsFromArray:newItemsFromCache];
  NSMutableArray *indexPathArray = [NSMutableArray arrayWithCapacity:newItemsSize];
  for (NSUInteger i = currentItemsArraySize; i < (currentItemsArraySize + newItemsSize); i++) {
    [indexPathArray addObject:[NSIndexPath indexPathForRow:i inSection:0]];
  }

  dispatch_async(dispatch_get_main_queue(), ^{
    [self beginUpdates];
    [self insertRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationNone];
    [self endUpdates];
  });
});

断言:

2014-05-09 15:31:03.832 MyApp[8650:60b] * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (90) must be equal to the number of rows contained in that section before the update (90), plus or minus the number of rows inserted or deleted from that section (10 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).' * First throw call stack: (0x184ae709c 0x190a65d78 0x184ae6f5c 0x185617194 0x187bb11c4 0x1001204d4 0x191034420 0x1910343e0 0x19103756c 0x184aa6d64 0x184aa50a4 0x1849e5b38 0x18a40b830 0x187a240e8 0x1000ee63c 0x19104faa0) libc++abi.dylib: terminating with uncaught exception of type NSException

通常,这个断言是因为我在调用 insertRowsAtIndexPaths: 之前没有更新我的数据源,但奇怪的是断言似乎认为表更新已经执行,即使所有此时我所做的是更新表数据源数组。

例如,当我查看 indexPathArray 时,它告诉我我将尝试插入第 80 行到第 89 行,但断言说我在数据源中已经有 90 个项目没有意义。

(lldb) po indexPathArray
<__NSArrayM 0x17044ac20>(
<NSIndexPath: 0xc000000000280016> {length = 2, path = 0 - 80},
<NSIndexPath: 0xc000000000288016> {length = 2, path = 0 - 81},
<NSIndexPath: 0xc000000000290016> {length = 2, path = 0 - 82},
<NSIndexPath: 0xc000000000298016> {length = 2, path = 0 - 83},
<NSIndexPath: 0xc0000000002a0016> {length = 2, path = 0 - 84},
<NSIndexPath: 0xc0000000002a8016> {length = 2, path = 0 - 85},
<NSIndexPath: 0xc0000000002b0016> {length = 2, path = 0 - 86},
<NSIndexPath: 0xc0000000002b8016> {length = 2, path = 0 - 87},
<NSIndexPath: 0xc0000000002c0016> {length = 2, path = 0 - 88},
<NSIndexPath: 0xc0000000002c8016> {length = 2, path = 0 - 89}
)

什么可能导致这种情况只是随机发生,我该如何防止这种情况?


编辑:我将尝试根据这个 http://blog.csdn.net/chuanyituoku/article/details/17288991 重写它这基本上应该做我想要的(确保数据源在被枚举时不会发生变化)并修复迈克尔描述的竞争条件。

- (NSMutableArray *)objects {
  __block NSMutableArray *syncObjects;
  dispatch_sync(synchronous_queue, ^{
    syncObjects = _objects
  });
  return syncObjects;
}

- (void)setObjects:(NSMutableArray *)objects {
  dispatch_barrier_async(synchronous_queue, ^{
    _objects = objects;
  });
}

最佳答案

当您向 TableView 插入行(或从 TableView 中删除行)时,您必须确保对 TableView 数据源方法的后续调用为 TableView 提供一致的世界观。

例如当 TableView 有 4 行,而您插入 2 行时,对 -numberOfRowsInTableView: 的后续调用必须返回 6,而 -tableView:cellForRowAtIndexPath: 方法应返回两个新插入行的新数据。

到目前为止一切顺利...

现在,您的代码中存在竞争条件。

第一个竞争条件:您从不同线程访问 iVar originalItems

第二个竞争条件:在更新 originalItems 数组之后,但在主线程上同步调用 -insertRowsAtIndexPaths:withRowAnimation: 之前, TableView 可能会调用数据源方法。这导致了崩溃。当您在主线程上执行所有更新操作时,崩溃就会消失...

可能的解决方案

我只会在主线程上进行更新!因此,您可以创建一个名为 -addItems: 的方法,它将新行添加到 tableview 并且可以从任何线程调用。如果您这样做,不会有性能损失。如果 newItems 数组的计算开销很大,您仍然可以在另一个线程中进行,但实际更新必须在主线程中进行。此外,您可以在每次访问 originalItems 数组时放置一个同步块(synchronized block),这样您就可以从其他线程获得只读访问权限。

// you can call this method from any thread
- (void)addItems:(NSArray *)newItems
{
    // Ensure we are on the main thread
    if(![NSThread isMainThread]) {
        newItems = [newItems copy];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self addItems:newItems];
        });
        return;
    }

    // We are on the main thread...

    // Update the data source and insert the new items
    NSMutableArray *indexPathArray;
    @synchronized(self) {
        NSUInteger currentItemsArraySize = originalItems.count;
        [originalItems addObjectsFromArray:newItems];
        indexPathArray = [NSMutableArray new];
        for (NSUInteger i = currentItemsArraySize; i < (currentItemsArraySize + newItemsSize); i++) {
            [indexPathArray addObject:[NSIndexPath indexPathForRow:i inSection:0]];
        }
    }

    [self beginUpdates];
    [self insertRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationNone];
    [self endUpdates];
}

关于ios - 将 GCD 与 TableView 一起使用会导致间歇性断言,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23608499/

相关文章:

ios - 如何在 Swift 中使用扩展协议(protocol)公开 Obj-C 类的现有属性

ios - UIRefreshControl 怪异

objective-c - 如何从 UITableViewCell.accessoryView 调用 PopOver Controller ?

ios - 如何使 UITableView 看起来像这样

objective-c - 如何使用当前的模态视图 Controller 在 View Controller 之外呈现模态视图 Controller :animated?

ios - 如何使用 MapBox 读取离线 map

iphone - 从其矩形中删除事件指示器?

ios - 在 TableView 中显示搜索结果时没有结果 - swift

ios - 将 segue 添加到分割 View Controller

iOS force landscape in one viewController