IGListKit 是 Instagram 维护一个 UI 框架,采用面向协议的思想,基于 UICollectionView 实现,由数据驱动的 UI 列表框架。本文基于 IGListKit 源码对其主要设计思想进行分析。
分析前,我们现看一下 IGListKit 的数据和 UI 对应关系图
可以看出 IGListKit 都是基于 IGListAdapter 进行数据传递和 UI 刷新的操作,接下来从 IGListAdapter 入手分析 IGListKit 具体做了哪些工作。
IGListAdapter
初始化:
- (instancetype)initWithUpdater:(id )updater
viewController:(UIViewController *)viewController
workingRangeSize:(NSInteger)workingRangeSize {
IGAssertMainThread();
IGParameterAssert(updater);
if (self = [super init]) {
// objectLookupPointerFunctions 返回 hash 表计算 hash 以及比较 value 是否相同的设置
NSPointerFunctions *keyFunctions = [updater objectLookupPointerFunctions];
NSPointerFunctions *valueFunctions = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory];
// table 是以 object 为 key,sectionController 为 value 的 map
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];
// 将 cell 和 sectionController 映射
_viewSectionControllerMap = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality | NSMapTableStrongMemory
valueOptions:NSMapTableStrongMemory];
_updater = updater;
_viewController = viewController;
[IGListDebugger trackAdapter:self];
}
return self;
}
IGListSectionMap: 作用是映射 sectionController 和 collectionView 的 section 的对应关系,能在 O(1) 的时间复杂度根据 section 获取 sectionController。内部实现结果如下图:
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。
- (void)setCollectionView:(UICollectionView *)collectionView {
if (_collectionView != collectionView || _collectionView.dataSource != self) {
static NSMapTable *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 对应的数据
// IGListAdapter+UICollectionView.m
#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 {...}
数据源更新
:
IGListAdapter 提供以下几种方法让外部进行数据更新:
- (void)performUpdatesAnimated:(BOOL)animated completion:(nullable IGListUpdaterCompletion)completion;
- (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion;
- (void)reloadObjects:(NSArray *)objects;
我们先以 -reloadDataWithCompletion:
方法为例子,分析数据更新的过程:
- (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion {
IGAssertMainThread();
id 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;
}
// 重新读取一次数据源代理方法,数据根据diffIdentifier去重
NSArray *uniqueObjects = objectsWithDuplicateIdentifiersRemoved([dataSource objectsForListAdapter:self]);
__weak __typeof__(self) weakSelf = self;
[self.updater reloadDataWithCollectionViewBlock:[self _collectionViewBlock]
reloadUpdateBlock:^{
// 移除所有 section controllers 以便于重新生成
[weakSelf.sectionMap reset];
// 根据去重后的数据源重新生成 section controller
[weakSelf _updateObjects:uniqueObjects dataSource:dataSource];
} completion:^(BOOL finished) {
[weakSelf _notifyDidUpdate:IGListAdapterUpdateTypeReloadData animated:NO];
if (completion) {
completion(finished);
}
}];
}
刷新数据之前,会先将数据去重,保证数据对应的 diffIdentifier 是唯一的。然后调用 IGListAdapterUpdater 的方法进行刷新数据
- (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];
}
- (void)_queueUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
IGAssertMainThread();
__weak __typeof__(self) weakSelf = self;
// dispatch_async 是为了执行 -performBatchUpdatesWithCollectionViewBlock: 前提供更多时间来完成数据更新处理,减少在主线程上进行差异化的操作
dispatch_async(dispatch_get_main_queue(), ^{
if (weakSelf.state != IGListBatchUpdateStateIdle
|| ![weakSelf hasChanges]) {
return;
}
if (weakSelf.hasQueuedReloadData) {
[weakSelf performReloadDataWithCollectionViewBlock:collectionViewBlock];
} else {
[weakSelf performBatchUpdatesWithCollectionViewBlock:collectionViewBlock];
}
});
}
之后进入条件判断执行 -performReloadDataWithCollectionViewBlock:
方法
- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
IGAssertMainThread();
id 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:
- (void)performUpdatesAnimated:(BOOL)animated completion:(IGListUpdaterCompletion)completion {
// 略...
[self _enterBatchUpdates];
[self.updater performUpdateWithCollectionViewBlock:[self _collectionViewBlock]
fromObjects:fromObjects
toObjectsBlock:toObjectsBlock
animated:animated
objectTransitionBlock:^(NSArray *toObjects) {
// 重新捕获一次 sectionMap,防止同时间有数据被删除
weakSelf.previousSectionMap = [weakSelf.sectionMap copy
// 更新 sectionMap 数据,刷新 collectiView 背景图
[weakSelf _updateObjects:toObjects dataSource:dataSource];
} completion:^(BOOL finished) {
// release the previous items
weakSelf.previousSectionMap = nil;
[weakSelf _notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated];
if (completion) {
completion(finished);
}
[weakSelf _exitBatchUpdates];
}];
}
updater 会将更新数据 sectionMap 的操作保存到 objectTransitionBlock 中
- (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);
// 正在执行更新的过程中,同一时间内可能会有多个其他更新任务加入,
// 执行更新动作的时候,是第一次加入的 fromObject 和 最后加入的 toObjects
// 如果 self.fromObject == nil, 应该有先使用之前加入并且还没有执行的 batch update 任务的终点数据源(toObjects)
// 这样做的目的是使整个数据变化可以串联起来
self.fromObjects = self.fromObjects ?: self.pendingTransitionToObjects ?: fromObjects;
self.toObjectsBlock = toObjectsBlock;
// disabled animations will always take priority
// reset to YES in -cleanupState
self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated;
// 保证每次刷新使用最新的 objectTransitionBlock
self.objectTransitionBlock = objectTransitionBlock;
IGListUpdatingCompletion localCompletion = completion;
if (localCompletion) {
[self.completionBlocks addObject:localCompletion];
}
[self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
}
IGListUpdater 处理完传入的 fromObjects 和 toObjects,并保存数据转化的闭包 objectTransitionBlock,会调用 -_queueUpdateWithCollectionViewBlock:
方法,利用 dispatch_async 异步调用 -performBatchUpdatesWithCollectionViewBlock:
- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
IGAssertMainThread();
IGAssert(self.state == IGListBatchUpdateStateIdle, @"Should not call batch updates when state isn't idle");
// 创建局部变量,以便我们可以立即清除状态,但将这些数据传递到批处理更新任务中
id 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);
}
};
// collectionView 如果被销毁,则结束更新恢复相关状态
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;
// 更新包括 IGListAdapter 的 sectionController 和 objects 的映射关系等数据
// 保证执行刷新前,数据已经是最新的
if (objectTransitionBlock != nil) {
objectTransitionBlock(toObjects);
}
// 触发批量刷新任务的数据更新闭包(包括插入、删除、刷新单个 section 的数据)
// objectTransitionBlock 之后执行是为了保证 section 级别的刷新在 item 级别刷新之前进行
for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) {
itemUpdateBlock();
}
// 收集批量刷新完成的回调,后续所有操作完了之后一并处理
[completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks];
self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock;
};
// 执行全量的数据更新并刷新 UI
void (^reloadDataFallback)(void) = ^{
executeUpdateBlocks();
[self _cleanStateAfterUpdates];
[self _performBatchUpdatesItemBlockApplied];
[collectionView reloadData];
[collectionView layoutIfNeeded];
executeCompletionBlocks(YES);
};
// 如果当前 collection 没有显示,跳过差分/分批刷新
const BOOL iOS83OrLater = (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_8_3);
if (iOS83OrLater && self.allowsBackgroundReloading && collectionView.window == nil) {
[self _beginPerformBatchUpdatesToObjects:toObjects];
reloadDataFallback();
return;
}
// 禁止同时执行多个 -performBatchUpdates:
[self _beginPerformBatchUpdatesToObjects:toObjects];
const IGListExperiment experiments = self.experiments;
// 计算新旧数据源差分部分,算法参考: https://dl.acm.org/citation.cfm?id=359467&dl=ACM&coll=DL
IGListIndexSetResult *(^performDiff)(void) = ^{
return IGListDiffExperiment(fromObjects, toObjects, IGListDiffEquality, experiments);
};
// block executed in the first param block of -[UICollectionView performBatchUpdates:completion:]
void (^batchUpdatesBlock)(IGListIndexSetResult *result) = ^(IGListIndexSetResult *result){
// 更新数据
executeUpdateBlocks();
// 根据整理差分算法结果,过滤相关 section/item 数据,把 item 级别的刷新转换成 section 级别来规避 UICollectionView 的 bug,并调用 collectionView reload/insert/delete/move 操作
self.applyingUpdateData = [self _flushCollectionView:collectionView
withDiffResult:result
batchUpdates:self.batchUpdates
fromObjects:fromObjects];
// 更新相关数据状态, 清空批量更新任务和等待更新的数据
[self _cleanStateAfterUpdates];
[self _performBatchUpdatesItemBlockApplied];
};
// block used as the second param of -[UICollectionView performBatchUpdates:completion:]
void (^batchUpdatesCompletionBlock)(BOOL) = ^(BOOL finished) {
IGListBatchUpdateData *oldApplyingUpdateData = self.applyingUpdateData;
executeCompletionBlocks(finished);
[delegate listAdapterUpdater:self didPerformBatchUpdates:oldApplyingUpdateData collectionView:collectionView];
// queue another update in case something changed during batch updates. this method will bail next runloop if
// there are no changes
// 如果 batch update 任务执行的过程中尤其比那话,则异步在下一个 runloop 周期执行相关更新动作
[self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
};
// block that executes the batch update and exception handling
void (^performUpdate)(IGListIndexSetResult *) = ^(IGListIndexSetResult *result){
[collectionView layoutIfNeeded];
@try {
// 对外通知即将进行 batch update
[delegate listAdapterUpdater:self
willPerformBatchUpdatesWithCollectionView:collectionView
fromObjects:fromObjects
toObjects:toObjects
listIndexSetResult:result];
if (collectionView.dataSource == nil) {
// 如果数据源为空则不再刷新的 UICollectionview
batchUpdatesCompletionBlock(NO);
} else if (result.changeCount > 100 && IGListExperimentEnabled(experiments, IGListExperimentReloadDataFallback)) {
// 差分变化数量超过100,进行全量刷新
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(), ^{
//根据差分结果刷新 UICollectionView
performUpdate(result);
});
});
} else {
IGListIndexSetResult *result = performDiff();
performUpdate(result);
}
}
该数据更新过程调用链大概是:
|---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 属性的默认代理
@protocol IGListCollectionViewDelegateLayout
@interface IGListAdapter (UICollectionView)
<
UICollectionViewDataSource,
IGListCollectionViewDelegateLayout
>
IGListAdapter 会实现相关代理方法,进行对 cell 级别的视图管理,包含视图 UICollectionView 滚动,cell 大小、cell 显示等事件,并通过 IGListAdapterPerformanceDelegate 对外通知
- (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
//...略
[performanceDelegate listAdapter:self didCallSizeOnSectionController:sectionController atIndex:indexPath.item];
//...略
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
id performanceDelegate = self.performanceDelegate;
[performanceDelegate listAdapterWillCallScroll:self];
//...略
[performanceDelegate listAdapter:self didCallScroll:scrollView];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
id 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 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 performanceDelegate = self.performanceDelegate;
[performanceDelegate listAdapterWillCallEndDisplayCell:self];
// ...略
[performanceDelegate listAdapter:self didCallEndDisplayCell:cell onSectionController:sectionController atIndex:indexPath.item];
}
视图交互:
cell 的拖动会首先触发 UICollectionView 的代理方法 -collectionView:moveItemAtIndexPath:toIndexPath
。在这个方法中会判断拖动开始/结束位置,根据不同的情况进行数据刷新
- (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]) {
// 同一个 section 内的挪动
[self moveInSectionControllerInteractive:sourceSectionController
fromIndex:sourceItemIndex
toIndex:destinationItemIndex];
} else {
// 撤销修改
[self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
}
return;
}
// 跨 section 移动, 如果 section 的 item 数目为1
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 之间则后者
- (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 中实现该代理方法,进行对应的数据刷新更新即可
- (void)moveSectionControllerInteractive:(IGListSectionController *)sectionController
fromIndex:(NSInteger)fromIndex
toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0) {
// ... 略
if (fromIndex != toIndex) {
id dataSource = self.dataSource;
NSArray *previousObjects = [self.sectionMap objects];
if (self.isLastInteractiveMoveToLastSectionIndex) {
// 如果 item 是被移动到 UICollectionView 最底部
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];
// inform the data source to update its model
[self.moveDelegate listAdapter:self moveObject:object from:previousObjects to:objects];
// update our model based on that provided by the data source
NSArray> *updatedObjects = [dataSource objectsForListAdapter:self];
[self _updateObjects:updatedObjects dataSource:dataSource];
}
// 刷新 UI
// 这里 from index 和 to index 可能是相同的, 但是实际上可能是以 section 的方式向上/下移动了一个 section
[self.updater moveSectionInCollectionView:collectionView fromIndex:fromIndex toIndex:toIndex];
}
跨 UICollectionView section 间拖动 UICollectionViewCell 需要对原始/目标 section 的位置/ item 数目进行相关判断,最后执行 IGListUpdater 的 -moveSectionInCollectionView:fromIndex:toIndex:
方法
- (void)moveSectionInCollectionView:(UICollectionView *)collectionView
fromIndex:(NSInteger)fromIndex
toIndex:(NSInteger)toIndex {
// iOS 移动是以 item 为移动单位的拖动
// 如果 originating section 中的 item 数量是1,将这个 item 拖动到 item 数目同样为1的 target section
// 拖动之后 target section 的 item 数目为2, originating section 的数目为 0
// 基于这种情况必须使用 reloadData
[collectionView reloadData];
// 似乎在 UICollectionVie 的 -moveItemAtIndexPath 代理方法调用期间调用的 -reloadData 不会按预期重新加载所有单元格,
// 因此,这里进一步重新加载了所有可见部分,以确保没有任何 item 上的数据与 dataSource 不同步。
id 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];
// prevent double-animation from reloadData + reloadSections
[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 拖动的调用栈大概为:
|---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
- (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 相关的方法
@interface IGListCollectionViewLayoutInvalidationContext : UICollectionViewLayoutInvalidationContext
// 追加视图
@property (nonatomic, assign) BOOL ig_invalidateSupplementaryAttributes;
@property (nonatomic, assign) BOOL ig_invalidateAllAttributes;
@end
IGListCollectionViewLayoutInvalidationContext 类继承了 UICollectionViewLayoutInvalidationContext,用于记录刷新布局相关逻辑
// -[UICollectionView setFrame:] / -[UICollectionView setBounds:] 会触发
- (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)) {
// size 改变之后,必须进行全量刷新
context.ig_invalidateAllAttributes = YES;
}
return context;
}
-invalidationContextForBoundsChange:
当 UICollectionView 发生变化的时候(比如视图 frame 发生改变),在进行视图刷新之前,会触发该方法返回 UICollectionViewLayoutInvalidationContext 对象来告诉UICollectionView 布局刷新的相关信息。
// 根据 context 中的信息重新计算布局改变的部分。
// -[UICollectionView setDataSource:] / -[UICollectionView setFrame:] 会触发该方法
// 也可以主动调用,强制刷新
- (void)invalidateLayoutWithContext:(IGListCollectionViewLayoutInvalidationContext *)context {
BOOL hasInvalidatedItemIndexPaths = NO;
if ([context respondsToSelector:@selector(invalidatedItemIndexPaths)]) {
hasInvalidatedItemIndexPaths = [context invalidatedItemIndexPaths].count > 0;
}
// _minimumInvalidatedSection 用来记录指定从哪个 section 开始的布局失效,需要重新布局
if (hasInvalidatedItemIndexPaths
|| [context invalidateEverything]
|| context.ig_invalidateAllAttributes) {
// invalidates all
_minimumInvalidatedSection = 0;
} else if ([context invalidateDataSourceCounts] && _minimumInvalidatedSection == NSNotFound) {
// invalidateDataSourceCounts 标记 layout 需要重新从 UICollectionView 查询 section 和 item 数目
// UICollectionView 调用 -reloadData 或者插入/删除 item 的时候 invalidateDataSourceCounts = YES
// 如果 layout 需要重新 UICollectionView 的信息或者没有找到重新刷新的 section 启动,则刷新起点 section 默认为0
_minimumInvalidatedSection = 0;
}
if (context.ig_invalidateSupplementaryAttributes) {
// 清空追加视图的布局信息缓存
[self _resetSupplementaryAttributesCache];
}
[super invalidateLayoutWithContext:context];
}
-invalidateLayoutWithContext:
方法在 UICollectionView 布局信息发生变化会被系统调用,IGListCollectionViewLayout 实现了该方法,在调用的过程中会对一些布局缓存进行更新(主要是缓存 UICollectionViewLayoutAttributes 对象),具体细节不再展开。
除此之外,UICollectionViewLayoutInvalidationContext 本身提供了几个方法,用户可以主动调用来进行局部 UI 刷新
// 调用此方法以标识布局中需要更新的特定单元格。
// 指定的更新的所有 indexPath 对象将添加到属性 invalidatedItemIndexPaths 中。
- (void)invalidateItemsAtIndexPaths:(NSArray *)indexPaths API_AVAILABLE(ios(8.0));
// 重新计算一个或者多个追加视图的布局
- (void)invalidateSupplementaryElementsOfKind:(NSString *)elementKind atIndexPaths:(NSArray *)indexPaths API_AVAILABLE(ios(8.0));
// 重新计算一个或者多个装饰视图的布局
- (void)invalidateDecorationElementsOfKind:(NSString *)elementKind atIndexPaths:(NSArray *)indexPaths API_AVAILABLE(ios(8.0));