草庐IT

UICollectionView iOS 13以下删除动画crash

落影loyinglin 2023-03-28 原文

问题反馈

线上突发一个Top1的crash告警,场景是UICollectionView在删除的时候触发。

错误方法deleteItemsAtIndexPaths,错误信息如下
NSException Invalid update: invalid number of items in section 3. The number of items contained in an existing section after the update (40) must be equal to the number of items contained in that section before the update (40), plus or minus the number of items inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).

问题分析

1、从crash原因可以知道是UICollectionView在deleteItem的时候前后的item数量一致;(正常应该是删除前40,删除39)
2、这是某个cell的删除逻辑;
3、用户点击cell跳转界面后,又触发了原来UICollectionView的cell删除动画;


从slardar(APM)的聚合信息,可以看到:
4、最后页面是并不是原来UICollectionView的界面;
5、同时所有crash版本为<=iOS 13版本。

通过上述信息和用户行为日志,可以猜测UICollectionView是在界面跳转之后触发删除动画导致crash。用iOS 12设备找到复现路径:
先正常触发UICollectionView的初始化和cell加载 => 从UICollectionView触发界面跳转,进入下一级界面 => 触发删除Cell的业务逻辑 => UICollectionView开始删除动画 => 出现crash。

下面是crash的具体代码:

- (void)bookShelfCollectionViewDeleteItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
    [self.collectionView performBatchUpdates:^{
        [self.collectionView deleteItemsAtIndexPaths:indexPaths];
    } completion:^(BOOL finished) {
    }];
}

问题归因

UICollectionView有一个逻辑是会把最近使用的cell排在最前面,于是从UICollectionView点击cell发生界面跳转后会触发UICollectionView刷新并调reloadData。但是这个reloadData并不会直接触发UICollectionView马上从dataSource和delegate去获取数据和UI,而是会等到UICollectionView展示的时候再进行触发。

然后在新界面触发某些业务逻辑,导致UICollectionView调用了deleteItemsAtIndexPaths进行cell的移除动画,此时就会产生crash。

问题修复

区分UICollectionView删除cell场景,如果是用户手动移除则会进行动画;如果是非手动触发删除则直接调用reloadData,不调用deleteItemsAtIndexPaths。

问题延伸

为什么iOS 13以上没有crash?

这是UICollectionView内部对动画前后的数量校验,iOS 12及以下的系统会有NSAssert的断言触发;iOS 13开始没有NSAssert,但是同样会有异常Log。如下:

[UICollectionView] Performing reloadData as a fallback — Invalid update: invalid number of items in section 0\. The number of items contained in an existing section after the update (17) must be equal to the number of items contained in that section before the update (17), plus or minus the number of items inserted or deleted from that section (0 inserted, 1 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: 0x7fd253012800; frame = (0 150; 375 567); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x60000351f480>; layer = <CALayer: 0x600003b186e0>; contentOffset: {0, 0}; contentSize: {375, 350}; adjustedContentInset: {0, 0, 49, 0}; layout: <UICollectionViewFlowLayout: 0x7fd251f0d5b0>; dataSource: <ViewController: 0x7fd251f07760>>

非当前界面调用reloadData,何时会回调cellForItemAtIndexPath?

界面出现的时候会触发layoutSubviews,此时会通过_updateVisibleCellsNow回调delegate。

UICollectionView为什么会有这个crash?

crash代码如下:

- (void)bookShelfCollectionViewDeleteItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
    [self.collectionView performBatchUpdates:^{
        [self.collectionView deleteItemsAtIndexPaths:indexPaths];
    } completion:^(BOOL finished) {
    }];
}

crash的原因是collectionView在执行deleteItemsAtIndexPaths:的时候,会对比删除前后section的item数量。假如原来item数量是20,移除了1个,那么之后的数量应该是19。

UICollectionView内部有一个关于item数量的缓存,在首次调用numberOfItemsInSection:之后会缓存这个结果值,后续继续调用numberOfItemsInSection:就不会回调dataSource去询问。

如下,只有count1会回调dataSouce,count2就直接用缓存的值。

[self.collectionView reloadData];
NSLog(@"count1:%d", [self.collectionView numberOfItemsInSection:0]); // 会回调dataSource询问
NSLog(@"count2:%d", [self.collectionView numberOfItemsInSection:0]); // 直接返回

当UICollectionView执行reloadData的时候,如果UICollectionView在当前界面会触发layoutSubviews,然后会调用_updateItemCounts更新这个缓存数据。

如果UICollectionView不在当前界面,此时这个缓存会失效,但此刻并不会马上调用dataSource的numberOfItemsInSection:。

[self.collectionView reloadData];
...
...
[self.datas removeObjectAtIndex:0];
[self.collectionView performBatchUpdates:^{
    [self.collectionView deleteItemsAtIndexPaths:@[[NSIndexPath indexPathForRow:0 inSection:0]]];
} completion:^(BOOL finished) {
}];

在后续的performBatchUpdates时候才会回调dataSource询问当前有多少个item,于是我们会按照remove数据之后的数量返回20-1=19个(因为performBatchUpdate是在第3行removeObjectAtIndex之后执行);

然后在删除动画结束时候,UICollectionView继续询问dataSource当前有多少个item,我们会返回当前的数量19个;

于是UICollectionView就认为出现异常:因为动画前返回是19个,现在删除1个之后返回还是19个。

这个也可以解释一个奇怪现象,如果在移除数据之前调用一遍numberOfItemsInSection:,即使按照原来的复现路径也不会crash。

因为第一行更新了缓存为正确数量。

NSLog(@"count:%d", [self.collectionView numberOfItemsInSection:0]);
[self.datas removeObjectAtIndex:0];
[self.collectionView performBatchUpdates:^{
    [self.collectionView deleteItemsAtIndexPaths:@[[NSIndexPath indexPathForRow:0 inSection:0]]];
} completion:^(BOOL finished) {
}];

UICollectionView.h 找到一个UICollectionViewData.h类,这里面的属性long long* _sectionItemCounts这个很可能就是负责缓存item数量的变量。

有关UICollectionView iOS 13以下删除动画crash的更多相关文章

  1. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  2. ruby - 我可以使用 Ruby 从 CSV 中删除列吗? - 2

    查看Ruby的CSV库的文档,我非常确定这是可能且简单的。我只需要使用Ruby删除CSV文件的前三列,但我没有成功运行它。 最佳答案 csv_table=CSV.read(file_path_in,:headers=>true)csv_table.delete("header_name")csv_table.to_csv#=>ThenewCSVinstringformat检查CSV::Table文档:http://ruby-doc.org/stdlib-1.9.2/libdoc/csv/rdoc/CSV/Table.html

  3. ruby - 我可以使用 aws-sdk-ruby 在 AWS S3 上使用事务性文件删除/上传吗? - 2

    我发现ActiveRecord::Base.transaction在复杂方法中非常有效。我想知道是否可以在如下事务中从AWSS3上传/删除文件:S3Object.transactiondo#writeintofiles#raiseanexceptionend引发异常后,每个操作都应在S3上回滚。S3Object这可能吗?? 最佳答案 虽然S3API具有批量删除功能,但它不支持事务,因为每个删除操作都可以独立于其他操作成功/失败。该API不提供任何批量上传功能(通过PUT或POST),因此每个上传操作都是通过一个独立的API调用完成的

  4. ruby - 如何安全地删除文件? - 2

    在Ruby中是否有Gem或安全删除文件的方法?我想避免系统上可能不存在的外部程序。“安全删除”指的是覆盖文件内容。 最佳答案 如果您使用的是*nix,一个很好的方法是使用exec/open3/open4调用shred:`shred-fxuz#{filename}`http://www.gnu.org/s/coreutils/manual/html_node/shred-invocation.html检查这个类似的帖子:Writingafileshredderinpythonorruby?

  5. ruby-on-rails - 标准化文件名的字符串,删除重音和特殊字符 - 2

    我正在尝试找到一种方法来规范化字符串以将其作为文件名传递。到目前为止我有这个:my_string.mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/n,'').downcase.gsub(/[^a-z]/,'_')但第一个问题:-字符。我猜这个方法还有更多问题。我不控制名称,名称字符串可以有重音符、空格和特殊字符。我想删除所有这些,用相应的字母('é'=>'e')替换重音符号,并将其余的替换为'_'字符。名字是这样的:“Prélèvements-常规”“健康证”...我希望它们像一个没有空格/特殊字符的文件名:“prelevements_routin

  6. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

  7. ruby - 安装libv8(3.11.8.13)出错,Bundler无法继续 - 2

    运行bundleinstall后出现此错误:Gem::Package::FormatError:nometadatafoundin/Users/jeanosorio/.rvm/gems/ruby-1.9.3-p286/cache/libv8-3.11.8.13-x86_64-darwin-12.gemAnerroroccurredwhileinstallinglibv8(3.11.8.13),andBundlercannotcontinue.Makesurethat`geminstalllibv8-v'3.11.8.13'`succeedsbeforebundling.我试试gemin

  8. ruby-on-rails - 为什么在 Rails 5.1.1 中删除了 session 存储初始化程序 - 2

    我去了这个website查看Rails5.0.0和Rails5.1.1之间的区别为什么5.1.1不再包含:config/initializers/session_store.rb?谢谢 最佳答案 这是删除它的提交:Setupdefaultsessionstoreinternally,nolongerthroughanapplicationinitializer总而言之,新应用没有该初始化器,session存储默认设置为cookie存储。即与在该初始值设定项的生成版本中指定的值相同。 关于

  9. ruby - 如果它是标点符号,我怎么能从字符串中删除最后一个字符,在 ruby​​ 中? - 2

    啊,正则表达式有点困惑。我正在尝试删除字符串末尾所有可能的标点符号:ifstr[str.length-1]=='?'||str[str.length-1]=='.'||str[str.length-1]=='!'orstr[str.length-1]==','||str[str.length-1]==';'str.chomp!end我相信有更好的方法来做到这一点。有什么指点吗? 最佳答案 str.sub!(/[?.!,;]?$/,'')[?.!,;]-字符类。匹配这5个字符中的任何一个(注意,。在字符类中并不特殊)?-前一个字符或组

  10. 键删除后 ruby​​ 哈希内存泄漏 - 2

    你好,我无法成功如何在散列中删除key后释放内存。当我从哈希中删除键时,内存不会释放,也不会在手动调用GC.start后释放。当从Hash中删除键并且这些对象在某处泄漏时,这是预期的行为还是GC不释放内存?如何在Ruby中删除Hash中的键并在内存中取消分配它?例子:irb(main):001:0>`ps-orss=-p#{Process.pid}`.to_i=>4748irb(main):002:0>a={}=>{}irb(main):003:0>1000000.times{|i|a[i]="test#{i}"}=>1000000irb(main):004:0>`ps-orss=-p

随机推荐