阅读 522

IGListKit 源码解析

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

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

image-20191105193430090

可以看出 IGListKit 都是基于 IGListAdapter 进行数据传递和 UI 刷新的操作,接下来从 IGListAdapter 入手分析 IGListKit 具体做了哪些工作。

IGListAdapter

初始化:

- (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。内部实现结果如下图:

graph LR
object -- objectToSectionControllerMap --> IGListSectionController
IGListSectionController -- objectToSectionControllerMap --> object
IGListSectionController -- sectionControllerToSectionMap --> section
section -- sectionControllerToSectionMap --> IGListSectionController
复制代码

IGListDisplayHandler: 作用和对外暴露的 IGListAdapterPerformanceDelegate 类似,主要是对 UICollectionViewCell 生命周日相关对调的处理(cell 显示/消失/分区头部、尾部显示/消失),内部会把事件传给 IGListSectionController 的 displayDelegate;在 IGListAdapter+UICollectionView.m 文件中进行调用。

IGListWorkingRangeHandler: 负责 collectionView 每个 section(sectionController) 的预加载的准备工作。在 IGListAdapter+UICollectionView.m 文件中进行调用,相关数据会保存起来,提供给 IGListAdapter 使用。

IGListAdapterUpdateListener: 代理集合,IGListAdapter 更新完数据后对集合的代理进行通知

数据源:

IGListAdapter 会作为 UICollectionView 默认的 dataSource。

- (void)setCollectionView:(UICollectionView *)collectionView {
    if (_collectionView != collectionView || _collectionView.dataSource != self) {
        static NSMapTable<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 对应的数据

// 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 提供以下几种方法让外部进行数据更新:

- (void)performUpdatesAnimated:(BOOL)animated completion:(nullable IGListUpdaterCompletion)completion;

- (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion;

- (void)reloadObjects:(NSArray *)objects;
复制代码

我们先以 -reloadDataWithCompletion: 方法为例子,分析数据更新的过程:

- (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion {
    IGAssertMainThread();

    id<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 的方法进行刷新数据

- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
                   reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock
                          completion:(nullable IGListUpdatingCompletion)completion {
    IGAssertMainThread();
    IGParameterAssert(collectionViewBlock != nil);
    IGParameterAssert(reloadUpdateBlock != nil);

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

    self.reloadUpdates = reloadUpdateBlock;
    self.queuedReloadData = YES;
    [self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
}
复制代码
- (void)_queueUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
    IGAssertMainThread();
    
    __weak __typeof__(self) weakSelf = self;
    
    // dispatch_async 是为了执行 -performBatchUpdatesWithCollectionViewBlock: 前提供更多时间来完成数据更新处理,减少在主线程上进行差异化的操作
    dispatch_async(dispatch_get_main_queue(), ^{
        if (weakSelf.state != IGListBatchUpdateStateIdle
            || ![weakSelf hasChanges]) {
            return;
        }
        
        if (weakSelf.hasQueuedReloadData) {
            [weakSelf performReloadDataWithCollectionViewBlock:collectionViewBlock];
        } else {
            [weakSelf performBatchUpdatesWithCollectionViewBlock:collectionViewBlock];
        }
    });
}
复制代码

之后进入条件判断执行 -performReloadDataWithCollectionViewBlock: 方法

- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
    IGAssertMainThread();
    
    id<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:

- (void)performUpdatesAnimated:(BOOL)animated completion:(IGListUpdaterCompletion)completion {
    // 略...
    [self _enterBatchUpdates];
    [self.updater performUpdateWithCollectionViewBlock:[self _collectionViewBlock]
                                           fromObjects:fromObjects
                                        toObjectsBlock:toObjectsBlock
                                              animated:animated
                                 objectTransitionBlock:^(NSArray *toObjects) {
                                     // 重新捕获一次 sectionMap,防止同时间有数据被删除
                                     weakSelf.previousSectionMap = [weakSelf.sectionMap copy   
                                     // 更新 sectionMap 数据,刷新 collectiView 背景图
                                     [weakSelf _updateObjects:toObjects dataSource:dataSource];
                                 } completion:^(BOOL finished) {
                                     // release the previous items
                                     weakSelf.previousSectionMap = nil;

                                     [weakSelf _notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated];
                                     if (completion) {
                                         completion(finished);
                                     }
                                     [weakSelf _exitBatchUpdates];
                                 }];
}
复制代码

updater 会将更新数据 sectionMap 的操作保存到 objectTransitionBlock 中

- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock
                            fromObjects:(NSArray *)fromObjects
                         toObjectsBlock:(IGListToObjectBlock)toObjectsBlock
                               animated:(BOOL)animated
                  objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock
                             completion:(IGListUpdatingCompletion)completion {
    IGAssertMainThread();
    IGParameterAssert(collectionViewBlock != nil);
    IGParameterAssert(objectTransitionBlock != nil);

    // 正在执行更新的过程中,同一时间内可能会有多个其他更新任务加入,
    // 执行更新动作的时候,是第一次加入的 fromObject 和 最后加入的 toObjects
    // 如果 self.fromObject == nil, 应该有先使用之前加入并且还没有执行的 batch update 任务的终点数据源(toObjects)
    // 这样做的目的是使整个数据变化可以串联起来
    self.fromObjects = self.fromObjects ?: self.pendingTransitionToObjects ?: fromObjects;
    self.toObjectsBlock = toObjectsBlock;

    // disabled animations will always take priority
    // reset to YES in -cleanupState
    self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated;

    // 保证每次刷新使用最新的 objectTransitionBlock
    self.objectTransitionBlock = objectTransitionBlock;

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

    [self _queueUpdateWithCollectionViewBlock:collectionViewBlock];
}
复制代码

IGListUpdater 处理完传入的 fromObjects 和 toObjects,并保存数据转化的闭包 objectTransitionBlock,会调用 -_queueUpdateWithCollectionViewBlock: 方法,利用 dispatch_async 异步调用 -performBatchUpdatesWithCollectionViewBlock:

- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock {
   IGAssertMainThread();
   IGAssert(self.state == IGListBatchUpdateStateIdle, @"Should not call batch updates when state isn't idle");

   // 创建局部变量,以便我们可以立即清除状态,但将这些数据传递到批处理更新任务中
   id<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);
   }
}
复制代码

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

|---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 属性的默认代理

@protocol IGListCollectionViewDelegateLayout <UICollectionViewDelegateFlowLayout>
  
@interface IGListAdapter (UICollectionView)
<
UICollectionViewDataSource,
IGListCollectionViewDelegateLayout
>
复制代码

IGListAdapter 会实现相关代理方法,进行对 cell 级别的视图管理,包含视图 UICollectionView 滚动,cell 大小、cell 显示等事件,并通过 IGListAdapterPerformanceDelegate 对外通知

- (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
  //...略
  [performanceDelegate listAdapter:self didCallSizeOnSectionController:sectionController atIndex:indexPath.item];
  //...略
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    id<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 。在这个方法中会判断拖动开始/结束位置,根据不同的情况进行数据刷新

- (void)collectionView:(UICollectionView *)collectionView
   moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath
           toIndexPath:(NSIndexPath *)destinationIndexPath {

    if (@available(iOS 9.0, *)) {
        const NSInteger sourceSectionIndex = sourceIndexPath.section;
        const NSInteger destinationSectionIndex = destinationIndexPath.section;
        const NSInteger sourceItemIndex = sourceIndexPath.item;
        const NSInteger destinationItemIndex = destinationIndexPath.item;

        IGListSectionController *sourceSectionController = [self sectionControllerForSection:sourceSectionIndex];
        IGListSectionController *destinationSectionController = [self sectionControllerForSection:destinationSectionIndex];

        if (sourceSectionController == destinationSectionController) {

            if ([sourceSectionController canMoveItemAtIndex:sourceItemIndex toIndex:destinationItemIndex]) {
                // 同一个 section 内的挪动
                [self moveInSectionControllerInteractive:sourceSectionController
                                               fromIndex:sourceItemIndex
                                                 toIndex:destinationItemIndex];
            } else {
                // 撤销修改
                [self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
            }
            return;
        }

        // 跨 section 移动, 如果 section 的 item 数目为1
        if ([sourceSectionController numberOfItems] == 1 && [destinationSectionController numberOfItems] == 1) {

            [self moveSectionControllerInteractive:sourceSectionController
                                         fromIndex:sourceSectionIndex
                                           toIndex:destinationSectionIndex];
            return;
        }

        // 撤销修改
        [self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
    }
}
复制代码

成功拖动之后会触发 IGListUpdater 的 -moveInSectionControllerInteractive 或者 -moveSectionControllerInteractive:fromIndex:toIndex,在同一个 UICollectionView section 中拖动则触发前者,跨 section 之间则后者

- (void)moveInSectionControllerInteractive:(IGListSectionController *)sectionController
                                 fromIndex:(NSInteger)fromIndex
                                   toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0) {
  	//... 略
    [sectionController moveObjectFromIndex:fromIndex toIndex:toIndex];
}
复制代码

在同一个 section 中拖动 UICollectionViewCell 比较简单,实现中回去调用对应 sectionController 的 -moveObjectFromIndex:toIndex:,使用者在自定义的 sectionController 中实现该代理方法,进行对应的数据刷新更新即可

- (void)moveSectionControllerInteractive:(IGListSectionController *)sectionController
                               fromIndex:(NSInteger)fromIndex
                                 toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0) {
    // ... 略
    if (fromIndex != toIndex) {
        id<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: 方法

- (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 拖动的调用栈大概为:

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

- (void)_willDisplayReusableView:(UICollectionReusableView *)view
                 forListAdapter:(IGListAdapter *)listAdapter
              sectionController:(IGListSectionController *)sectionController
                         object:(id)object
                      indexPath:(NSIndexPath *)indexPath {
    IGParameterAssert(view != nil);
    IGParameterAssert(listAdapter != nil);
    IGParameterAssert(object != nil);
    IGParameterAssert(indexPath != nil);

    [self.visibleViewObjectMap setObject:object forKey:view];
    NSCountedSet *visibleListSections = self.visibleListSections;
    if ([visibleListSections countForObject:sectionController] == 0) {
        [sectionController.displayDelegate listAdapter:listAdapter willDisplaySectionController:sectionController];
        [listAdapter.delegate listAdapter:listAdapter willDisplayObject:object atIndex:indexPath.section];
    }
    [visibleListSections addObject:sectionController];
}
复制代码

IGListKit 中的 IGListDisplayHandler 利用 NSCountedSet 记录 UICollectionView section 的显示状态,旨在通知外部每个 section 的显示/消失事件。

prefetchingEnabled

当调用 collectionView:didEndDisplayingCell:forItemAtIndexPath: 后,cell 不会立刻进入复用队列,系统会keeps it around for a bit。相当于会缓存该 cell 一小段时间,在这段时间内如果该 cell 再次回到屏幕中,便不会重新调用 cellForItemAtIndexPath:,而是直接显示。

至于系统会缓存多久,官方并没有给出明确的时间,感觉跟程序运行时开销有关。

如果想关闭该功能,需要设置 collectionView.prefetchingEnabled = NO;

UICollectionViewLayoutInvalidationContext

当改变 UICollectionView item 的时候,通过调用 -invalidateLayout 方法让 UICollectionView 布局失效,通过 Invalidation Context 声明了在布局失效时布局的哪些部分需要被更新,布局对象就可以根据该信息减小重新计算的数据量。

IGListKit 提供了自定义的 IGListCollectionViewLayout 类来优化 UICollectionView 的刷新,IGListCollectionViewLayout 实现和 UICollectionViewLayoutInvalidationContext 相关的方法

@interface IGListCollectionViewLayoutInvalidationContext : UICollectionViewLayoutInvalidationContext
// 追加视图
@property (nonatomic, assign) BOOL ig_invalidateSupplementaryAttributes;
@property (nonatomic, assign) BOOL ig_invalidateAllAttributes;
@end
复制代码

IGListCollectionViewLayoutInvalidationContext 类继承了 UICollectionViewLayoutInvalidationContext,用于记录刷新布局相关逻辑

// -[UICollectionView setFrame:] / -[UICollectionView setBounds:] 会触发
- (UICollectionViewLayoutInvalidationContext *)invalidationContextForBoundsChange:(CGRect)newBounds {
    const CGRect oldBounds = self.collectionView.bounds;
    
    IGListCollectionViewLayoutInvalidationContext *context =
    (IGListCollectionViewLayoutInvalidationContext *)[super invalidationContextForBoundsChange:newBounds];
  	// 每次都需要刷新 追加视图
    context.ig_invalidateSupplementaryAttributes = YES;
    if (!CGSizeEqualToSize(oldBounds.size, newBounds.size)) {
      	// size 改变之后,必须进行全量刷新
        context.ig_invalidateAllAttributes = YES;
    }
    return context;
}
复制代码

-invalidationContextForBoundsChange: 当 UICollectionView 发生变化的时候(比如视图 frame 发生改变),在进行视图刷新之前,会触发该方法返回 UICollectionViewLayoutInvalidationContext 对象来告诉UICollectionView 布局刷新的相关信息。

// 根据 context 中的信息重新计算布局改变的部分。
// -[UICollectionView setDataSource:] / -[UICollectionView setFrame:] 会触发该方法
// 也可以主动调用,强制刷新
- (void)invalidateLayoutWithContext:(IGListCollectionViewLayoutInvalidationContext *)context {
    BOOL hasInvalidatedItemIndexPaths = NO;
    if ([context respondsToSelector:@selector(invalidatedItemIndexPaths)]) {
        hasInvalidatedItemIndexPaths = [context invalidatedItemIndexPaths].count > 0;
    }
    
  	// _minimumInvalidatedSection 用来记录指定从哪个 section 开始的布局失效,需要重新布局
    if (hasInvalidatedItemIndexPaths
        || [context invalidateEverything]
        || context.ig_invalidateAllAttributes) {
        // invalidates all
        _minimumInvalidatedSection = 0;
    } else if ([context invalidateDataSourceCounts] && _minimumInvalidatedSection == NSNotFound) {
      	// invalidateDataSourceCounts 标记 layout 需要重新从 UICollectionView 查询 section 和 item 数目
      	// UICollectionView 调用 -reloadData 或者插入/删除 item 的时候 invalidateDataSourceCounts = YES
      	// 如果 layout 需要重新 UICollectionView 的信息或者没有找到重新刷新的 section 启动,则刷新起点 section 默认为0
        _minimumInvalidatedSection = 0;
    }
    
    if (context.ig_invalidateSupplementaryAttributes) {
      	// 清空追加视图的布局信息缓存
        [self _resetSupplementaryAttributesCache];
    }
    
    [super invalidateLayoutWithContext:context];
}
复制代码

-invalidateLayoutWithContext: 方法在 UICollectionView 布局信息发生变化会被系统调用,IGListCollectionViewLayout 实现了该方法,在调用的过程中会对一些布局缓存进行更新(主要是缓存 UICollectionViewLayoutAttributes 对象),具体细节不再展开。

除此之外,UICollectionViewLayoutInvalidationContext 本身提供了几个方法,用户可以主动调用来进行局部 UI 刷新

// 调用此方法以标识布局中需要更新的特定单元格。 
// 指定的更新的所有 indexPath 对象将添加到属性 invalidatedItemIndexPaths 中。
- (void)invalidateItemsAtIndexPaths:(NSArray<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));
复制代码