美文网首页iOS
IGListKit 源码解析

IGListKit 源码解析

作者: 806349745123 | 来源:发表于2019-11-11 21:45 被阅读0次

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

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


    image.png

    可以看出 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.png

    可以看出来,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));
    

    相关文章

      网友评论

        本文标题:IGListKit 源码解析

        本文链接:https://www.haomeiwen.com/subject/tjyxictx.html