@@ -129,7 +129,16 @@ - (void)setLayoutDelegate:(id<ASDataControllerLayoutDelegate>)layoutDelegate
129129
130130#pragma mark - Cell Layout
131131
132+ /* *
133+ * Allocates and layouts nodes from the given collection elements, and blocks the current thread while doing so.
134+ *
135+ * @param elements The elements from which nodes can be allocated and laid out.
136+ * @param strictlyOnCurrentThread Whether or not all the work must be done strictly on the current thread.
137+ * YES means all nodes will be allocated and laid out serially on the current thread.
138+ * NO means the work can be offloaded to other thread(s), potentially reduce the blocking time on the calling thread.
139+ */
132140- (void )_allocateNodesFromElements : (NSArray <ASCollectionElement *> *)elements
141+ strictlyOnCurrentThread : (BOOL )strictlyOnCurrentThread
133142{
134143 NSUInteger nodeCount = elements.count ;
135144 __weak id <ASDataControllerSource> weakDataSource = _dataSource;
@@ -142,12 +151,7 @@ - (void)_allocateNodesFromElements:(NSArray<ASCollectionElement *> *)elements
142151 {
143152 as_activity_create_for_scope (" Data controller batch" );
144153
145- dispatch_queue_t queue = dispatch_get_global_queue (QOS_CLASS_USER_INITIATED , 0 );
146- NSUInteger threadCount = 0 ;
147- if ([_dataSource dataControllerShouldSerializeNodeCreation: self ]) {
148- threadCount = 1 ;
149- }
150- ASDispatchApply (nodeCount, queue, threadCount, ^(size_t i) {
154+ void (^work)(size_t ) = ^(size_t i) {
151155 __strong id <ASDataControllerSource> strongDataSource = weakDataSource;
152156 if (strongDataSource == nil ) {
153157 return ;
@@ -166,7 +170,20 @@ - (void)_allocateNodesFromElements:(NSArray<ASCollectionElement *> *)elements
166170 if (ASSizeRangeHasSignificantArea (sizeRange)) {
167171 [self _layoutNode: node withConstrainedSize: sizeRange];
168172 }
169- });
173+ };
174+
175+ if (strictlyOnCurrentThread) {
176+ for (NSUInteger i = 0 ; i < nodeCount; i++) {
177+ work (i);
178+ }
179+ } else {
180+ dispatch_queue_t queue = dispatch_get_global_queue (QOS_CLASS_USER_INITIATED , 0 );
181+ NSUInteger threadCount = 0 ;
182+ if ([_dataSource dataControllerShouldSerializeNodeCreation: self ]) {
183+ threadCount = 1 ;
184+ }
185+ ASDispatchApply (nodeCount, queue, threadCount, work);
186+ }
170187 }
171188
172189 ASSignpostEnd (DataControllerBatch, self, " count: %lu" , (unsigned long )nodeCount);
@@ -620,8 +637,11 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet
620637 Class <ASDataControllerLayoutDelegate> layoutDelegateClass = [self .layoutDelegate class ];
621638
622639 // Step 3: Call the layout delegate if possible. Otherwise, allocate and layout all elements
623- dispatch_block_t step3 = ^{
640+ void (^ step3)( BOOL ) = ^( BOOL strictlyOnCurrentThread) {
624641 if (canDelegate) {
642+ // Don't pass strictlyOnCurrentThread to the layout delegate. Instead give it
643+ // total control over its threading behavior, as long as it blocks the
644+ // calling thread while preparing the layout (which is part of the API contract).
625645 [layoutDelegateClass calculateLayoutWithContext: layoutContext];
626646 } else {
627647 const auto elementsToProcess = [[NSMutableArray <ASCollectionElement *> alloc] init ];
@@ -635,21 +655,24 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet
635655 [elementsToProcess addObject: element];
636656 }
637657 }
638- [self _allocateNodesFromElements: elementsToProcess];
658+ [self _allocateNodesFromElements: elementsToProcess
659+ strictlyOnCurrentThread: strictlyOnCurrentThread];
639660 }
640661 };
641662
642663 // Step 3 can be done on the main thread or on _editingTransactionQueue
643664 // depending on an experiment.
644665 BOOL mainThreadOnly = ASActivateExperimentalFeature (ASExperimentalMainThreadOnlyDataController);
645666 if (mainThreadOnly) {
646- // We'll still dispatch to _editingTransactionQueue only to schedule a block
667+ // In main-thread-only mode allocate and layout all nodes serially on the main thread.
668+ //
669+ // After this step, we'll still dispatch to _editingTransactionQueue only to schedule a block
647670 // to _mainSerialQueue to execute next steps. This is not optimized because
648671 // in theory we can skip _editingTransactionQueue entirely, but it's much safer
649672 // because change sets will still flow through the pipeline in pretty the same way
650673 // (main thread -> _editingTransactionQueue -> _mainSerialQueue) and so
651674 // any methods that block on _editingTransactionQueue will still work.
652- step3 ();
675+ step3 (YES );
653676 }
654677
655678 ++_editingTransactionGroupCount;
@@ -658,7 +681,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet
658681 as_activity_scope_enter (as_activity_create (" Prepare nodes for collection update" , AS_ACTIVITY_CURRENT , OS_ACTIVITY_FLAG_DEFAULT ), &preparationScope);
659682
660683 if (!mainThreadOnly) {
661- step3 ();
684+ step3 (NO );
662685 }
663686
664687 // Step 4: Inform the delegate on main thread
0 commit comments