Fixes out of order blocks running on main queue in ASDataController

ASDataController’s completedNodes is accessed by treating the main thread as a serial queue. The ​*problem*​ is, operations can cut in line by being added while on the main thread. I.e. it just calls the block instead of dispatch_async’ing to the main thread. This results in data inconsistency.

To address this, I've added a queue of operations which get run serially (not to be confused with a serial dispatch queue). Instead of running blocks directly on the main thread, if it’s called while not on the main thread, it dispatch_asyncs to the main thread and runs any blocks in the queue. If it ​*is*​ on the main thread, it runs any blocks already in the queue and then runs the block.
This commit is contained in:
Garrett Moon
2015-12-10 19:53:51 -08:00
parent 2d9064dd1e
commit f94229796c

View File

@@ -16,6 +16,7 @@
#import "ASMultidimensionalArrayUtils.h" #import "ASMultidimensionalArrayUtils.h"
#import "ASInternalHelpers.h" #import "ASInternalHelpers.h"
#import "ASLayout.h" #import "ASLayout.h"
#import "ASThread.h"
//#define LOG(...) NSLog(__VA_ARGS__) //#define LOG(...) NSLog(__VA_ARGS__)
#define LOG(...) #define LOG(...)
@@ -26,12 +27,22 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind";
static void *kASSizingQueueContext = &kASSizingQueueContext; static void *kASSizingQueueContext = &kASSizingQueueContext;
@interface ASMainQueueSerialQueue : NSObject
{
ASDN::Mutex _serialQueueLock;
NSMutableArray *_blocks;
}
- (void)performBlockOnMainThread:(dispatch_block_t)block;
@end
@interface ASDataController () { @interface ASDataController () {
NSMutableArray *_externalCompletedNodes; // Main thread only. External data access can immediately query this if available. NSMutableArray *_externalCompletedNodes; // Main thread only. External data access can immediately query this if available.
NSMutableDictionary *_completedNodes; // Main thread only. External data access can immediately query this if _externalCompletedNodes is unavailable. NSMutableDictionary *_completedNodes; // Main thread only. External data access can immediately query this if _externalCompletedNodes is unavailable.
NSMutableDictionary *_editingNodes; // Modified on _editingTransactionQueue only. Updates propogated to _completedNodes. NSMutableDictionary *_editingNodes; // Modified on _editingTransactionQueue only. Updates propogated to _completedNodes.
ASMainQueueSerialQueue *_mainSerialQueue;
NSMutableArray *_pendingEditCommandBlocks; // To be run on the main thread. Handles begin/endUpdates tracking. NSMutableArray *_pendingEditCommandBlocks; // To be run on the main thread. Handles begin/endUpdates tracking.
NSOperationQueue *_editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes. NSOperationQueue *_editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes.
@@ -63,6 +74,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
_completedNodes[ASDataControllerRowNodeKind] = [NSMutableArray array]; _completedNodes[ASDataControllerRowNodeKind] = [NSMutableArray array];
_editingNodes[ASDataControllerRowNodeKind] = [NSMutableArray array]; _editingNodes[ASDataControllerRowNodeKind] = [NSMutableArray array];
_mainSerialQueue = [[ASMainQueueSerialQueue alloc] init];
_pendingEditCommandBlocks = [NSMutableArray array]; _pendingEditCommandBlocks = [NSMutableArray array];
_editingTransactionQueue = [[NSOperationQueue alloc] init]; _editingTransactionQueue = [[NSOperationQueue alloc] init];
@@ -208,12 +221,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
// Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads.
NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(editingNodes); NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(editingNodes);
ASPerformBlockOnMainThread(^{ [_mainSerialQueue performBlockOnMainThread:^{
_completedNodes[kind] = completedNodes; _completedNodes[kind] = completedNodes;
if (completionBlock) { if (completionBlock) {
completionBlock(nodes, indexPaths); completionBlock(nodes, indexPaths);
} }
}); }];
} }
- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock - (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock
@@ -227,13 +240,13 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
ASDeleteElementsInMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths); ASDeleteElementsInMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths);
_editingNodes[kind] = editingNodes; _editingNodes[kind] = editingNodes;
ASPerformBlockOnMainThread(^{ [_mainSerialQueue performBlockOnMainThread:^{
NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths);
ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths);
if (completionBlock) { if (completionBlock) {
completionBlock(nodes, indexPaths); completionBlock(nodes, indexPaths);
} }
}); }];
} }
- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock - (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock
@@ -250,12 +263,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
// Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads.
NSArray *sectionsForCompleted = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(sections); NSArray *sectionsForCompleted = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(sections);
ASPerformBlockOnMainThread(^{ [_mainSerialQueue performBlockOnMainThread:^{
[_completedNodes[kind] insertObjects:sectionsForCompleted atIndexes:indexSet]; [_completedNodes[kind] insertObjects:sectionsForCompleted atIndexes:indexSet];
if (completionBlock) { if (completionBlock) {
completionBlock(sections, indexSet); completionBlock(sections, indexSet);
} }
}); }];
} }
- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock - (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock
@@ -263,12 +276,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
if (indexSet.count == 0) if (indexSet.count == 0)
return; return;
[_editingNodes[kind] removeObjectsAtIndexes:indexSet]; [_editingNodes[kind] removeObjectsAtIndexes:indexSet];
ASPerformBlockOnMainThread(^{ [_mainSerialQueue performBlockOnMainThread:^{
[_completedNodes[kind] removeObjectsAtIndexes:indexSet]; [_completedNodes[kind] removeObjectsAtIndexes:indexSet];
if (completionBlock) { if (completionBlock) {
completionBlock(indexSet); completionBlock(indexSet);
} }
}); }];
} }
#pragma mark - Internal Data Querying + Editing #pragma mark - Internal Data Querying + Editing
@@ -512,14 +525,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
LOG(@"endUpdatesWithCompletion - beginning"); LOG(@"endUpdatesWithCompletion - beginning");
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
ASPerformBlockOnMainThread(^{ [_mainSerialQueue performBlockOnMainThread:^{
// Deep copy _completedNodes to _externalCompletedNodes. // Deep copy _completedNodes to _externalCompletedNodes.
// Any external queries from now on will be done on _externalCompletedNodes, to guarantee data consistency with the delegate. // Any external queries from now on will be done on _externalCompletedNodes, to guarantee data consistency with the delegate.
_externalCompletedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_completedNodes[ASDataControllerRowNodeKind]); _externalCompletedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_completedNodes[ASDataControllerRowNodeKind]);
LOG(@"endUpdatesWithCompletion - begin updates call to delegate"); LOG(@"endUpdatesWithCompletion - begin updates call to delegate");
[_delegate dataControllerBeginUpdates:self]; [_delegate dataControllerBeginUpdates:self];
}); }];
}]; }];
// Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates. // Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates.
@@ -532,13 +545,13 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[_pendingEditCommandBlocks removeAllObjects]; [_pendingEditCommandBlocks removeAllObjects];
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
ASPerformBlockOnMainThread(^{ [_mainSerialQueue performBlockOnMainThread:^{
// Now that the transaction is done, _completedNodes can be accessed externally again. // Now that the transaction is done, _completedNodes can be accessed externally again.
_externalCompletedNodes = nil; _externalCompletedNodes = nil;
LOG(@"endUpdatesWithCompletion - calling delegate end"); LOG(@"endUpdatesWithCompletion - calling delegate end");
[_delegate dataController:self endUpdatesAnimated:animated completion:completion]; [_delegate dataController:self endUpdatesAnimated:animated completion:completion];
}); }];
}]; }];
} }
} }
@@ -819,11 +832,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
// i.e there might be some nodes that were measured using the old constrained size but haven't been added to _completedNodes // i.e there might be some nodes that were measured using the old constrained size but haven't been added to _completedNodes
// (see _layoutNodes:atIndexPaths:withAnimationOptions:). // (see _layoutNodes:atIndexPaths:withAnimationOptions:).
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
ASPerformBlockOnMainThread(^{ [_mainSerialQueue performBlockOnMainThread:^{
for (NSString *kind in [_completedNodes keyEnumerator]) { for (NSString *kind in [_completedNodes keyEnumerator]) {
[self _relayoutNodesOfKind:kind]; [self _relayoutNodesOfKind:kind];
} }
}); }];
}]; }];
}]; }];
} }
@@ -971,3 +984,47 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
} }
@end @end
@implementation ASMainQueueSerialQueue
- (instancetype)init
{
if (!(self = [super init])) {
return nil;
}
_blocks = [[NSMutableArray alloc] init];
return self;
}
- (void)performBlockOnMainThread:(dispatch_block_t)block
{
ASDN::MutexLocker l(_serialQueueLock);
[_blocks addObject:block];
[self runBlocks];
}
- (void)runBlocks
{
dispatch_block_t mainThread = ^{
ASDN::MutexLocker l(_serialQueueLock);
for (NSUInteger i = 0; i < _blocks.count; i++) {
dispatch_block_t block = [_blocks objectAtIndex:i];
block();
}
[_blocks removeAllObjects];
};
if ([NSThread isMainThread]) {
ASDN::MutexUnlocker u(_serialQueueLock);
mainThread();
} else {
dispatch_async(dispatch_get_main_queue(), ^{
mainThread();
});
}
}
@end