Home IGListKit 的数据处理 - Updater&Diff
Post
Cancel

IGListKit 的数据处理 - Updater&Diff

IGListAdapterUpdater

在初始化 IGListAdapter 时提供了一个 id<IGListUpdatingDelegate> updater 参数,调用者可以自己自定义一个支持 IGListUpdatingDelegate 协议的类,来实现 IGListUpdatingDelegate 的方法。 IGListAdapter 在更新 UICollectionView 和数据源时都是通过 updater 来进行操作, IGListKit 为我们提供了一个默认的 updaterIGListAdapterUpdaterIGListAdapter 支持 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
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
// 当更新逻辑执行完成时调用的 `block` , `finished` 表示更新是否完成。
typedef void (^IGListUpdatingCompletion)(BOOL finished);

// 当 `adapter` 对 `UICollectionView` 进行改动时调用, `toObjects` 表示新的 `objects`
typedef void (^IGListObjectTransitionBlock)(NSArray *toObjects);

// 包含所有更新的 `block` 
typedef void (^IGListItemUpdateBlock)(void);

// `adapter` 对 `UICollectionView` 进行 `reload` 是调用
typedef void (^IGListReloadUpdateBlock)(void);

// 返回转换后的 `objects`
typedef NSArray * _Nullable (^IGListToObjectBlock)(void);

// 获取执行更新的 `UICollectionView`
typedef UICollectionView * _Nullable (^IGListCollectionViewBlock)(void);

// `IGListUpdatingDelegate` 用于处理 `section` 和 `row` 级别的更新,接口的实现需要对集合处理或者转发。
@protocol IGListUpdatingDelegate <NSObject>

// 用于在集合中寻找 `object` 。
- (NSPointerFunctions *)objectLookupPointerFunctions;

/*
用于判断如何在 `objects` 进行转换。可以在 `objects` 直接执行 diff , reload 每个 section ,或者直接调用 `UICollectionView` 的 `-reloadData` 方法。
最后, `UICollectionView` 必须要配置好 `toObjects` 数组中对应的每个 `section` 。
*/
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
                                 fromObjects:(nullable NSArray<id <IGListDiffable>> *)fromObjects
                              toObjectsBlock:(nullable IGListToObjectBlock)toObjectsBlock
                                    animated:(BOOL)animated
                       objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock
                                  completion:(nullable IGListUpdatingCompletion)completion;

// 插入对应的 indexPaths
- (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray <NSIndexPath *> *)indexPaths;

// 删除对应的 indexPaths
- (void)deleteItemsFromCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray <NSIndexPath *> *)indexPaths;

// 移动对应的 indexPath
- (void)moveItemInCollectionView:(UICollectionView *)collectionView
                   fromIndexPath:(NSIndexPath *)fromIndexPath
                     toIndexPath:(NSIndexPath *)toIndexPath;

// reload 对应的 fromIndexPath 和 toIndexPath
- (void)reloadItemInCollectionView:(UICollectionView *)collectionView
                     fromIndexPath:(NSIndexPath *)fromIndexPath
                       toIndexPath:(NSIndexPath *)toIndexPath;

// section 级别的处理,移动 index 对应的 section
- (void)moveSectionInCollectionView:(UICollectionView *)collectionView
                          fromIndex:(NSInteger)fromIndex
                            toIndex:(NSInteger)toIndex;

// 执行 reload data
- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
                        reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock
                               completion:(nullable IGListUpdatingCompletion)completion;

// reload 对应的 sections
- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections;

// 执行 `IGListItemUpdateBlock`
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
                                    animated:(BOOL)animated
                                 itemUpdates:(IGListItemUpdateBlock)itemUpdates
                                  completion:(nullable IGListUpdatingCompletion)completion;

@end

IGListAdapterUpdater 内部提供了一套队列刷新机制,使用 IGListBatchUpdates 记录批量刷新的 block

1
2
@property (nonatomic, strong, readonly) NSMutableArray<void (^)(void)> *itemUpdateBlocks;
@property (nonatomic, strong, readonly) NSMutableArray<void (^)(BOOL)> *itemCompletionBlocks;

批量与全局

IGListAdapterUpdater 提供的方法可以分为两种:

1.批量刷新,通过 diff 算法计算出需要进行操作的 Cell 或者 SectionIGListCollectionViewBlock 用于提供 UICollectionView ,通过 block 的方式来获取,可以保证在调用 block 时获取到 UICollectionView 是最新设置的。 IGListToObjectBlock 的作用也是一样的,保证获取的到 toObjects 是最新的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
                            		fromObjects:(NSArray *)fromObjects
                         		 toObjectsBlock:(IGListToObjectBlock)toObjectsBlock
                               	   animated:(BOOL)animated
                       objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock
                                  completion:(IGListUpdatingCompletion)completion {
    self.fromObjects = self.fromObjects ?: self.pendingTransitionToObjects ?: fromObjects;
    self.toObjectsBlock = toObjectsBlock;

    self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated;

    self.objectTransitionBlock = objectTransitionBlock;

    IGListUpdatingCompletion localCompletion = completion;
    if (localCompletion) {
        [self.completionBlocks addObject:localCompletion];
    }

    [self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
}

self.fromObjects = self.fromObjects ?: self.pendingTransitionToObjects ?: fromObjectsself.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated 的作用在下文会说到。

2.全局刷新,作用类似于 UICollectionViewreloadData 方法:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
                   reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock
                          completion:(nullable IGListUpdatingCompletion)completion {
    IGListUpdatingCompletion localCompletion = completion;
    if (localCompletion) {
        [self.completionBlocks addObject:localCompletion];
    }

    self.reloadUpdates = reloadUpdateBlock;
    self.queuedReloadData = YES;
    [self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
}

可以看到批量刷新和全局刷新的实现到最后都会调用 _queueUpdateWithCollectionViewBlock ,而在 _queueUpdateWithCollectionViewBlock 方法中会根据是否为 reloadData 来调用不同的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)_queueUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
    __weak __typeof__(self) weakSelf = self;
    // 这里在 `main_queue` 上使用`dispatch_async` 的原因是如果在短时间内多次调用批量刷新的方法,
    // 可以去掉多余的 `diff` 计算和页面刷新,只需要执行一次。
    dispatch_async(dispatch_get_main_queue(), ^{
        // 如果 `updater` 不是在 `IGListBatchUpdateStateIdle` 状态或者没有改变,则直接返回。 
        if (weakSelf.state != IGListBatchUpdateStateIdle
            || ![weakSelf hasChanges]) {
            return;
        }
        // 判断是否 hasQueuedReloadData 来调用不同的刷新方法
        if (weakSelf.hasQueuedReloadData) {
            [weakSelf performReloadDataWithCollectionViewBlock:collectionViewBlock];
        } else {
            [weakSelf performBatchUpdatesWithCollectionViewBlock:collectionViewBlock];
        }
    });
}

在进行批量更新操作时,如果 stateIGListBatchUpdateStateExecutingBatchUpdateBlock ,执行批量更新 block 的状态,则直接执行 block 即可,不需要添加到 itemUpdateBlocks 中:

1
2
3
4
5
6
7
8
if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) {
    itemUpdates();
} else {
    [batchUpdates.itemUpdateBlocks addObject:itemUpdates];
    self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated;

    [self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
}

状态配置

在看 perform 方法实现前先看下状态的相关定义,以便更好理解整体流程。

1
2
3
4
5
6
typedef NS_ENUM (NSInteger, IGListBatchUpdateState) {
    IGListBatchUpdateStateIdle,
    IGListBatchUpdateStateQueuedBatchUpdate,
    IGListBatchUpdateStateExecutingBatchUpdateBlock,
    IGListBatchUpdateStateExecutedBatchUpdateBlock,
};
  1. IGListBatchUpdateStateIdle ,空闲状态,即当前无 perform 任务;
  2. IGListBatchUpdateStateQueuedBatchUpdate ,已加入到批量更新的状态中,防止内部在同一时间内多次调用 performBatchUpdatesWithCollectionViewBlock: 方法;
  3. IGListBatchUpdateStateExecutingBatchUpdateBlock 正在执行批量更新操作;
  4. IGListBatchUpdateStateExecutedBatchUpdateBlock 已经完成批量更新操作。

在整个更新流程中, updater.state 会在这四种状态间切换,在不同状态间执行重复刷新操作时, updater 会因应不同的状态调用不同的方法,这块的处理是为了保证 UI 跟数据源之间的一致性和减少多余的刷新操作。

在每次开始进行刷新操作前,都会记录复制一份 updater 的相关属性到本地变量中,同时会调用 cleanStateBeforeUpdates 方法清空属性,这样同时调用刷新方法也不会互相覆盖掉,彼此间的状态也不会互相影响:

1
2
3
4
5
6
7
8
9
- (void)cleanStateBeforeUpdates {
    self.queuedUpdateIsAnimated = YES;
    self.fromObjects = nil;
    self.toObjectsBlock = nil;
    self.reloadUpdates = nil;
    self.queuedReloadData = NO;
    self.objectTransitionBlock = nil;
    [self.completionBlocks removeAllObjects];
}

updater 提供了 hasChanges 来判断是否有改动,避免多余的操作和一直执行 perform 操作:

1
2
3
4
5
6
- (BOOL)hasChanges {
    return self.hasQueuedReloadData
    || [self.batchUpdates hasChanges]
    || self.fromObjects != nil
    || self.toObjectsBlock != nil;
}

全局刷新

performReloadDataWithCollectionViewBlockreloadData 时调用,不需要进行 diff 的计算,处理起来也简单一点。方法首先初始化相关本地变量,然后调用 cleanStateBeforeUpdates 方法清空属性,防止和其它刷新任务互相影响:

1
2
3
4
5
6
7
8
9
- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
    id<IGListAdapterUpdaterDelegate> delegate = self.delegate;
    void (^reloadUpdates)(void) = self.reloadUpdates;
    IGListBatchUpdates *batchUpdates = self.batchUpdates;
    NSMutableArray *completionBlocks = [self.completionBlocks mutableCopy];

    [self cleanStateBeforeUpdates];
	  ...
}

设置 executeCompletionBlocks ,遍历 completionBlocks ,执行完毕后恢复 stateIGListBatchUpdateStateIdle

1
2
3
4
5
6
7
void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) {
    for (IGListUpdatingCompletion block in completionBlocks) {
        block(finished);
    }

    self.state = IGListBatchUpdateStateIdle;
};

判断 collectionView 是否为 nil ,如果为 nil 则直接返回:

1
2
3
4
5
6
7
UICollectionView *collectionView = collectionViewBlock();
if (collectionView == nil) {
    [self _cleanStateAfterUpdates];
    executeCompletionBlocks(NO);
    [_delegate listAdapterUpdater:self didFinishWithoutUpdatesWithCollectionView:collectionView];
    return;
}

设置 stateIGListBatchUpdateStateExecutingBatchUpdateBlock ,进入执行 updateBlock 的流程,如果有设置 reloadUpdates ,则执行 reloadUpdates 。即使是在 reloadData 流程中,也需要调用所有 itemUpdateBlocks ,因为调用方有可能在 itemUpdateBlock 中对数据进行修改,必须要保证数据源和 UI 一致。把 batchUpdates.itemCompletionBlocks 添加到 completionBlocks 中,保证所有的 completionBlocks 都会被执行。最后调用定义好的 executeCompletionBlocks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
self.state = IGListBatchUpdateStateExecutingBatchUpdateBlock;

if (reloadUpdates) {
    reloadUpdates();
}

for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) {
    itemUpdateBlock();
}

[completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks];

self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock;

[self _cleanStateAfterUpdates];

[delegate listAdapterUpdater:self willReloadDataWithCollectionView:collectionView isFallbackReload:NO];
[collectionView reloadData];
[collectionView.collectionViewLayout invalidateLayout];
[collectionView layoutIfNeeded];
[delegate listAdapterUpdater:self didReloadDataWithCollectionView:collectionView isFallbackReload:NO];

executeCompletionBlocks(YES);

批量刷新

performBatchUpdatesWithCollectionViewBlock 进行批量更新时,需要处理各个状态的边界逻辑,所以比 performReloadDataWithCollectionViewBlock 更加复杂,在代码中是个 204 行的函数,下面拆开来说下具体的实现:

1.首先创建本地变量来记录所有的相关的属性,防止在执行批量更新过程中,又再次调用了 performBatchUpdatesWithCollectionViewBlock 接口,导致原有的属性被覆盖, cleanStateBeforeUpdates 会将相关属性复原,确保对更新过程中的其它 performBatchUpdatesWithCollectionViewBlock 调用没影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
    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;
    const BOOL allowsReloadingOnTooManyUpdates = self.allowsReloadingOnTooManyUpdates;
    const IGListExperiment experiments = self.experiments;
    IGListBatchUpdates *batchUpdates = self.batchUpdates;

    [self cleanStateBeforeUpdates];
	  
}

2.如果 collectionViewnil 就直接返回,这块的处理和 performReloadDataWithCollectionViewBlock 是一致的,通过 toObjectsBlock 来获取 toObjects ,这里使用 block 的原因是可以保证在获取 toObjects 时对应的数据源是最新的:

1
2
3
4
5
6
7
8
9
10
11
12
UICollectionView *collectionView = collectionViewBlock();
if (collectionView == nil) {
    [self _cleanStateAfterUpdates];
    executeCompletionBlocks(NO);
    [_delegate listAdapterUpdater:self didFinishWithoutUpdatesWithCollectionView:collectionView];
    return;
}

NSArray *toObjects = nil;
if (toObjectsBlock != nil) {
    toObjects = objectsWithDuplicateIdentifiersRemoved(toObjectsBlock());
}

3.定义 executeUpdateBlocks ,首先设置 stateIGListBatchUpdateStateExecutingBatchUpdateBlock ,防止多次执行。然后在执行 itemUpdateBlock 前先调用 objectTransitionBlock ,使得数据源更新到最新的 toObjects ,以保证数据源跟 UI 一致。执行 itemUpdateBlock ,在 itemUpdateBlock 中处理 NSIndexPath 对应的插入,删除和刷新操作。最后把 batchUpdates.itemCompletionBlocks 添加到 completionBlocks 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void (^executeUpdateBlocks)(void) = ^{
    self.state = IGListBatchUpdateStateExecutingBatchUpdateBlock;
    
    if (objectTransitionBlock != nil) {
        objectTransitionBlock(toObjects);
    }
    
    for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) {
        itemUpdateBlock();
    }

    [completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks];

    self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock;
};

4.定义 reloadDataFallback ,如果 collectionView 所在的 window 不可见,则直接 reloadData ,跳过 diff 操作。在 reloadDataFallback 的最后,调用 _queueUpdateWithCollectionViewBlock: ,防止丢失一些批量更新过程中进行的更新操作,如果在下一个 Runloop 过程中没有更新操作, _queueUpdateWithCollectionViewBlock 会直接退出。设置 pendingTransitionToObjectstoObjects ,在后续的更新中 pendingTransitionToObjects 作为 fromObjects 使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void (^reloadDataFallback)(void) = ^{
    [delegate listAdapterUpdater:self willReloadDataWithCollectionView:collectionView isFallbackReload:YES];
    executeUpdateBlocks();
    [self _cleanStateAfterUpdates];
    [self _performBatchUpdatesItemBlockApplied];
    [collectionView reloadData];
    [collectionView layoutIfNeeded];
    executeCompletionBlocks(YES);
    [delegate listAdapterUpdater:self didReloadDataWithCollectionView:collectionView isFallbackReload:YES];

    [self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
};

[self _beginPerformBatchUpdatesToObjects:toObjects];

if (self.allowsBackgroundReloading && collectionView.window == nil) {
    reloadDataFallback();
    return;
}

5.定义 batchUpdatesBlock ,放到 -[UICollectionView performBatchUpdates:completion:] 第一个 block 参数中,如果 singleItemSectionUpdatesYES ,即每个 section 中只有 1 个 item ,那么可以在 section 层面进行处理,直接调用 UICollectionView 的操作 section 的相关方法即可:

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 (^batchUpdatesBlock)(IGListIndexSetResult *result) = ^(IGListIndexSetResult *result){
    executeUpdateBlocks();
    if (self.singleItemSectionUpdates) {
        [collectionView deleteSections:result.deletes];
        [collectionView insertSections:result.inserts];
        for (IGListMoveIndex *move in result.moves) {
            [collectionView moveSection:move.from toSection:move.to];
        }
        
        self.applyingUpdateData = [[IGListBatchUpdateData alloc]
                                   initWithInsertSections:result.inserts
                                   deleteSections:result.deletes
                                   moveSections:[NSSet setWithArray:result.moves]
                                   insertIndexPaths:@[]
                                   deleteIndexPaths:@[]
                                   updateIndexPaths:@[]
                                   moveIndexPaths:@[]];
    } else {
        self.applyingUpdateData = IGListApplyUpdatesToCollectionView(collectionView,
                                                                     result,
                                                                     self.batchUpdates,
                                                                     fromObjects,
                                                                     experiments,
                                                                     self.sectionMovesAsDeletesInserts,
                                                                     self.preferItemReloadsForSectionReloads);
    }

    [self _cleanStateAfterUpdates];
    [self _performBatchUpdatesItemBlockApplied];
};

6.在 IGListApplyUpdatesToCollectionView 中针对 reload 操作进行特殊处理。sectionReloads 在手动调用 reload 方法时会记录对应的 section ,合并 diff 和手动 reloadssectionreloads 中,同时如果有需要的话使用 delete + insert 代替 move

1
2
3
4
5
6
7
8
9
10
11
12
13
NSMutableIndexSet *reloads = [diffResult.updates mutableCopy];
[reloads addIndexes:batchUpdates.sectionReloads];

NSMutableIndexSet *inserts = [diffResult.inserts mutableCopy];
NSMutableIndexSet *deletes = [diffResult.deletes mutableCopy];
NSMutableArray<NSIndexPath *> *itemUpdates = [NSMutableArray new];
if (sectionMovesAsDeletesInserts) {
    for (IGListMoveIndex *move in moves) {
        [deletes addIndex:move.from];
        [inserts addIndex:move.to];
    }
    moves = [NSSet new];
}

如之前提到的在 performBatchUpdatesreload 是不安全的,所以只有在 moves/inserts/deletes 都为 0 时才执行 reload 操作,否则使用 delete + insert 代替:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (preferItemReloadsForSectionReloads
    && moves.count == 0 && inserts.count == 0 && deletes.count == 0 && reloads.count > 0) {
    [reloads enumerateIndexesUsingBlock:^(NSUInteger sectionIndex, BOOL * _Nonnull stop) {
        NSMutableIndexSet *localIndexSet = [NSMutableIndexSet indexSetWithIndex:sectionIndex];
        if (sectionIndex < [collectionView numberOfSections]
            && sectionIndex < [collectionView.dataSource numberOfSectionsInCollectionView:collectionView]
            && [collectionView numberOfItemsInSection:sectionIndex] == [collectionView.dataSource collectionView:collectionView numberOfItemsInSection:sectionIndex]) {
            [itemUpdates addObjectsFromArray:convertSectionReloadToItemUpdates(localIndexSet, collectionView)];
        } else {
            IGListConvertReloadToDeleteInsert(localIndexSet, deletes, inserts, diffResult, fromObjects);
        }
    }];
} else {
    IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, diffResult, fromObjects);
}

将 itemReloads 转换为 itemDeletes + itemInserts ,生成最后的 updateDatacollectionView 根据 updateDataitem 进行操作, ig_applyBatchUpdateData: 内部调用对应的 delete/insert/move/reload 方法:

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
NSMutableArray<NSIndexPath *> *itemInserts = batchUpdates.itemInserts;
NSMutableArray<NSIndexPath *> *itemDeletes = batchUpdates.itemDeletes;
NSMutableArray<IGListMoveIndexPath *> *itemMoves = batchUpdates.itemMoves;

NSSet<NSIndexPath *> *uniqueDeletes = [NSSet setWithArray:itemDeletes];
NSMutableSet<NSIndexPath *> *reloadDeletePaths = [NSMutableSet new];
NSMutableSet<NSIndexPath *> *reloadInsertPaths = [NSMutableSet new];
for (IGListReloadIndexPath *reload in batchUpdates.itemReloads) {
    if (![uniqueDeletes containsObject:reload.fromIndexPath]) {
        [reloadDeletePaths addObject:reload.fromIndexPath];
        [reloadInsertPaths addObject:reload.toIndexPath];
    }
}
[itemDeletes addObjectsFromArray:[reloadDeletePaths allObjects]];
[itemInserts addObjectsFromArray:[reloadInsertPaths allObjects]];

IGListBatchUpdateData *updateData = [[IGListBatchUpdateData alloc] initWithInsertSections:inserts
                                                                           deleteSections:deletes
                                                                             moveSections:moves
                                                                         insertIndexPaths:itemInserts
                                                                         deleteIndexPaths:itemDeletes
                                                                         updateIndexPaths:itemUpdates
                                                                           moveIndexPaths:itemMoves];
[collectionView ig_applyBatchUpdateData:updateData];
return updateData;

7.设置 fallbackWithoutUpdates ,在 collectionView.dataSourcenil 时调用:

1
2
3
4
5
void (^fallbackWithoutUpdates)(void) = ^(void) {
    executeCompletionBlocks(NO);
    [delegate listAdapterUpdater:self didFinishWithoutUpdatesWithCollectionView:collectionView];
    [self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
};

8.设置 batchUpdatesCompletionBlock ,放到 -[UICollectionView performBatchUpdates:completion:] 第二个 block 参数中:

1
2
3
4
5
6
void (^batchUpdatesCompletionBlock)(BOOL) = ^(BOOL finished) {
    IGListBatchUpdateData *oldApplyingUpdateData = self.applyingUpdateData;
    executeCompletionBlocks(finished);
    [delegate listAdapterUpdater:self didPerformBatchUpdates:oldApplyingUpdateData collectionView:collectionView];
    [self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
};

9.把 [UICollectionView performBatchUpdates 封装起来,如果在 batchUpdatesBlock 处理时崩溃的了,显示出来的第一行 App 符号就不是 block 了。 block 生成的名字会包含行数,如果行数调整了,就会被标记为不同的崩溃,这会对崩溃记录造成影响:

1
2
3
4
5
6
7
8
9
10
11
void (^performUpdate)(IGListIndexSetResult *) = ^(IGListIndexSetResult *result){
    [delegate listAdapterUpdater:self
willPerformBatchUpdatesWithCollectionView:collectionView
                     fromObjects:fromObjects
                       toObjects:toObjects
              listIndexSetResult:result
                        animated:animated];
    IGListAdapterUpdaterPerformBatchUpdate(collectionView, animated, ^{
        batchUpdatesBlock(result);
    }, batchUpdatesCompletionBlock);
};

10.初始化 tryToPerformUpdatetryToPerformUpdate 会把之前设置好的 block ,设置一个 try-catch ,防止崩溃,根据边界情况判断是否需要 fallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void (^tryToPerformUpdate)(IGListIndexSetResult *) = ^(IGListIndexSetResult *result){
    if (!IGListExperimentEnabled(experiments, IGListExperimentSkipLayoutBeforeUpdate)) {
        [collectionView layoutIfNeeded];
    }

    @try {
        if (collectionView.dataSource == nil) {
            fallbackWithoutUpdates();
        } else if (result.changeCount > 100 && allowsReloadingOnTooManyUpdates) {
            reloadDataFallback();
        } else {
            performUpdate(result);
        }
    } @catch (NSException *exception) {
        [delegate listAdapterUpdater:self
                      collectionView:collectionView
              willCrashWithException:exception
                         fromObjects:fromObjects
                           toObjects:toObjects
                          diffResult:result
                             updates:(id)self.applyingUpdateData];
        @throw exception;
    }
};

11.最后通过 diff 算法计算出 IGListIndexSetResult ,调用 tryToPerformUpdate(result)

1
2
3
4
5
6
const BOOL onBackgroundThread = IGListExperimentEnabled(experiments, IGListExperimentBackgroundDiffing);
[delegate listAdapterUpdater:self willDiffFromObjects:fromObjects toObjects:toObjects];
IGListAdapterUpdaterPerformDiffing(fromObjects, toObjects, IGListDiffEquality, experiments, onBackgroundThread, ^(IGListIndexSetResult *result){
    [delegate listAdapterUpdater:self didDiffWithResults:result onBackgroundThread:onBackgroundThread];
    tryToPerformUpdate(result);
});

调用顺序如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 使用 fromObjects 和 toObjects 计算出 diff
IGListAdapterUpdaterPerformDiffing -> 
// 判断是否需要执行更新,
tryToPerformUpdate ->
// 执行更新
performUpdate -> 
// 调用 `UICollectionView` 的 `performBatchUpdates` 方法
IGListAdapterUpdaterPerformBatchUpdate -> 
// batchUpdatesBlock 中执行 executeUpdateBlocks
batchUpdatesBlock -> 
// 执行 UICollectionView section 和 items 的相关操作
[UICollectionView section  items 操作]

完成刷新后调用相关的 block

1
batchUpdatesCompletionBlock -> executeCompletionBlocks 

IGListReloadDataUpdater

除了支持批量刷新的 IGListAdapterUpdaterIGListKit 还提供了仅支持全局刷新的 IGListReloadDataUpdater ,实现非常简单,且执行的是 [UICollectionView reloadData] 。其所有 IGListUpdatingDelegate 的相关方法都会调用 _synchronousReloadDataWithCollectionView: 方法进行更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
                            fromObjects:(NSArray *)fromObjects
                         toObjectsBlock:(IGListToObjectBlock)toObjectsBlock
                               animated:(BOOL)animated
                  objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock
                             completion:(IGListUpdatingCompletion)completion {
    if (toObjectsBlock != nil) {
        NSArray *toObjects = toObjectsBlock() ?: @[];
        objectTransitionBlock(toObjects);
    }
    [self _synchronousReloadDataWithCollectionView:collectionViewBlock()];
    if (completion) {
        completion(YES);
    }
}

- (void)_synchronousReloadDataWithCollectionView:(UICollectionView *)collectionView {
    [collectionView reloadData];
    [collectionView layoutIfNeeded];
}

可以看到实现非常简单,如果只需要进行 reloadData ,可以使用 IGListReloadDataUpdater 替换掉 IGListAdapterUpdater

Diff

在 iOS 还没有系统级地支持 Diff 特性的年代,在使用 UITableView/UICollectionView 时,当数据源发生变化,我们就需要手动根据数据源计算出变化的 NSIndexPaths 并进行更新,这个方法的时间复杂度一般是 O(n^2) ,在遍历旧数据内对新数据进行遍历,或者说直接 reloadData ,在 UITableView/UICollectionView 的复用机制下,只需要重新生成显示在屏幕的 Cell ,所带来的影响只是丢失了动画。而 IGListKitIGListDiff 可以在时间复杂度 O(n) 的前提下为我们计算出对应的 NSIndexPaths 简单易易用,再也不需要直接 reloadData 。下面来说说 IGListDiff 的核心实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// 记录 Diff 所需要的状态
struct IGListEntry {
    /// 记录旧数组中具有相同 hash 值的对象出现次数
    NSInteger oldCounter = 0;
    /// 记录新数组中具有相同 hash 值的对象出现次数
    NSInteger newCounter = 0;
    /// The indexes of the data in the old array
    /// 记录旧数组中当前 hash 对应的对象出现的位置 
    stack<NSInteger> oldIndexes;
    /// 数据是否有更新
    BOOL updated = NO;
};

/// 记录 IGListEntry 和位置( index ), index 默认为 NSNotFound
struct IGListRecord {
    IGListEntry *entry;
    mutable NSInteger index;

    IGListRecord() {
        entry = NULL;
        index = NSNotFound;
    }
};

1.首先先获取新旧数据所对应的数量: newCountoldCount ,然后创建新旧数据所对应的 NSMapTable

1
2
3
4
5
6
7
8
9
10
11
static id IGListDiffing(BOOL returnIndexPaths,
                        NSInteger fromSection,
                        NSInteger toSection,
                        NSArray<id<IGListDiffable>> *oldArray,
                        NSArray<id<IGListDiffable>> *newArray,
                        IGListDiffOption option) {
    const NSInteger newCount = newArray.count;
    const NSInteger oldCount = oldArray.count;

    NSMapTable *oldMap = [NSMapTable strongToStrongObjectsMapTable];
    NSMapTable *newMap = [NSMapTable strongToStrongObjectsMapTable];

2.如果 newCount 为 0 ,那么就是说 oldArray 的所有数据都需要进行删除,那么我们可以尽早返回,生成一个删除所有数据的 IGListIndexPathResult/IGListIndexSetResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (newCount == 0) {
    if (returnIndexPaths) {
        return [[IGListIndexPathResult alloc] initWithInserts:[NSArray new]
                                                      deletes:indexPathsAndPopulateMap(oldArray, fromSection, oldMap)
                                                      updates:[NSArray new]
                                                        moves:[NSArray new]
                                              oldIndexPathMap:oldMap
                                              newIndexPathMap:newMap];
    } else {
        [oldArray enumerateObjectsUsingBlock:^(id<IGListDiffable> obj, NSUInteger idx, BOOL *stop) {
            addIndexToMap(returnIndexPaths, fromSection, idx, obj, oldMap);
        }];
        return [[IGListIndexSetResult alloc] initWithInserts:[NSIndexSet new]
                                                     deletes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, oldCount)]
                                                     updates:[NSIndexSet new]
                                                       moves:[NSArray new]
                                                 oldIndexMap:oldMap
                                                 newIndexMap:newMap];
    }
}

3.如果 oldCount 为 0 ,那么就是说 newArray 的所有数据都需要进行插入,那么我们可以尽早返回,生成一个插入所有数据的 IGListIndexPathResult/IGListIndexSetResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (oldCount == 0) {
    if (returnIndexPaths) {
        return [[IGListIndexPathResult alloc] initWithInserts:indexPathsAndPopulateMap(newArray, toSection, newMap)
                                                      deletes:[NSArray new]
                                                      updates:[NSArray new]
                                                        moves:[NSArray new]
                                              oldIndexPathMap:oldMap
                                              newIndexPathMap:newMap];
    } else {
        [newArray enumerateObjectsUsingBlock:^(id<IGListDiffable> obj, NSUInteger idx, BOOL *stop) {
            addIndexToMap(returnIndexPaths, toSection, idx, obj, newMap);
        }];
        return [[IGListIndexSetResult alloc] initWithInserts:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newCount)]
                                                     deletes:[NSIndexSet new]
                                                     updates:[NSIndexSet new]
                                                       moves:[NSArray new]
                                                 oldIndexMap:oldMap
                                                 newIndexMap:newMap];
    }
}

4.如果 newCountoldCount 都不为 0 ,那么就可以进行 Diff 计算,首先需要创建一个 table ,使用 diffIdentifierkeyIGListEntryvalue ,这里使用 unordered_map ,因为会比 NSDictionary 快很多。

1
unordered_map<id<NSObject>, IGListEntry, IGListHashID, IGListEqualID> table;

5.遍历 newArray 的数据,获取对应的 entryentryoldIndexes 插入 NSNotFound ,且设置到 newResultsArray 中:

1
2
3
4
5
6
7
8
9
10
vector<IGListRecord> newResultsArray(newCount);
for (NSInteger i = 0; i < newCount; i++) {
    id<NSObject> key = IGListTableKey(newArray[i]);
    IGListEntry &entry = table[key];
    entry.newCounter++;

    entry.oldIndexes.push(NSNotFound);

    newResultsArray[i].entry = &entry;
}

6.遍历 oldArray 的数据,获取对应的 entryentryoldIndexes 插入对应的位置 i ,且设置到 oldResultsArray 中:

1
2
3
4
5
6
7
8
9
10
vector<IGListRecord> oldResultsArray(oldCount);
for (NSInteger i = oldCount - 1; i >= 0; i--) {
    id<NSObject> key = IGListTableKey(oldArray[i]);
    IGListEntry &entry = table[key];
    entry.oldCounter++;

    entry.oldIndexes.push(i);

    oldResultsArray[i].entry = &entry;
}

7.处理那些在 oldArraynewArray 中都有出现的数据,遍历 newResultsArray ,根据 entry 的数据进行对比:

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
for (NSInteger i = 0; i < newCount; i++) {
    IGListEntry *entry = newResultsArray[i].entry;
		
    // 1. 获取原始位置 originalIndex , 如果 item 是插入的数据,那么 originalIndex 就为 NSNotFound
    const NSInteger originalIndex = entry->oldIndexes.top();
    entry->oldIndexes.pop();

    if (originalIndex < oldCount) {
        // 获取新旧数据的对象
        const id<IGListDiffable> n = newArray[i];
        const id<IGListDiffable> o = oldArray[originalIndex];
        switch (option) {
            case IGListDiffPointerPersonality:
                // 只通过指针进行比较
                if (n != o) {
                    entry->updated = YES;
                }
                break;
            case IGListDiffEquality:
                // 使用 `-[IGListDiffable isEqualToDiffableObject:]` 进行比较,以 n 和 o 指向的对象不同为前提
                if (n != o && ![n isEqualToDiffableObject:o]) {
                    entry->updated = YES;
                }
                break;
        }
    }
    if (originalIndex != NSNotFound
        && entry->newCounter > 0
        && entry->oldCounter > 0) {
        // 如果在 `newArray` 和 `oldArray` 中都出现,则进行位置的双向绑定
        newResultsArray[i].index = originalIndex;
        oldResultsArray[originalIndex].index = i;
    }
}

8.创建需要记录数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 存储最后的 NSIndexPaths 或者 indexes
id mInserts, mMoves, mUpdates, mDeletes;
if (returnIndexPaths) {
    mInserts = [NSMutableArray<NSIndexPath *> new];
    mMoves = [NSMutableArray<IGListMoveIndexPath *> new];
    mUpdates = [NSMutableArray<NSIndexPath *> new];
    mDeletes = [NSMutableArray<NSIndexPath *> new];
} else {
    mInserts = [NSMutableIndexSet new];
    mMoves = [NSMutableArray<IGListMoveIndex *> new];
    mUpdates = [NSMutableIndexSet new];
    mDeletes = [NSMutableIndexSet new];
}
// 追踪删除的 items 的偏移量来计算 items 的移动位置vector<NSInteger> deleteOffsets(oldCount), insertOffsets(newCount);
NSInteger runningOffset = 0;

9.计算删除的数据:

for (NSInteger i = 0; i < oldCount; i++) {
    deleteOffsets[i] = runningOffset;
    const IGListRecord record = oldResultsArray[i];
    // 如果 `record.index` 为 `NSNotFound` ,则表示其没有在 `newArray` 中出现,已被删除
    if (record.index == NSNotFound) {
        addIndexToCollection(returnIndexPaths, mDeletes, fromSection, i);
        runningOffset++;
    }

    addIndexToMap(returnIndexPaths, fromSection, i, oldArray[i], oldMap);
}

11.最后的计算

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
for (NSInteger i = 0; i < newCount; i++) {
    insertOffsets[i] = runningOffset;
    const IGListRecord record = newResultsArray[i];
    const NSInteger oldIndex = record.index;
    // 如果 `record.index` 为 `NSNotFound` ,则表示其没有在 `oldArray` 中出现,是新插入的
    if (record.index == NSNotFound) {
        addIndexToCollection(returnIndexPaths, mInserts, toSection, i);
        runningOffset++;
    } else {
        // 如果 record.entry-> updated 为 YES ,则表示
        if (record.entry->updated) {
            addIndexToCollection(returnIndexPaths, mUpdates, fromSection, oldIndex);
        }
        // 计算 indexes 是否匹配,据此来判断是否需要移动
		  // oldIndex - deleteOffset + insertOffset != i ,则位置发生变化,需要移动。
        const NSInteger insertOffset = insertOffsets[i];
        const NSInteger deleteOffset = deleteOffsets[oldIndex];
        if ((oldIndex - deleteOffset + insertOffset) != i) {
            id move;
            if (returnIndexPaths) {
                NSIndexPath *from = [NSIndexPath indexPathForItem:oldIndex inSection:fromSection];
                NSIndexPath *to = [NSIndexPath indexPathForItem:i inSection:toSection];
                move = [[IGListMoveIndexPath alloc] initWithFrom:from to:to];
            } else {
                move = [[IGListMoveIndex alloc] initWithFrom:oldIndex to:i];
            }
            [mMoves addObject:move];
        }
    }

    addIndexToMap(returnIndexPaths, toSection, i, newArray[i], newMap);
}

12.完成计算,返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (returnIndexPaths) {
    return [[IGListIndexPathResult alloc] initWithInserts:mInserts
                                                  deletes:mDeletes
                                                  updates:mUpdates
                                                    moves:mMoves
                                          oldIndexPathMap:oldMap
                                          newIndexPathMap:newMap];
} else {
    return [[IGListIndexSetResult alloc] initWithInserts:mInserts
                                                 deletes:mDeletes
                                                 updates:mUpdates
                                                   moves:mMoves
                                             oldIndexMap:oldMap
                                             newIndexMap:newMap];
}

从上面的计算可以看出需要 5 次 for 循环进行遍历,也就是时间复杂度为 O(5n) ,在 n 足够大的情况下可以忽略,时间复杂度可视作 O(n)

这篇文章有使用两个数组作为例子进行说明 IGListDiff 是如何进行计算的: IGListKit diff 实现简析

This post is licensed under CC BY 4.0 by the author.

IGListKit 的管理者 - IGListAdapter

IGListKit - 完结篇

Comments powered by Disqus.