【iOS开发】UICollectionView:Invali

1、问题背景

今天收到了如下的 线上Crash:

image-20230719162721476.png

2、复现问题

冗长的堆栈信息,核心内容如下:

OS Version: iPhone OS 16.5.1 (20F75)
Hardware Model: iPhone12,1
Launch Time: 2023-07-19 09:38:13
Date/Time: 2023-07-19 09:53:35
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (5) must be equal to the number of items contained in that section before the update (3), plus or minus the number of items inserted or deleted from that section (2 inserted, 2 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out). Collection view: <UICollectionView: 0x106b56000; frame = (0 32; 414 279); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x2830be5e0>; backgroundColor = UIExtendedGrayColorSpace 0 0; layer = <CALayer: 0x28387c540>; contentOffset: {0, 0}; contentSize: {1122, 279}; adjustedContentInset: {0, 0, 0, 0}; layout: <UIPrintPreviewFlowLayout: 0x1280625d0>; dataSource: <UIPrintPreviewViewController: 0x106970a00>>
Last Exception Backtrace:
0 CoreFoundation 0x00000001a699ccc0 0x1a6993000 + 40128
1 libobjc.A.dylib bool method_lists_contains_any<method_list_t*>(method_list_t**, method_list_t**, objc_selector**, unsigned long) (in libobjc.A.dylib) 1503
2 Foundation 0x00000001a113a56c 0x1a0c59000 + 5117292
3 UIKitCore -[UICollectionView _Bug_Detected_In_Client_Of_UICollectionView_Invalid_Number_Of_Items_In_Section:] (in UIKitCore) 95
4 UIKitCore -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:collectionViewAnimator:] (in UIKitCore) 9915
5 UIKitCore -[UICollectionView _updateRowsAtIndexPaths:updateAction:updates:] (in UIKitCore) 395
6 UIKitCore -[UICollectionView reloadItemsAtIndexPaths:] (in UIKitCore) 51

当看到 -[UICollectionView reloadItemsAtIndexPaths:] ,就大概知道怎么复现了,然后简单调试后,写了如下代码,可以稳定复现:

#import "AMKCrashByInvalidUpdateCollectionViewController.h"@interface AMKCrashByInvalidUpdateCollectionViewController () <UICollectionViewDelegateFlowLayout, UICollectionViewDataSource>
@property (nonatomic, strong, readwrite, nullable) UICollectionView *collectionView;
@property (nonatomic, strong, readwrite, nullable) NSMutableArray<NSMutableArray *> *dataSource;
@end@implementation AMKCrashByInvalidUpdateCollectionViewController
​
+ (void)load {
    id __block token = [NSNotificationCenter.defaultCenter addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull note) {
        [NSNotificationCenter.defaultCenter removeObserver:token];
        [(UINavigationController *)UIApplication.sharedApplication.delegate.window.rootViewController pushViewController:self.new animated:YES];
    }];
}
​
#pragma mark - Dealloc
​
- (void)dealloc {
    
}
​
#pragma mark - Init Methods#pragma mark - Life Circle
​
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = self.view.backgroundColor?:[UIColor whiteColor];
    self.navigationItem.rightBarButtonItems = @[
        [UIBarButtonItem.alloc initWithTitle:@"Crash0" style:UIBarButtonItemStylePlain target:self action:@selector(crashByInvalidUpdate_0:)],
        [UIBarButtonItem.alloc initWithTitle:@"Crash1" style:UIBarButtonItemStylePlain target:self action:@selector(crashByInvalidUpdate_1:)],
        [UIBarButtonItem.alloc initWithTitle:@"Crash2" style:UIBarButtonItemStylePlain target:self action:@selector(crashByInvalidUpdate_2:)],
        [UIBarButtonItem.alloc initWithTitle:@"Crash3" style:UIBarButtonItemStylePlain target:self action:@selector(crashByInvalidUpdate_3:)],
        [UIBarButtonItem.alloc initWithTitle:@"Reload" style:UIBarButtonItemStylePlain target:self.collectionView action:@selector(reloadData)]
    ];
    [self.collectionView reloadData];
}
​
#pragma mark - Getters & Setters
​
- (UICollectionView *)collectionView {
    if(!_collectionView) {
        UICollectionViewFlowLayout *collectionViewFlowLayout = [UICollectionViewFlowLayout.alloc init];
        _collectionView = [UICollectionView.alloc initWithFrame:self.view.bounds collectionViewLayout:collectionViewFlowLayout];
        _collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        _collectionView.backgroundColor = [UIColor whiteColor];
        _collectionView.dataSource = self;
        _collectionView.delegate = self;
        [_collectionView registerClass:UICollectionViewCell.class forCellWithReuseIdentifier:NSStringFromClass(UICollectionViewCell.class)];
        [_collectionView registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:NSStringFromClass(UICollectionReusableView.class)];
        [_collectionView registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:NSStringFromClass(UICollectionReusableView.class)];
        [self.view addSubview:_collectionView];
    }
    return _collectionView;
}
​
#pragma mark - Data & Networking
​
- (NSMutableArray<NSMutableArray *> *)dataSource {
    if (!_dataSource) {
        _dataSource = @[].mutableCopy;
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
        [_dataSource addObject:@[@0, @1, @2].mutableCopy];
    }
    return _dataSource;
}
​
#pragma mark - Layout Subviews#pragma mark - Action Methods// 2023-07-19 11:43:35.418265+0800 AMKCategories_Example[29884:11021065] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (5) must be equal to the number of items contained in that section before the update (3), plus or minus the number of items inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out). Collection view: <UICollectionView: 0x106020800; frame = (0 0; 414 808); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x282268c60>; backgroundColor = <UIDynamicSystemColor: 0x2839a1ec0; name = systemBackgroundColor>; layer = <CALayer: 0x282c946a0>; contentOffset: {0, 0}; contentSize: {414, 50}; adjustedContentInset: {0, 0, 34, 0}; layout: <UICollectionViewFlowLayout: 0x105d55900>; dataSource: <AMKCrashByInvalidUpdateCollectionViewController: 0x105f21440>>'
- (void)crashByInvalidUpdate_0:(id)sender {
    [self.dataSource[0] addObjectsFromArray:@[@3, @4]];
    [self.collectionView reloadItemsAtIndexPaths:@[[NSIndexPath indexPathForRow:5 inSection:0]]];
}
​
- (void)crashByInvalidUpdate_1:(id)sender {
    [self.dataSource[0] addObjectsFromArray:@[@3, @4]];
    [self.collectionView reloadItemsAtIndexPaths:@[[NSIndexPath indexPathForRow:5 inSection:1]]];
}
​
- (void)crashByInvalidUpdate_2:(id)sender {
    [self.dataSource[0] addObjectsFromArray:@[@3, @4]];
    [self.collectionView reloadItemsAtIndexPaths:@[]];
}
​
// 2023-07-19 11:18:20.410828+0800 AMKCategories_Example[29850:11010021] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid batch updates detected: the number of sections and/or items returned by the data source before and/or after performing the batch updates are inconsistent with the updates.
- (void)crashByInvalidUpdate_3:(id)sender {
    [self.dataSource[0] addObjectsFromArray:@[@3, @4]];
    [self.collectionView reloadItemsAtIndexPaths:@[[NSIndexPath indexPathForRow:5 inSection:0]]];
}
​
#pragma mark - Notifications#pragma mark - KVO#pragma mark - Protocol#pragma mark UICollectionViewDataSource
​
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return self.dataSource.count;
}
​
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return self.dataSource[section].count;
}
​
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass(UICollectionViewCell.class) forIndexPath:indexPath];
    cell.contentView.backgroundColor = [UIColor colorWithRed:arc4random()%255/255.0 green:arc4random()%255/255.0 blue:arc4random()%255/255.0 alpha:0.2];
    return cell;
}
​
#pragma mark UICollectionViewDelegate
​
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    [collectionView deselectItemAtIndexPath:indexPath animated:YES];
    NSLog(@"%@", indexPath);
}
​
#pragma mark - Helper Methods@end

3、定位问题

经过上述 4个 case 的验证,确认:当通过 -reloadItemsAtIndexPaths: 方法局部刷新时,只要「当前 collectionView 与数据源 的“section 个数”、“各 section 中 item 的个数” 不一致」,就会引起 crash

—— 即便参数为「空数组」,即便「只有 section A 的 items 个数对不上,但是刷新的 section B」,都会 Crash

4、解决方案

有了上述结论,就很容易给出解决方案了:

—— 在局部刷新时,校验下「当前 collectionView 与数据源 的“section 个数”、“各 section 中 item 的个数”」是否一致

  • 若一致,则不做干预,继续局部刷新
  • 若不一致,则刷新整个视图

整理代码如下:

#import "UICollectionView+AMKCrashProtectorForInvalidUpdate.h"
#import <AMKCategories/NSObject+AMKMethodSwizzling.h>@implementation UICollectionView (AMKCrashProtectorForInvalidUpdate)
​
+ (void)load {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self amk_swizzleInstanceMethod:@selector(reloadItemsAtIndexPaths:) withMethod:@selector(AMKCrashProtector_reloadItemsAtIndexPaths:)];
    });
}
​
- (void)AMKCrashProtector_reloadItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
    NSException *exception = nil;
    
    // 若「section 数量不一致」,则直接刷新
    NSInteger oldNumberOfSections = self.numberOfSections;
    NSInteger newNumberOfSections = [self.dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)] ? [self.dataSource numberOfSectionsInCollectionView:self] : oldNumberOfSections;
    if (oldNumberOfSections != newNumberOfSections) {
        NSString *reason = [NSString stringWithFormat:@"Invalid update: 当前有 %ld 个 section,但 dataSource 有 %ld 个。Collection view: %@", oldNumberOfSections, newNumberOfSections, self.description];
        exception = [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:@{}];
    }
    // 否则逐个验证「section 的 items 数量是否一致」,只要有不一致的,则直接刷新
    else if (self.dataSource) {
        for (NSInteger section = 0; section < self.numberOfSections; section++) {
            NSInteger oldNumberOfItemsInSection = [self numberOfItemsInSection:section];
            NSInteger newNumberOfItemsInSection = [self.dataSource collectionView:self numberOfItemsInSection:section];
            if (oldNumberOfItemsInSection != newNumberOfItemsInSection) {
                NSString *reason = [NSString stringWithFormat:@"Invalid update: section %ld 当前有 %ld 个 items,但 dataSource 有 %ld 个。Collection view: %@", section, oldNumberOfItemsInSection, newNumberOfItemsInSection, self.description];
                exception = [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:@{}];
                break;
            }
        }
    }
        
    if (exception) {
        @try {
            [exception raise];
        } @catch (NSException *exception) {
            // 此处直接弹窗警告了,正式项目中,可以直接上报
            NSString *title = @"CrashProtector";
            NSString *message = [NSString stringWithFormat:@"检测到无效的 indexPath,已整体刷新:%@", exception];
            UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
            [alertController addAction:[UIAlertAction actionWithTitle:@"好的" style:UIAlertActionStyleCancel handler:nil]];
            [UIApplication.sharedApplication.delegate.window.rootViewController presentViewController:alertController animated:YES completion:nil];
            NSLog(@"%@ => %@", exception, exception.userInfo);
        }
        
        [self reloadData];
    } else {
        [self AMKCrashProtector_reloadItemsAtIndexPaths:indexPaths];
    }
}
​
@end

运行效果如下:

4731dd46896500a80fc647d36a8cdd04.jpg

全部评论

相关推荐

不愿透露姓名的神秘牛友
昨天 17:17
点赞 评论 收藏
分享
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务