IGListKit 是 Instagram 維護(hù)一個(gè) UI 框架茅逮,采用面向協(xié)議的思想,基于 UICollectionView 實(shí)現(xiàn)判哥,由數(shù)據(jù)驅(qū)動(dòng)的 UI 列表框架献雅。本文基于 IGListKit 源碼對(duì)其主要設(shè)計(jì)思想進(jìn)行分析。
分析前塌计,我們現(xiàn)看一下 IGListKit 的數(shù)據(jù)和 UI 對(duì)應(yīng)關(guān)系圖
可以看出 IGListKit 都是基于 IGListAdapter 進(jìn)行數(shù)據(jù)傳遞和 UI 刷新的操作挺身,接下來從 IGListAdapter 入手分析 IGListKit 具體做了哪些工作。
IGListAdapter
初始化:
- (instancetype)initWithUpdater:(id <IGListUpdatingDelegate>)updater
viewController:(UIViewController *)viewController
workingRangeSize:(NSInteger)workingRangeSize {
IGAssertMainThread();
IGParameterAssert(updater);
if (self = [super init]) {
// objectLookupPointerFunctions 返回 hash 表計(jì)算 hash 以及比較 value 是否相同的設(shè)置
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 的對(duì)應(yīng)關(guān)系章钾,能在 O(1) 的時(shí)間復(fù)雜度根據(jù) section 獲取 sectionController墙贱。內(nèi)部實(shí)現(xiàn)結(jié)果如下圖:
graph LR
object -- objectToSectionControllerMap --> IGListSectionController
IGListSectionController -- objectToSectionControllerMap --> object
IGListSectionController -- sectionControllerToSectionMap --> section
section -- sectionControllerToSectionMap --> IGListSectionController
IGListDisplayHandler: 作用和對(duì)外暴露的 IGListAdapterPerformanceDelegate 類似,主要是對(duì) UICollectionViewCell 生命周日相關(guān)對(duì)調(diào)的處理(cell 顯示/消失/分區(qū)頭部贱傀、尾部顯示/消失)嫩痰,內(nèi)部會(huì)把事件傳給 IGListSectionController 的 displayDelegate;在 IGListAdapter+UICollectionView.m 文件中進(jìn)行調(diào)用窍箍。
IGListWorkingRangeHandler: 負(fù)責(zé) collectionView 每個(gè) section(sectionController) 的預(yù)加載的準(zhǔn)備工作。在 IGListAdapter+UICollectionView.m 文件中進(jìn)行調(diào)用丽旅,相關(guān)數(shù)據(jù)會(huì)保存起來椰棘,提供給 IGListAdapter 使用。
IGListAdapterUpdateListener: 代理集合榄笙,IGListAdapter 更新完數(shù)據(jù)后對(duì)集合的代理進(jìn)行通知
數(shù)據(jù)源:
IGListAdapter 會(huì)作為 UICollectionView 默認(rèn)的 dataSource邪狞。
- (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];
// 使當(dāng)前的布局失效,同時(shí)觸發(fā)布局更新
[_collectionView.collectionViewLayout invalidateLayout];
[self _updateCollectionViewDelegate];
if (!IGListExperimentEnabled(self.experiments, IGListExperimentGetCollectionViewAtUpdate)
|| settingFirstCollectionView) {
[self _updateAfterPublicSettingsChange];
}
}
}
globalCollectionViewAdapterMap: key 為 collectionView茅撞,value 為 IGListAdapter
通過 - (void)setCollectionView:(UICollectionView *)collectionView
關(guān)聯(lián) IGListAdapter 和 UICollectionView:
1. globalCollectionViewAdapterMap 先移除舊的 _collectionView 對(duì)應(yīng)的 IGListAdapter帆卓,就是代碼中的 self
2. 將新 collectionView 之前綁定的 IGListAdapter 取消對(duì) collectionView 綁定
3. 將新 collectionView 和當(dāng)前 IGListAdapter 綁定
dataSource 的方法實(shí)現(xiàn)再 IGListAdapter+UICollectionView.m 中,dataSource 的代理方法通過 IGSectionController 返回每個(gè) section 對(duì)應(yīng)的數(shù)據(jù)
// 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 {...}
數(shù)據(jù)源更新 <IGListUpdatingDelegate>
:
IGListAdapter 提供以下幾種方法讓外部進(jìn)行數(shù)據(jù)更新:
- (void)performUpdatesAnimated:(BOOL)animated completion:(nullable IGListUpdaterCompletion)completion;
- (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion;
- (void)reloadObjects:(NSArray *)objects;
我們先以 -reloadDataWithCompletion:
方法為例子米丘,分析數(shù)據(jù)更新的過程:
- (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;
}
// 重新讀取一次數(shù)據(jù)源代理方法剑令,數(shù)據(jù)根據(jù)diffIdentifier去重
NSArray *uniqueObjects = objectsWithDuplicateIdentifiersRemoved([dataSource objectsForListAdapter:self]);
__weak __typeof__(self) weakSelf = self;
[self.updater reloadDataWithCollectionViewBlock:[self _collectionViewBlock]
reloadUpdateBlock:^{
// 移除所有 section controllers 以便于重新生成
[weakSelf.sectionMap reset];
// 根據(jù)去重后的數(shù)據(jù)源重新生成 section controller
[weakSelf _updateObjects:uniqueObjects dataSource:dataSource];
} completion:^(BOOL finished) {
[weakSelf _notifyDidUpdate:IGListAdapterUpdateTypeReloadData animated:NO];
if (completion) {
completion(finished);
}
}];
}
刷新數(shù)據(jù)之前,會(huì)先將數(shù)據(jù)去重拄查,保證數(shù)據(jù)對(duì)應(yīng)的 diffIdentifier 是唯一的吁津。然后調(diào)用 IGListAdapterUpdater 的方法進(jìn)行刷新數(shù)據(jù)
- (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 是為了執(zhí)行 -performBatchUpdatesWithCollectionViewBlock: 前提供更多時(shí)間來完成數(shù)據(jù)更新處理,減少在主線程上進(jìn)行差異化的操作
dispatch_async(dispatch_get_main_queue(), ^{
if (weakSelf.state != IGListBatchUpdateStateIdle
|| ![weakSelf hasChanges]) {
return;
}
if (weakSelf.hasQueuedReloadData) {
[weakSelf performReloadDataWithCollectionViewBlock:collectionViewBlock];
} else {
[weakSelf performBatchUpdatesWithCollectionViewBlock:collectionViewBlock];
}
});
}
之后進(jìn)入條件判斷執(zhí)行 -performReloadDataWithCollectionViewBlock:
方法
- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
IGAssertMainThread();
id<IGListAdapterUpdaterDelegate> delegate = self.delegate;
void (^reloadUpdates)(void) = self.reloadUpdates;
IGListBatchUpdates *batchUpdates = self.batchUpdates;
NSMutableArray *completionBlocks = [self.completionBlocks mutableCopy];
// 清空相關(guān)狀態(tài)
[self cleanStateBeforeUpdates];
void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) {
for (IGListUpdatingCompletion block in completionBlocks) {
block(finished);
}
self.state = IGListBatchUpdateStateIdle;
};
// 防止 collectionView 被釋放導(dǎo)致崩潰
UICollectionView *collectionView = collectionViewBlock();
if (collectionView == nil) {
[self _cleanStateAfterUpdates];
executeCompletionBlocks(NO);
return;
}
// 更新狀態(tài)堕扶,避免更新數(shù)據(jù)的過程中去通知視圖更新
self.state = IGListBatchUpdateStateExecutingBatchUpdateBlock;
// 通知外部移除所有 section controllers碍脏,然后重新生成
if (reloadUpdates) {
reloadUpdates();
}
// 即使我們只是調(diào)用reloadData,也要執(zhí)行所有存儲(chǔ)的 batchUpdates 任務(wù)
// 實(shí)際效果所有 section 視圖的突變將被丟棄稍算,建議使用者也將其實(shí)際的數(shù)據(jù)更新也放入 batchUpdates 任務(wù)集合中典尾,因此,如果我們不執(zhí)行該塊糊探,則 batchUpdates 是不會(huì)被觸發(fā)
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:
中也會(huì)觸發(fā)保存在 batchUpdates 中的更新任務(wù)钾埂,以便及時(shí)刷新數(shù)據(jù)/界面,然后通過代理通知外部 UICollectionView 刷新的前后事件科平。
可以看出 -reloadDataWithCompletion:
基本等同于強(qiáng)制刷新勃教,會(huì)把所有刷新任務(wù)全部執(zhí)行完之后,通知 UICollectionView 刷新界面匠抗。
與 -reloadDataWithCompletion:
不同的是故源,IGListAdapter 還有提供另外一個(gè)方法進(jìn)行數(shù)據(jù)刷新 - (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,防止同時(shí)間有數(shù)據(jù)被刪除
weakSelf.previousSectionMap = [weakSelf.sectionMap copy
// 更新 sectionMap 數(shù)據(jù)汞贸,刷新 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 會(huì)將更新數(shù)據(jù) 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);
// 正在執(zhí)行更新的過程中绳军,同一時(shí)間內(nèi)可能會(huì)有多個(gè)其他更新任務(wù)加入印机,
// 執(zhí)行更新動(dòng)作的時(shí)候,是第一次加入的 fromObject 和 最后加入的 toObjects
// 如果 self.fromObject == nil, 應(yīng)該有先使用之前加入并且還沒有執(zhí)行的 batch update 任務(wù)的終點(diǎn)數(shù)據(jù)源(toObjects)
// 這樣做的目的是使整個(gè)數(shù)據(jù)變化可以串聯(lián)起來
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门驾,并保存數(shù)據(jù)轉(zhuǎn)化的閉包 objectTransitionBlock射赛,會(huì)調(diào)用 -_queueUpdateWithCollectionViewBlock:
方法,利用 dispatch_async 異步調(diào)用 -performBatchUpdatesWithCollectionViewBlock:
- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
IGAssertMainThread();
IGAssert(self.state == IGListBatchUpdateStateIdle, @"Should not call batch updates when state isn't idle");
// 創(chuàng)建局部變量奶是,以便我們可以立即清除狀態(tài)楣责,但將這些數(shù)據(jù)傳遞到批處理更新任務(wù)中
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;
// 清理所有狀態(tài),以便在當(dāng)前更新進(jìn)行時(shí)可以合并新的更新
[self cleanStateBeforeUpdates];
// 初始化更新完成之后的回調(diào)
void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) {
self.applyingUpdateData = nil;
self.state = IGListBatchUpdateStateIdle;
for (IGListUpdatingCompletion block in completionBlocks) {
block(finished);
}
};
// collectionView 如果被銷毀聂沙,則結(jié)束更新恢復(fù)相關(guān)狀態(tài)
UICollectionView *collectionView = collectionViewBlock();
if (collectionView == nil) {
[self _cleanStateAfterUpdates];
executeCompletionBlocks(NO);
return;
}
NSArray *toObjects = nil;
if (toObjectsBlock != nil) {
toObjects = objectsWithDuplicateIdentifiersRemoved(toObjectsBlock());
}
// 初始化數(shù)據(jù)刷新的閉包
void (^executeUpdateBlocks)(void) = ^{
self.state = IGListBatchUpdateStateExecutingBatchUpdateBlock;
// 更新包括 IGListAdapter 的 sectionController 和 objects 的映射關(guān)系等數(shù)據(jù)
// 保證執(zhí)行刷新前秆麸,數(shù)據(jù)已經(jīng)是最新的
if (objectTransitionBlock != nil) {
objectTransitionBlock(toObjects);
}
// 觸發(fā)批量刷新任務(wù)的數(shù)據(jù)更新閉包(包括插入、刪除及汉、刷新單個(gè) section 的數(shù)據(jù))
// objectTransitionBlock 之后執(zhí)行是為了保證 section 級(jí)別的刷新在 item 級(jí)別刷新之前進(jìn)行
for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) {
itemUpdateBlock();
}
// 收集批量刷新完成的回調(diào)沮趣,后續(xù)所有操作完了之后一并處理
[completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks];
self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock;
};
// 執(zhí)行全量的數(shù)據(jù)更新并刷新 UI
void (^reloadDataFallback)(void) = ^{
executeUpdateBlocks();
[self _cleanStateAfterUpdates];
[self _performBatchUpdatesItemBlockApplied];
[collectionView reloadData];
[collectionView layoutIfNeeded];
executeCompletionBlocks(YES);
};
// 如果當(dāng)前 collection 沒有顯示,跳過差分/分批刷新
const BOOL iOS83OrLater = (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_8_3);
if (iOS83OrLater && self.allowsBackgroundReloading && collectionView.window == nil) {
[self _beginPerformBatchUpdatesToObjects:toObjects];
reloadDataFallback();
return;
}
// 禁止同時(shí)執(zhí)行多個(gè) -performBatchUpdates:
[self _beginPerformBatchUpdatesToObjects:toObjects];
const IGListExperiment experiments = self.experiments;
// 計(jì)算新舊數(shù)據(jù)源差分部分坷随,算法參考: 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){
// 更新數(shù)據(jù)
executeUpdateBlocks();
// 根據(jù)整理差分算法結(jié)果房铭,過濾相關(guān) section/item 數(shù)據(jù),把 item 級(jí)別的刷新轉(zhuǎn)換成 section 級(jí)別來規(guī)避 UICollectionView 的 bug温眉,并調(diào)用 collectionView reload/insert/delete/move 操作
self.applyingUpdateData = [self _flushCollectionView:collectionView
withDiffResult:result
batchUpdates:self.batchUpdates
fromObjects:fromObjects];
// 更新相關(guān)數(shù)據(jù)狀態(tài), 清空批量更新任務(wù)和等待更新的數(shù)據(jù)
[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 任務(wù)執(zhí)行的過程中尤其比那話缸匪,則異步在下一個(gè) runloop 周期執(zhí)行相關(guān)更新動(dòng)作
[self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
};
// block that executes the batch update and exception handling
void (^performUpdate)(IGListIndexSetResult *) = ^(IGListIndexSetResult *result){
[collectionView layoutIfNeeded];
@try {
// 對(duì)外通知即將進(jìn)行 batch update
[delegate listAdapterUpdater:self
willPerformBatchUpdatesWithCollectionView:collectionView
fromObjects:fromObjects
toObjects:toObjects
listIndexSetResult:result];
if (collectionView.dataSource == nil) {
// 如果數(shù)據(jù)源為空則不再刷新的 UICollectionview
batchUpdatesCompletionBlock(NO);
} else if (result.changeCount > 100 && IGListExperimentEnabled(experiments, IGListExperimentReloadDataFallback)) {
// 差分變化數(shù)量超過100,進(jìn)行全量刷新
reloadDataFallback();
} else if (animated) {
// 執(zhí)行差分更新的批量動(dòng)畫
[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) {
// 異常對(duì)外通知
[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), ^{
// 計(jì)算完差分部分
IGListIndexSetResult *result = performDiff();
dispatch_async(dispatch_get_main_queue(), ^{
//根據(jù)差分結(jié)果刷新 UICollectionView
performUpdate(result);
});
});
} else {
IGListIndexSetResult *result = performDiff();
performUpdate(result);
}
}
該數(shù)據(jù)更新過程調(diào)用鏈大概是:
|---performUpdatesAnimated:completion:
|---performUpdateWithCollectionViewBlock:fromObjects:toObjectsBlock:animated:objectTransitionBlock:completion:
|---_queueUpdateWithCollectionViewBlock:
|---performBatchUpdatesWithCollectionViewBlock:
整個(gè) performUpdates 的大部分邏輯都是由 IGListUpdater 完成类溢,重中之重都幾種放 -performBatchUpdatesWithCollectionViewBlock:
方法:
1. 判斷 collectionView 是否在顯示豪嗽,若不在屏幕窗口上顯示,直接全量刷新數(shù)據(jù)和視圖豌骏;反之繼續(xù)步驟2
2. 子線程調(diào)用 IGListDiffExperiment龟梦,計(jì)算數(shù)據(jù)的差分變化,計(jì)算完畢之后在主線程觸發(fā)界面刷新邏輯
3. 通過代理對(duì)外通知即將進(jìn)行 batch update 批量更新
4. 如果 collectionView 的 dataSource 為 nil窃躲,結(jié)束更新過程计贰;反之繼續(xù)
5. 差分變化的數(shù)據(jù)個(gè)數(shù)超過100,直接調(diào)用 reloadData 全量刷新數(shù)據(jù)/視圖蒂窒;若變化數(shù)據(jù)小于100躁倒,則調(diào)用 `-[UICollectionView performBatchUpdates:completion:]` 批量刷新數(shù)據(jù)/視圖,刷新過程中會(huì)調(diào)用 `-_flushCollectionView:withDiffResult:batchUpdates:fromObjects:` 將數(shù)據(jù)源提供的數(shù)據(jù)和 diff 結(jié)果包裝成批量更新的數(shù)據(jù)類型 IGListBatchUpdateData 以便 UICollectionView 進(jìn)行讀取
視圖管理 <IGListAdapterPerformanceDelegate>
:
IGListAdapter 會(huì)作為 collectionView 屬性的默認(rèn)代理
@protocol IGListCollectionViewDelegateLayout <UICollectionViewDelegateFlowLayout>
@interface IGListAdapter (UICollectionView)
<
UICollectionViewDataSource,
IGListCollectionViewDelegateLayout
>
IGListAdapter 會(huì)實(shí)現(xiàn)相關(guān)代理方法洒琢,進(jìn)行對(duì) cell 級(jí)別的視圖管理秧秉,包含視圖 UICollectionView 滾動(dòng),cell 大小衰抑、cell 顯示等事件象迎,并通過 IGListAdapterPerformanceDelegate 對(duì)外通知
- (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 的拖動(dòng)會(huì)首先觸發(fā) UICollectionView 的代理方法 -collectionView:moveItemAtIndexPath:toIndexPath
。在這個(gè)方法中會(huì)判斷拖動(dòng)開始/結(jié)束位置,根據(jù)不同的情況進(jìn)行數(shù)據(jù)刷新
- (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]) {
// 同一個(gè) section 內(nèi)的挪動(dòng)
[self moveInSectionControllerInteractive:sourceSectionController
fromIndex:sourceItemIndex
toIndex:destinationItemIndex];
} else {
// 撤銷修改
[self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
}
return;
}
// 跨 section 移動(dòng)砾淌, 如果 section 的 item 數(shù)目為1
if ([sourceSectionController numberOfItems] == 1 && [destinationSectionController numberOfItems] == 1) {
[self moveSectionControllerInteractive:sourceSectionController
fromIndex:sourceSectionIndex
toIndex:destinationSectionIndex];
return;
}
// 撤銷修改
[self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
}
}
成功拖動(dòng)之后會(huì)觸發(fā) IGListUpdater 的 -moveInSectionControllerInteractive
或者 -moveSectionControllerInteractive:fromIndex:toIndex
啦撮,在同一個(gè) UICollectionView section 中拖動(dòng)則觸發(fā)前者,跨 section 之間則后者
- (void)moveInSectionControllerInteractive:(IGListSectionController *)sectionController
fromIndex:(NSInteger)fromIndex
toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0) {
//... 略
[sectionController moveObjectFromIndex:fromIndex toIndex:toIndex];
}
在同一個(gè) section 中拖動(dòng) UICollectionViewCell 比較簡單汪厨,實(shí)現(xiàn)中回去調(diào)用對(duì)應(yīng) sectionController 的 -moveObjectFromIndex:toIndex:
赃春,使用者在自定義的 sectionController 中實(shí)現(xiàn)該代理方法,進(jìn)行對(duì)應(yīng)的數(shù)據(jù)刷新更新即可
- (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) {
// 如果 item 是被移動(dòng)到 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<id<IGListDiffable>> *updatedObjects = [dataSource objectsForListAdapter:self];
[self _updateObjects:updatedObjects dataSource:dataSource];
}
// 刷新 UI
// 這里 from index 和 to index 可能是相同的, 但是實(shí)際上可能是以 section 的方式向上/下移動(dòng)了一個(gè) section
[self.updater moveSectionInCollectionView:collectionView fromIndex:fromIndex toIndex:toIndex];
}
跨 UICollectionView section 間拖動(dòng) UICollectionViewCell 需要對(duì)原始/目標(biāo) section 的位置/ item 數(shù)目進(jìn)行相關(guān)判斷劫乱,最后執(zhí)行 IGListUpdater 的 -moveSectionInCollectionView:fromIndex:toIndex:
方法
- (void)moveSectionInCollectionView:(UICollectionView *)collectionView
fromIndex:(NSInteger)fromIndex
toIndex:(NSInteger)toIndex {
// iOS 移動(dòng)是以 item 為移動(dòng)單位的拖動(dòng)
// 如果 originating section 中的 item 數(shù)量是1织中,將這個(gè) item 拖動(dòng)到 item 數(shù)目同樣為1的 target section
// 拖動(dòng)之后 target section 的 item 數(shù)目為2, originating section 的數(shù)目為 0
// 基于這種情況必須使用 reloadData
[collectionView reloadData];
// 似乎在 UICollectionVie 的 -moveItemAtIndexPath 代理方法調(diào)用期間調(diào)用的 -reloadData 不會(huì)按預(yù)期重新加載所有單元格衷戈,
// 因此狭吼,這里進(jìn)一步重新加載了所有可見部分,以確保沒有任何 item 上的數(shù)據(jù)與 dataSource 不同步脱惰。
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];
// prevent double-animation from reloadData + reloadSections
[CATransaction begin];
[CATransaction setDisableActions:YES];
[collectionView performBatchUpdates:^{
[collectionView reloadSections:visibleSections];
} completion:^(BOOL finished) {
[CATransaction commit];
}];
}
在 -moveSectionInCollectionView:fromIndex:toIndex:
方法會(huì)現(xiàn)調(diào)用 -[UICollectionView reloadDate]
來規(guī)避 origin section item 數(shù)目為0的情況,之后還會(huì)對(duì)應(yīng)當(dāng)前屏幕顯示區(qū)域進(jìn)行 batch update 來規(guī)避 UICollectionView 不能及時(shí)刷新的 bug窿春。
整個(gè) UICollectionViewCell 拖動(dòng)的調(diào)用棧大概為:
|---collectionView:moveItemAtIndexPath:toIndexPath:
|---moveInSectionControllerInteractive:fromIndex:toIndex: # section 內(nèi)拖動(dòng)
|---moveObjectFromIndex:toIndex:
|---moveSectionControllerInteractive:fromIndex:toIndex: # section 間拖動(dòng)
|---_updateObjects:dataSource
|---moveSectionInCollectionView:fromIndex:toIndex # updater
|---performBatchUpdates:completion: # UICollectionView
總結(jié)來說拉一,整個(gè) IGListKit 結(jié)構(gòu)可以用下圖來概括:
可以看出來,IGListAdapter 負(fù)責(zé)不同功能的屬性都是通過面向協(xié)議來進(jìn)行開發(fā)旧乞,不同的功能模塊粒度都比較小蔚润,避免模塊之間的循環(huán)依賴,實(shí)現(xiàn)數(shù)據(jù)跟視圖的有效解耦尺栖。
不僅如此,IGListKit 通過 IGListDiffable 協(xié)議加上 diff 算法,對(duì)外隱藏?cái)?shù)據(jù)更新的細(xì)節(jié)沫勿,用戶只需關(guān)注業(yè)務(wù)數(shù)據(jù)薛窥,減輕了數(shù)據(jù)更新的操作。
其他
IGListKit 中還用到一些平時(shí)沒有注意到的特性
NSCountedSet
插入 NSCountedSet 對(duì)象的每個(gè)不同的對(duì)象都有一個(gè)與之相關(guān)的計(jì)數(shù)器挫以,同一個(gè)對(duì)象每加入一次 NSCountedSet 集合中者蠕,對(duì)應(yīng)的 count 就會(huì)加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 的顯示狀態(tài),旨在通知外部每個(gè) section 的顯示/消失事件掐松。
prefetchingEnabled
當(dāng)調(diào)用 collectionView:didEndDisplayingCell:forItemAtIndexPath:
后踱侣,cell 不會(huì)立刻進(jìn)入復(fù)用隊(duì)列,系統(tǒng)會(huì)keeps it around for a bit大磺。相當(dāng)于會(huì)緩存該 cell 一小段時(shí)間抡句,在這段時(shí)間內(nèi)如果該 cell 再次回到屏幕中,便不會(huì)重新調(diào)用 cellForItemAtIndexPath:
杠愧,而是直接顯示待榔。
至于系統(tǒng)會(huì)緩存多久,官方并沒有給出明確的時(shí)間流济,感覺跟程序運(yùn)行時(shí)開銷有關(guān)究抓。
如果想關(guān)閉該功能猾担,需要設(shè)置 collectionView.prefetchingEnabled = NO;
。
UICollectionViewLayoutInvalidationContext
當(dāng)改變 UICollectionView item 的時(shí)候刺下,通過調(diào)用 -invalidateLayout
方法讓 UICollectionView 布局失效绑嘹,通過 Invalidation Context 聲明了在布局失效時(shí)布局的哪些部分需要被更新,布局對(duì)象就可以根據(jù)該信息減小重新計(jì)算的數(shù)據(jù)量橘茉。
IGListKit 提供了自定義的 IGListCollectionViewLayout 類來優(yōu)化 UICollectionView 的刷新工腋,IGListCollectionViewLayout 實(shí)現(xiàn)和 UICollectionViewLayoutInvalidationContext 相關(guān)的方法
@interface IGListCollectionViewLayoutInvalidationContext : UICollectionViewLayoutInvalidationContext
// 追加視圖
@property (nonatomic, assign) BOOL ig_invalidateSupplementaryAttributes;
@property (nonatomic, assign) BOOL ig_invalidateAllAttributes;
@end
IGListCollectionViewLayoutInvalidationContext 類繼承了 UICollectionViewLayoutInvalidationContext,用于記錄刷新布局相關(guān)邏輯
// -[UICollectionView setFrame:] / -[UICollectionView setBounds:] 會(huì)觸發(fā)
- (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 改變之后畅卓,必須進(jìn)行全量刷新
context.ig_invalidateAllAttributes = YES;
}
return context;
}
-invalidationContextForBoundsChange:
當(dāng) UICollectionView 發(fā)生變化的時(shí)候(比如視圖 frame 發(fā)生改變)擅腰,在進(jìn)行視圖刷新之前,會(huì)觸發(fā)該方法返回 UICollectionViewLayoutInvalidationContext 對(duì)象來告訴UICollectionView 布局刷新的相關(guān)信息翁潘。
// 根據(jù) context 中的信息重新計(jì)算布局改變的部分趁冈。
// -[UICollectionView setDataSource:] / -[UICollectionView setFrame:] 會(huì)觸發(fā)該方法
// 也可以主動(dòng)調(diào)用,強(qiáng)制刷新
- (void)invalidateLayoutWithContext:(IGListCollectionViewLayoutInvalidationContext *)context {
BOOL hasInvalidatedItemIndexPaths = NO;
if ([context respondsToSelector:@selector(invalidatedItemIndexPaths)]) {
hasInvalidatedItemIndexPaths = [context invalidatedItemIndexPaths].count > 0;
}
// _minimumInvalidatedSection 用來記錄指定從哪個(gè) section 開始的布局失效拜马,需要重新布局
if (hasInvalidatedItemIndexPaths
|| [context invalidateEverything]
|| context.ig_invalidateAllAttributes) {
// invalidates all
_minimumInvalidatedSection = 0;
} else if ([context invalidateDataSourceCounts] && _minimumInvalidatedSection == NSNotFound) {
// invalidateDataSourceCounts 標(biāo)記 layout 需要重新從 UICollectionView 查詢 section 和 item 數(shù)目
// UICollectionView 調(diào)用 -reloadData 或者插入/刪除 item 的時(shí)候 invalidateDataSourceCounts = YES
// 如果 layout 需要重新 UICollectionView 的信息或者沒有找到重新刷新的 section 啟動(dòng)渗勘,則刷新起點(diǎn) section 默認(rèn)為0
_minimumInvalidatedSection = 0;
}
if (context.ig_invalidateSupplementaryAttributes) {
// 清空追加視圖的布局信息緩存
[self _resetSupplementaryAttributesCache];
}
[super invalidateLayoutWithContext:context];
}
-invalidateLayoutWithContext:
方法在 UICollectionView 布局信息發(fā)生變化會(huì)被系統(tǒng)調(diào)用,IGListCollectionViewLayout 實(shí)現(xiàn)了該方法俩莽,在調(diào)用的過程中會(huì)對(duì)一些布局緩存進(jìn)行更新(主要是緩存 UICollectionViewLayoutAttributes 對(duì)象)旺坠,具體細(xì)節(jié)不再展開。
除此之外扮超,UICollectionViewLayoutInvalidationContext 本身提供了幾個(gè)方法取刃,用戶可以主動(dòng)調(diào)用來進(jìn)行局部 UI 刷新
// 調(diào)用此方法以標(biāo)識(shí)布局中需要更新的特定單元格。
// 指定的更新的所有 indexPath 對(duì)象將添加到屬性 invalidatedItemIndexPaths 中出刷。
- (void)invalidateItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths API_AVAILABLE(ios(8.0));
// 重新計(jì)算一個(gè)或者多個(gè)追加視圖的布局
- (void)invalidateSupplementaryElementsOfKind:(NSString *)elementKind atIndexPaths:(NSArray<NSIndexPath *> *)indexPaths API_AVAILABLE(ios(8.0));
// 重新計(jì)算一個(gè)或者多個(gè)裝飾視圖的布局
- (void)invalidateDecorationElementsOfKind:(NSString *)elementKind atIndexPaths:(NSArray<NSIndexPath *> *)indexPaths API_AVAILABLE(ios(8.0));