mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 06:35:51 +00:00
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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user