IGListKit 是 Instagram 维护一个 UI 框架,采用面向协议的思想,基于 UICollectionView 实现,由数据驱动的 UI 列表框架。本文基于 IGListKit 源码对其主要设计思想进行分析。
分析前,我们现看一下 IGListKit 中的数据和 UI 对应关系图
可以看出 IGListKit 都是基于 IGListAdapter 进行数据传递和 UI 刷新的操作,接下来从 IGListAdapter 入手分析 IGListKit 具体做了哪些工作。
IGListAdapter 初始化: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 - (instancetype )initWithUpdater:(id <IGListUpdatingDelegate>)updater viewController:(UIViewController *)viewController workingRangeSize:(NSInteger )workingRangeSize { IGAssertMainThread(); IGParameterAssert(updater); if (self = [super init]) { NSPointerFunctions *keyFunctions = [updater objectLookupPointerFunctions]; NSPointerFunctions *valueFunctions = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory ]; NSMapTable *table = [[NSMapTable alloc] initWithKeyPointerFunctions:keyFunctions valuePointerFunctions:valueFunctions capacity:0 ]; _sectionMap = [[IGListSectionMap alloc] initWithMapTable:table]; _displayHandler = [IGListDisplayHandler new]; _workingRangeHandler = [[IGListWorkingRangeHandler alloc] initWithWorkingRangeSize:workingRangeSize]; _updateListeners = [NSHashTable weakObjectsHashTable]; _viewSectionControllerMap = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality | NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory ]; _updater = updater; _viewController = viewController; [IGListDebugger trackAdapter:self ]; } return self ; }
IGListSectionMap : 作用是映射 sectionController 和 collectionView 的 section 的对应关系,能在 O(1) 的时间复杂度根据 section 获取 sectionController。内部实现结果如下图:
1 2 3 4 5 graph LR object -- objectToSectionControllerMap --> IGListSectionController IGListSectionController -- objectToSectionControllerMap --> object IGListSectionController -- sectionControllerToSectionMap --> section section -- sectionControllerToSectionMap --> IGListSectionController
IGListDisplayHandler : 作用和对外暴露的 IGListAdapterPerformanceDelegate 类似,主要是对 UICollectionViewCell 生命周日相关对调的处理(cell 显示/消失/分区头部、尾部显示/消失),内部会把事件传给 IGListSectionController 的 displayDelegate;在 IGListAdapter+UICollectionView.m 文件中进行调用。
IGListWorkingRangeHandler : 负责 collectionView 每个 section(sectionController) 的预加载的准备工作。在 IGListAdapter+UICollectionView.m 文件中进行调用,相关数据会保存起来,提供给 IGListAdapter 使用。
IGListAdapterUpdateListener: 代理集合,IGListAdapter 更新完数据后对集合的代理进行通知
数据源: IGListAdapter 会作为 UICollectionView 默认的 dataSource。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 - (void )setCollectionView:(UICollectionView *)collectionView { if (_collectionView != collectionView || _collectionView.dataSource != self ) { static NSMapTable <UICollectionView *, IGListAdapter *> *globalCollectionViewAdapterMap = nil ; if (globalCollectionViewAdapterMap == nil ) { globalCollectionViewAdapterMap = [NSMapTable weakToWeakObjectsMapTable]; } [globalCollectionViewAdapterMap removeObjectForKey:_collectionView]; [[globalCollectionViewAdapterMap objectForKey:collectionView] setCollectionView:nil ]; [globalCollectionViewAdapterMap setObject:self forKey:collectionView]; _registeredCellIdentifiers = [NSMutableSet new]; _registeredNibNames = [NSMutableSet new]; _registeredSupplementaryViewIdentifiers = [NSMutableSet new]; _registeredSupplementaryViewNibNames = [NSMutableSet new]; const BOOL settingFirstCollectionView = _collectionView == nil ; _collectionView = collectionView; _collectionView.dataSource = self ; if (@available(iOS 10.0 , tvOS 10 , *)) { _collectionView.prefetchingEnabled = NO ; } [_collectionView.collectionViewLayout ig_hijackLayoutInteractiveReorderingMethodForAdapter:self ]; [_collectionView.collectionViewLayout invalidateLayout]; [self _updateCollectionViewDelegate]; if (!IGListExperimentEnabled(self .experiments, IGListExperimentGetCollectionViewAtUpdate) || settingFirstCollectionView) { [self _updateAfterPublicSettingsChange]; } } }
globalCollectionViewAdapterMap: key 为 collectionView,value 为 IGListAdapter
通过 - (void)setCollectionView:(UICollectionView *)collectionView
关联 IGListAdapter 和 UICollectionView:
1. globalCollectionViewAdapterMap 先移除旧的 _collectionView 对应的 IGListAdapter,就是代码中的 self
2. 将新 collectionView 之前绑定的 IGListAdapter 取消对 collectionView 绑定
3. 将新 collectionView 和当前 IGListAdapter 绑定
dataSource 的方法实现再 IGListAdapter+UICollectionView.m 中,dataSource 的代理方法通过 IGSectionController 返回每个 section 对应的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #pragma mark - UICollectionViewDataSource - (NSInteger )numberOfSectionsInCollectionView:(UICollectionView *)collectionView {...} - (NSInteger )collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger )section {...} - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {...} - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {...} - (BOOL )collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath { const NSInteger sectionIndex = indexPath.section; const NSInteger itemIndex = indexPath.item; IGListSectionController *sectionController = [self sectionControllerForSection:sectionIndex]; return [sectionController canMoveItemAtIndex:itemIndex]; } - (void )collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {...}
数据源更新 <IGListUpdatingDelegate>
: IGListAdapter 提供以下几种方法让外部进行数据更新:
1 2 3 4 5 - (void )performUpdatesAnimated:(BOOL )animated completion:(nullable IGListUpdaterCompletion)completion; - (void )reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion; - (void )reloadObjects:(NSArray *)objects;
我们先以 -reloadDataWithCompletion:
方法为例子,分析数据更新的过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 - (void )reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion { IGAssertMainThread(); id <IGListAdapterDataSource> dataSource = self .dataSource; UICollectionView *collectionView = self .collectionView; if (dataSource == nil || collectionView == nil ) { IGLKLog(@"Warning: Your call to %s is ignored as dataSource or collectionView haven't been set." , __PRETTY_FUNCTION__); if (completion) { completion(NO ); } return ; } NSArray *uniqueObjects = objectsWithDuplicateIdentifiersRemoved([dataSource objectsForListAdapter:self ]); __weak __typeof__(self ) weakSelf = self ; [self .updater reloadDataWithCollectionViewBlock:[self _collectionViewBlock] reloadUpdateBlock:^{ [weakSelf.sectionMap reset]; [weakSelf _updateObjects:uniqueObjects dataSource:dataSource]; } completion:^(BOOL finished) { [weakSelf _notifyDidUpdate:IGListAdapterUpdateTypeReloadData animated:NO ]; if (completion) { completion(finished); } }]; }
刷新数据之前,会先将数据去重,保证数据对应的 diffIdentifier 是唯一的。然后调用 IGListAdapterUpdater 的方法进行刷新数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - (void )reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock completion:(nullable IGListUpdatingCompletion)completion { IGAssertMainThread(); IGParameterAssert(collectionViewBlock != nil ); IGParameterAssert(reloadUpdateBlock != nil ); IGListUpdatingCompletion localCompletion = completion; if (localCompletion) { [self .completionBlocks addObject:localCompletion]; } self .reloadUpdates = reloadUpdateBlock; self .queuedReloadData = YES ; [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - (void )_queueUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { IGAssertMainThread(); __weak __typeof__(self ) weakSelf = self ; dispatch_async (dispatch_get_main_queue(), ^{ if (weakSelf.state != IGListBatchUpdateStateIdle || ![weakSelf hasChanges]) { return ; } if (weakSelf.hasQueuedReloadData) { [weakSelf performReloadDataWithCollectionViewBlock:collectionViewBlock]; } else { [weakSelf performBatchUpdatesWithCollectionViewBlock:collectionViewBlock]; } }); }
之后进入条件判断执行 -performReloadDataWithCollectionViewBlock:
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 - (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { IGAssertMainThread(); id<IGListAdapterUpdaterDelegate> delegate = self.delegate; void (^reloadUpdates)(void) = self.reloadUpdates; IGListBatchUpdates *batchUpdates = self.batchUpdates; NSMutableArray *completionBlocks = [self.completionBlocks mutableCopy]; // 清空相关状态 [self cleanStateBeforeUpdates]; void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) { for (IGListUpdatingCompletion block in completionBlocks) { block(finished); } self.state = IGListBatchUpdateStateIdle; }; // 防止 collectionView 被释放导致崩溃 UICollectionView *collectionView = collectionViewBlock(); if (collectionView == nil) { [self _cleanStateAfterUpdates]; executeCompletionBlocks(NO); return; } // 更新状态,避免更新数据的过程中去通知视图更新 self.state = IGListBatchUpdateStateExecutingBatchUpdateBlock; // 通知外部移除所有 section controllers,然后重新生成 if (reloadUpdates) { reloadUpdates(); } // 即使我们只是调用reloadData,也要执行所有存储的 batchUpdates 任务 // 实际效果所有 section 视图的突变将被丢弃,建议使用者也将其实际的数据更新也放入 batchUpdates 任务集合中,因此,如果我们不执行该块,则 batchUpdates 是不会被触发 for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) { itemUpdateBlock(); } // add any completion blocks from item updates. added after item blocks are executed in order to capture any // re-entrant updates [completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks]; self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock; [self _cleanStateAfterUpdates]; [delegate listAdapterUpdater:self willReloadDataWithCollectionView:collectionView]; [collectionView reloadData]; [collectionView.collectionViewLayout invalidateLayout]; [collectionView layoutIfNeeded]; [delegate listAdapterUpdater:self didReloadDataWithCollectionView:collectionView]; executeCompletionBlocks(YES); }
-performReloadDataWithCollectionViewBlock:
中也会触发保存在 batchUpdates 中的更新任务,以便及时刷新数据/界面,然后通过代理通知外部 UICollectionView 刷新的前后事件。
可以看出 -reloadDataWithCompletion:
基本等同于强制刷新,会把所有刷新任务全部执行完之后,通知 UICollectionView 刷新界面。
与 -reloadDataWithCompletion:
不同的是,IGListAdapter 还有提供另外一个方法进行数据刷新 - (void)performUpdatesAnimated:completion:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 - (void )performUpdatesAnimated:(BOOL )animated completion:(IGListUpdaterCompletion)completion { [self _enterBatchUpdates]; [self .updater performUpdateWithCollectionViewBlock:[self _collectionViewBlock] fromObjects:fromObjects toObjectsBlock:toObjectsBlock animated:animated objectTransitionBlock:^(NSArray *toObjects) { weakSelf.previousSectionMap = [weakSelf.sectionMap copy [weakSelf _updateObjects:toObjects dataSource:dataSource]; } completion:^(BOOL finished) { weakSelf.previousSectionMap = nil ; [weakSelf _notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated]; if (completion) { completion(finished); } [weakSelf _exitBatchUpdates]; }]; }
updater 会将更新数据 sectionMap 的操作保存到 objectTransitionBlock 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 - (void )performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock fromObjects:(NSArray *)fromObjects toObjectsBlock:(IGListToObjectBlock)toObjectsBlock animated:(BOOL )animated objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock completion:(IGListUpdatingCompletion)completion { IGAssertMainThread(); IGParameterAssert(collectionViewBlock != nil ); IGParameterAssert(objectTransitionBlock != nil ); self .fromObjects = self .fromObjects ?: self .pendingTransitionToObjects ?: fromObjects; self .toObjectsBlock = toObjectsBlock; self .queuedUpdateIsAnimated = self .queuedUpdateIsAnimated && animated; self .objectTransitionBlock = objectTransitionBlock; IGListUpdatingCompletion localCompletion = completion; if (localCompletion) { [self .completionBlocks addObject:localCompletion]; } [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; }
IGListUpdater 处理完传入的 fromObjects 和 toObjects,并保存数据转化的闭包 objectTransitionBlock,会调用 -_queueUpdateWithCollectionViewBlock:
方法,利用 dispatch_async 异步调用 -performBatchUpdatesWithCollectionViewBlock:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 - (void )performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { IGAssertMainThread(); IGAssert(self .state == IGListBatchUpdateStateIdle, @"Should not call batch updates when state isn't idle" ); id <IGListAdapterUpdaterDelegate> delegate = self .delegate; NSArray *fromObjects = [self .fromObjects copy ]; IGListToObjectBlock toObjectsBlock = [self .toObjectsBlock copy ]; NSMutableArray *completionBlocks = [self .completionBlocks mutableCopy]; void (^objectTransitionBlock)(NSArray *) = [self .objectTransitionBlock copy ]; const BOOL animated = self .queuedUpdateIsAnimated; IGListBatchUpdates *batchUpdates = self .batchUpdates; [self cleanStateBeforeUpdates]; void (^executeCompletionBlocks)(BOOL ) = ^(BOOL finished) { self .applyingUpdateData = nil ; self .state = IGListBatchUpdateStateIdle; for (IGListUpdatingCompletion block in completionBlocks) { block(finished); } }; UICollectionView *collectionView = collectionViewBlock(); if (collectionView == nil ) { [self _cleanStateAfterUpdates]; executeCompletionBlocks(NO ); return ; } NSArray *toObjects = nil ; if (toObjectsBlock != nil ) { toObjects = objectsWithDuplicateIdentifiersRemoved(toObjectsBlock()); } void (^executeUpdateBlocks)(void ) = ^{ self .state = IGListBatchUpdateStateExecutingBatchUpdateBlock; if (objectTransitionBlock != nil ) { objectTransitionBlock(toObjects); } for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) { itemUpdateBlock(); } [completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks]; self .state = IGListBatchUpdateStateExecutedBatchUpdateBlock; }; void (^reloadDataFallback)(void ) = ^{ executeUpdateBlocks(); [self _cleanStateAfterUpdates]; [self _performBatchUpdatesItemBlockApplied]; [collectionView reloadData]; [collectionView layoutIfNeeded]; executeCompletionBlocks(YES ); }; const BOOL iOS83OrLater = (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_8_3 ); if (iOS83OrLater && self .allowsBackgroundReloading && collectionView.window == nil ) { [self _beginPerformBatchUpdatesToObjects:toObjects]; reloadDataFallback(); return ; } [self _beginPerformBatchUpdatesToObjects:toObjects]; const IGListExperiment experiments = self .experiments; IGListIndexSetResult *(^performDiff)(void ) = ^{ return IGListDiffExperiment(fromObjects, toObjects, IGListDiffEquality, experiments); }; void (^batchUpdatesBlock)(IGListIndexSetResult *result) = ^(IGListIndexSetResult *result){ executeUpdateBlocks(); self .applyingUpdateData = [self _flushCollectionView:collectionView withDiffResult:result batchUpdates:self .batchUpdates fromObjects:fromObjects]; [self _cleanStateAfterUpdates]; [self _performBatchUpdatesItemBlockApplied]; }; void (^batchUpdatesCompletionBlock)(BOOL ) = ^(BOOL finished) { IGListBatchUpdateData *oldApplyingUpdateData = self .applyingUpdateData; executeCompletionBlocks(finished); [delegate listAdapterUpdater:self didPerformBatchUpdates:oldApplyingUpdateData collectionView:collectionView]; [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; }; void (^performUpdate)(IGListIndexSetResult *) = ^(IGListIndexSetResult *result){ [collectionView layoutIfNeeded]; @try { [delegate listAdapterUpdater:self willPerformBatchUpdatesWithCollectionView:collectionView fromObjects:fromObjects toObjects:toObjects listIndexSetResult:result]; if (collectionView.dataSource == nil ) { batchUpdatesCompletionBlock(NO ); } else if (result.changeCount > 100 && IGListExperimentEnabled(experiments, IGListExperimentReloadDataFallback)) { reloadDataFallback(); } else if (animated) { [collectionView performBatchUpdates:^{ batchUpdatesBlock(result); } completion:batchUpdatesCompletionBlock]; } else { [CATransaction begin]; [CATransaction setDisableActions:YES ]; [collectionView performBatchUpdates:^{ batchUpdatesBlock(result); } completion:^(BOOL finished) { [CATransaction commit]; batchUpdatesCompletionBlock(finished); }]; } } @catch (NSException *exception) { [delegate listAdapterUpdater:self collectionView:collectionView willCrashWithException:exception fromObjects:fromObjects toObjects:toObjects diffResult:result updates:(id )self .applyingUpdateData]; @throw exception; } }; if (IGListExperimentEnabled(experiments, IGListExperimentBackgroundDiffing)) { dispatch_async (dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0 ), ^{ IGListIndexSetResult *result = performDiff(); dispatch_async (dispatch_get_main_queue(), ^{ performUpdate(result); }); }); } else { IGListIndexSetResult *result = performDiff(); performUpdate(result); } }
该数据更新过程调用链大概是:
1 2 3 4 |---performUpdatesAnimated:completion: |---performUpdateWithCollectionViewBlock:fromObjects:toObjectsBlock:animated:objectTransitionBlock:completion: |---_queueUpdateWithCollectionViewBlock: |---performBatchUpdatesWithCollectionViewBlock:
整个 performUpdates 的大部分逻辑都是由 IGListUpdater 完成,重中之重都几种放 -performBatchUpdatesWithCollectionViewBlock:
方法:
1. 判断 collectionView 是否在显示,若不在屏幕窗口上显示,直接全量刷新数据和视图;反之继续步骤2
2. 子线程调用 IGListDiffExperiment,计算数据的差分变化,计算完毕之后在主线程触发界面刷新逻辑
3. 通过代理对外通知即将进行 batch update 批量更新
4. 如果 collectionView 的 dataSource 为 nil,结束更新过程;反之继续
5. 差分变化的数据个数超过100,直接调用 reloadData 全量刷新数据/视图;若变化数据小于100,则调用 `-[UICollectionView performBatchUpdates:completion:]` 批量刷新数据/视图,刷新过程中会调用 `-_flushCollectionView:withDiffResult:batchUpdates:fromObjects:` 将数据源提供的数据和 diff 结果包装成批量更新的数据类型 IGListBatchUpdateData 以便 UICollectionView 进行读取
IGListAdapter 会作为 collectionView 属性的默认代理
1 2 3 4 5 6 7 @protocol IGListCollectionViewDelegateLayout <UICollectionViewDelegateFlowLayout > @interface IGListAdapter (UICollectionView )< UICollectionViewDataSource ,IGListCollectionViewDelegateLayout >
IGListAdapter 会实现相关代理方法,进行对 cell 级别的视图管理,包含视图 UICollectionView 滚动,cell 大小、cell 显示等事件,并通过 IGListAdapterPerformanceDelegate 对外通知
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 - (CGSize )sizeForItemAtIndexPath:(NSIndexPath *)indexPath { [performanceDelegate listAdapter:self didCallSizeOnSectionController:sectionController atIndex:indexPath.item]; } - (void )scrollViewDidScroll:(UIScrollView *)scrollView { id <IGListAdapterPerformanceDelegate> performanceDelegate = self .performanceDelegate; [performanceDelegate listAdapterWillCallScroll:self ]; [performanceDelegate listAdapter:self didCallScroll:scrollView]; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { id <IGListAdapterPerformanceDelegate> performanceDelegate = self .performanceDelegate; [performanceDelegate listAdapterWillCallDequeueCell:self ]; [performanceDelegate listAdapter:self didCallDequeueCell:cell onSectionController:sectionController atIndex:indexPath.item]; } - (void )collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { id <IGListAdapterPerformanceDelegate> performanceDelegate = self .performanceDelegate; [performanceDelegate listAdapterWillCallDisplayCell:self ]; [performanceDelegate listAdapter:self didCallDisplayCell:cell onSectionController:sectionController atIndex:indexPath.item]; } - (void )collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { id <IGListAdapterPerformanceDelegate> performanceDelegate = self .performanceDelegate; [performanceDelegate listAdapterWillCallEndDisplayCell:self ]; [performanceDelegate listAdapter:self didCallEndDisplayCell:cell onSectionController:sectionController atIndex:indexPath.item]; }
视图交互: cell 的拖动会首先触发 UICollectionView 的代理方法 -collectionView:moveItemAtIndexPath:toIndexPath
。在这个方法中会判断拖动开始/结束位置,根据不同的情况进行数据刷新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 - (void )collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { if (@available(iOS 9.0 , *)) { const NSInteger sourceSectionIndex = sourceIndexPath.section; const NSInteger destinationSectionIndex = destinationIndexPath.section; const NSInteger sourceItemIndex = sourceIndexPath.item; const NSInteger destinationItemIndex = destinationIndexPath.item; IGListSectionController *sourceSectionController = [self sectionControllerForSection:sourceSectionIndex]; IGListSectionController *destinationSectionController = [self sectionControllerForSection:destinationSectionIndex]; if (sourceSectionController == destinationSectionController) { if ([sourceSectionController canMoveItemAtIndex:sourceItemIndex toIndex:destinationItemIndex]) { [self moveInSectionControllerInteractive:sourceSectionController fromIndex:sourceItemIndex toIndex:destinationItemIndex]; } else { [self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; } return ; } if ([sourceSectionController numberOfItems] == 1 && [destinationSectionController numberOfItems] == 1 ) { [self moveSectionControllerInteractive:sourceSectionController fromIndex:sourceSectionIndex toIndex:destinationSectionIndex]; return ; } [self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; } }
成功拖动之后会触发 IGListUpdater 的 -moveInSectionControllerInteractive
或者 -moveSectionControllerInteractive:fromIndex:toIndex
,在同一个 UICollectionView section 中拖动则触发前者,跨 section 之间则后者
1 2 3 4 5 6 - (void )moveInSectionControllerInteractive:(IGListSectionController *)sectionController fromIndex:(NSInteger )fromIndex toIndex:(NSInteger )toIndex NS_AVAILABLE_IOS (9 _0) { [sectionController moveObjectFromIndex:fromIndex toIndex:toIndex]; }
在同一个 section 中拖动 UICollectionViewCell 比较简单,实现中回去调用对应 sectionController 的 -moveObjectFromIndex:toIndex:
,使用者在自定义的 sectionController 中实现该代理方法,进行对应的数据刷新更新即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 - (void )moveSectionControllerInteractive:(IGListSectionController *)sectionController fromIndex:(NSInteger )fromIndex toIndex:(NSInteger )toIndex NS_AVAILABLE_IOS (9 _0) { if (fromIndex != toIndex) { id <IGListAdapterDataSource> dataSource = self .dataSource; NSArray *previousObjects = [self .sectionMap objects]; if (self .isLastInteractiveMoveToLastSectionIndex) { self .isLastInteractiveMoveToLastSectionIndex = NO ; } else if (fromIndex < toIndex) { toIndex -= 1 ; } NSMutableArray *mutObjects = [previousObjects mutableCopy]; id object = [previousObjects objectAtIndex:fromIndex]; [mutObjects removeObjectAtIndex:fromIndex]; [mutObjects insertObject:object atIndex:toIndex]; NSArray *objects = [mutObjects copy ]; [self .moveDelegate listAdapter:self moveObject:object from:previousObjects to:objects]; NSArray <id <IGListDiffable>> *updatedObjects = [dataSource objectsForListAdapter:self ]; [self _updateObjects:updatedObjects dataSource:dataSource]; } [self .updater moveSectionInCollectionView:collectionView fromIndex:fromIndex toIndex:toIndex]; }
跨 UICollectionView section 间拖动 UICollectionViewCell 需要对原始/目标 section 的位置/ item 数目进行相关判断,最后执行 IGListUpdater 的 -moveSectionInCollectionView:fromIndex:toIndex:
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 - (void )moveSectionInCollectionView:(UICollectionView *)collectionView fromIndex:(NSInteger )fromIndex toIndex:(NSInteger )toIndex { [collectionView reloadData]; id <IGListAdapterUpdaterDelegate> delegate = self .delegate; NSMutableIndexSet *visibleSections = [NSMutableIndexSet new]; NSArray *visibleIndexPaths = [collectionView indexPathsForVisibleItems]; for (NSIndexPath *visibleIndexPath in visibleIndexPaths) { [visibleSections addIndex:visibleIndexPath.section]; } [delegate listAdapterUpdater:self willReloadSections:visibleSections collectionView:collectionView]; [CATransaction begin]; [CATransaction setDisableActions:YES ]; [collectionView performBatchUpdates:^{ [collectionView reloadSections:visibleSections]; } completion:^(BOOL finished) { [CATransaction commit]; }]; }
在 -moveSectionInCollectionView:fromIndex:toIndex:
方法会现调用 -[UICollectionView reloadDate]
来规避 origin section item 数目为0的情况,之后还会对应当前屏幕显示区域进行 batch update 来规避 UICollectionView 不能及时刷新的 bug。
整个 UICollectionViewCell 拖动的调用栈大概为:
1 2 3 4 5 6 7 |---collectionView:moveItemAtIndexPath:toIndexPath: |---moveInSectionControllerInteractive:fromIndex:toIndex: # section 内拖动 |---moveObjectFromIndex:toIndex: |---moveSectionControllerInteractive:fromIndex:toIndex: # section 间拖动 |---_updateObjects:dataSource |---moveSectionInCollectionView:fromIndex:toIndex # updater |---performBatchUpdates:completion: # UICollectionView
总结来说,整个 IGListKit 结构可以用下图来概括:
可以看出来,IGListAdapter 负责不同功能的属性都是通过面向协议来进行开发,不同的功能模块粒度都比较小,避免模块之间的循环依赖,实现数据跟视图的有效解耦。
不仅如此,IGListKit 通过 IGListDiffable 协议加上 diff 算法,对外隐藏数据更新的细节,用户只需关注业务数据,减轻了数据更新的操作。
其他 IGListKit 中还用到一些平时没有注意到的特性
NSCountedSet
插入 NSCountedSet 对象的每个不同的对象都有一个与之相关的计数器,同一个对象每加入一次 NSCountedSet 集合中,对应的 count 就会加1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 - (void )_willDisplayReusableView:(UICollectionReusableView *)view forListAdapter:(IGListAdapter *)listAdapter sectionController:(IGListSectionController *)sectionController object:(id )object indexPath:(NSIndexPath *)indexPath { IGParameterAssert(view != nil ); IGParameterAssert(listAdapter != nil ); IGParameterAssert(object != nil ); IGParameterAssert(indexPath != nil ); [self .visibleViewObjectMap setObject:object forKey:view]; NSCountedSet *visibleListSections = self .visibleListSections; if ([visibleListSections countForObject:sectionController] == 0 ) { [sectionController.displayDelegate listAdapter:listAdapter willDisplaySectionController:sectionController]; [listAdapter.delegate listAdapter:listAdapter willDisplayObject:object atIndex:indexPath.section]; } [visibleListSections addObject:sectionController]; }
IGListKit 中的 IGListDisplayHandler 利用 NSCountedSet 记录 UICollectionView section 的显示状态,旨在通知外部每个 section 的显示/消失事件。
prefetchingEnabled
当调用 collectionView:didEndDisplayingCell:forItemAtIndexPath:
后,cell 不会立刻进入复用队列,系统会keeps it around for a bit 。相当于会缓存该 cell 一小段时间,在这段时间内如果该 cell 再次回到屏幕中,便不会重新调用 cellForItemAtIndexPath:
,而是直接显示。
至于系统会缓存多久,官方并没有给出明确的时间,感觉跟程序运行时开销有关。
如果想关闭该功能,需要设置 collectionView.prefetchingEnabled = NO;
。
UICollectionViewLayoutInvalidationContext
当改变 UICollectionView item 的时候,通过调用 -invalidateLayout
方法让 UICollectionView 布局失效,通过 Invalidation Context 声明了在布局失效时布局的哪些部分需要被更新,布局对象就可以根据该信息减小重新计算的数据量。
IGListKit 提供了自定义的 IGListCollectionViewLayout 类来优化 UICollectionView 的刷新,IGListCollectionViewLayout 实现和 UICollectionViewLayoutInvalidationContext 相关的方法
1 2 3 4 5 @interface IGListCollectionViewLayoutInvalidationContext : UICollectionViewLayoutInvalidationContext @property (nonatomic , assign ) BOOL ig_invalidateSupplementaryAttributes;@property (nonatomic , assign ) BOOL ig_invalidateAllAttributes;@end
IGListCollectionViewLayoutInvalidationContext 类继承了 UICollectionViewLayoutInvalidationContext,用于记录刷新布局相关逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 - (UICollectionViewLayoutInvalidationContext *)invalidationContextForBoundsChange:(CGRect )newBounds { const CGRect oldBounds = self .collectionView.bounds; IGListCollectionViewLayoutInvalidationContext *context = (IGListCollectionViewLayoutInvalidationContext *)[super invalidationContextForBoundsChange:newBounds]; context.ig_invalidateSupplementaryAttributes = YES ; if (!CGSizeEqualToSize (oldBounds.size, newBounds.size)) { context.ig_invalidateAllAttributes = YES ; } return context; }
-invalidationContextForBoundsChange:
当 UICollectionView 发生变化的时候(比如视图 frame 发生改变),在进行视图刷新之前,会触发该方法返回 UICollectionViewLayoutInvalidationContext 对象来告诉UICollectionView 布局刷新的相关信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 - (void )invalidateLayoutWithContext:(IGListCollectionViewLayoutInvalidationContext *)context { BOOL hasInvalidatedItemIndexPaths = NO ; if ([context respondsToSelector:@selector (invalidatedItemIndexPaths)]) { hasInvalidatedItemIndexPaths = [context invalidatedItemIndexPaths].count > 0 ; } if (hasInvalidatedItemIndexPaths || [context invalidateEverything] || context.ig_invalidateAllAttributes) { _minimumInvalidatedSection = 0 ; } else if ([context invalidateDataSourceCounts] && _minimumInvalidatedSection == NSNotFound ) { _minimumInvalidatedSection = 0 ; } if (context.ig_invalidateSupplementaryAttributes) { [self _resetSupplementaryAttributesCache]; } [super invalidateLayoutWithContext:context]; }
-invalidateLayoutWithContext:
方法在 UICollectionView 布局信息发生变化会被系统调用,IGListCollectionViewLayout 实现了该方法,在调用的过程中会对一些布局缓存进行更新(主要是缓存 UICollectionViewLayoutAttributes 对象),具体细节不再展开。
除此之外,UICollectionViewLayoutInvalidationContext 本身提供了几个方法,用户可以主动调用来进行局部 UI 刷新
1 2 3 4 5 6 7 8 9 - (void )invalidateItemsAtIndexPaths:(NSArray <NSIndexPath *> *)indexPaths API_AVAILABLE(ios(8.0 )); - (void )invalidateSupplementaryElementsOfKind:(NSString *)elementKind atIndexPaths:(NSArray <NSIndexPath *> *)indexPaths API_AVAILABLE(ios(8.0 )); - (void )invalidateDecorationElementsOfKind:(NSString *)elementKind atIndexPaths:(NSArray <NSIndexPath *> *)indexPaths API_AVAILABLE(ios(8.0 ));