0%

IGListKit 源码解析

IGListKit 是 Instagram 维护一个 UI 框架,采用面向协议的思想,基于 UICollectionView 实现,由数据驱动的 UI 列表框架。本文基于 IGListKit 源码对其主要设计思想进行分析。

分析前,我们现看一下 IGListKit 中的数据和 UI 对应关系图

image-20191105193430090

可以看出 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]) {
// 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。内部实现结果如下图:

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
// 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 {...}

数据源更新 <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;
}

// 重新读取一次数据源代理方法,数据根据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 的方法进行刷新数据

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 是为了执行 -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: 方法

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) {
// 重新捕获一次 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 中

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);

// 正在执行更新的过程中,同一时间内可能会有多个其他更新任务加入,
// 执行更新动作的时候,是第一次加入的 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:

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);
}
};

// 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);
}
}

该数据更新过程调用链大概是:

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 进行读取

视图管理 <IGListAdapterPerformanceDelegate>:

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]) {
// 同一个 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 之间则后者

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) {
// 如果 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<id<IGListDiffable>> *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: 方法

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 {
// 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<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: 方法会现调用 -[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 结构可以用下图来概括:

image-20191105170804580

可以看出来,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
// -[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 布局刷新的相关信息。

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
// 根据 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 刷新

1
2
3
4
5
6
7
8
9
// 调用此方法以标识布局中需要更新的特定单元格。 
// 指定的更新的所有 indexPath 对象将添加到属性 invalidatedItemIndexPaths 中。
- (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));