将GCD与表视图一起使用会导致间歇性断言

时间:2014-05-12 12:02:34

标签: 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] *由于未捕获的异常'NSInternalInconsistencyException'而终止应用程序,原因:'无效更新:第0部分中的行数无效。更新(90)后现有部分中包含的行必须等于更新前的该部分中包含的行数(90),加上或减去从该部分插入或删除的行数(插入10个,删除0个) )加上或减去移入或移出该部分的行数(0移入,0移出)。   * 第一次抛出调用堆栈:   (0x184ae709c 0x190a65d78 0x184ae6f5c 0x185617194 0x187bb11c4 0x1001204d4 0x191034420 0x1910343e0 0x19103756c 0x184aa6d64 0x184aa50a4 0x1849e5b38 0x18a40b830 0x187a240e8 0x1000ee63c 0x19104faa0)   libc ++ abi.dylib:以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;
  });
}

1 个答案:

答案 0 :(得分:2)

将行插入表视图(或从表视图中删除行)时,必须确保对表视图数据源方法的后续调用为表视图提供一致的世界视图。

E.g。当表视图有4行,并且您插入2行时,对-numberOfRowsInTableView:的后续调用必须返回6,并且-tableView:cellForRowAtIndexPath:方法应返回两个新插入行的新数据。

到目前为止一直很好......

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

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

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

可能的解决方案

我只会在主线程上进行更新!因此,您可以创建一个名为-addItems:的方法,该方法将新行添加到tableview,并且可以从任何线程调用。如果你这样做,将不会有性能损失。如果newItems数组的计算成本很高,您仍可以在另一个线程中执行,但实际更新必须在主线程上完成。此外,您可以围绕对originalItems数组的每次访问放置一个synchronized块,以便您可以从其他线程获得只读访问权。

// 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];
}