Xcode13构建的自适应瀑布流Layout在iOS15上crash的定位及修复

5,116 阅读6分钟

背景

快手App的某个页面在native化时,采用了自定义的瀑布流布局来展示A卡片与其下方的B卡片,由于时间与人力成本,在初期开发时复用了项目中已有的布局类。由于该布局不支持自适应item宽高,需要在数据返回后立刻计算出所有item的尺寸,严重拖慢了首屏数据的加载,因此需要对布局增加自适应item尺寸的能力,分散布局测算压力,加快首屏A卡片的渲染。

问题

优化入版后,有开发同学(外地工区)反馈在iOS15上的该页面遇到了必现的crash,表现为UICollectionView递归调用_updateVisibleCellsNow导致最终爆栈。但问题始终无法在笔者本地复现,且构建平台输出的beta包也无法复现。 图1

定位

根据开发同学反馈,在拉取最新开发分支的代码后,就开始遇到进入该页面出现卡死并爆栈的crash。堆栈记录的起点和终点均在系统方法中,从爆栈方法的命名猜测,是item的布局展示上出现了问题。由于该页面在本期的优化点为延迟A卡片item的布局时机至展示时计算,因此猜测可能是设置的预估高度出了问题。与反馈同学沟通并尝试调整问题点后,crash依旧存在,堆栈没有任何变化,因此排除了预估高度设置不当的原因。

在网上搜索了该问题的相关描述后,只找到一两条表现上类似的反馈,其问题点在动态测算item尺寸时,由于始终无法获得稳定的frame信息,collectionView会一直尝试测算,导致最终爆栈,因此猜测可能是A卡片的约束出了问题,无法获得问题的size导致frame一直在变化。再次让反馈同学调整问题点后,crash还是存在,且堆栈依旧没有任何变化,且A卡片的尺寸在第一次测算完后,便稳定返回相同的宽高,因此排除了上述的问题点。

在验证上述两个可能的问题点时,笔者本地切入反馈同学的分支后,发现其分支上出现了另外的crash问题,但笔者之前的分支并无此crash,因此怀疑其分支环境本身有误,影响了该页面的展示,但反馈同学在切换至最新的开发分支后,依旧可以稳定复现该问题,说明问题与分支环境并无关系,因此排除了分支环境可能带来的影响。

由于笔者本地没有反馈同学遇到问题的具体设备(iPhone11 iOS15.0.2),且debug包与beta包均无法复现,怀疑可能是其设备自身有问题(笔者工区QA处有一台7Plus本地无法调试,启动就崩,但beta包可以运行)。在大群里询问后,发现有其他的开发同学也遇到了该问题,因此排除了特定设备有误的可能行。

笔者在借取到问题设备并运行debug包后,发现依旧无法在本地复现。通过对比整个开发环境后,发现遇到问题的同学均使用Xcode13开发,笔者本地与构建平台的打包机均为Xcode12,因此怀疑问题是由Xcode13引入。在升级Xcode13后,笔者本地终于可以复现,由此确定了问题与Xcode版本有关。

修复

由于该页面本期优化的重点是分散提前进行的全量A卡片布局,在保持最小改动的前提下,复用了UICollectionViewLayoutitem高度自适应逻辑,其具体交互逻辑如下:

图2

由于使用的瀑布流布局继承自UICollectionViewFlowLayout,要开启item高度自适应,需要在初始化布局时设置一个预估高度(estimatedItemSize),用来计算collectionView第一次可以展示的item个数及布局。在item被填充数据后,layout会调用preferredAttributedsFitting:方法并通过itemsystemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:方法来获取最新的尺寸,并根据该尺寸重新调整item的布局信息(UICollectionViewLayoutAttributes)。

该页面整体是由上方的A卡片列表和下方的双列B卡片列表组合而成,其中A卡片的内容动态化程度较高,因此其高度通过自动布局依据数据自动撑开,而B卡片的内容较为固定,可提前计算出具体高度。本次的优化实现中,便通过重写A卡片itemsystemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:方法来接管系统默认的测算实现。

图3

通过观察preferredAttributedsFitting:方法在两个Xcode版本上的输出后,发现单列的A卡片在测算尺寸后,可以返回稳定的frame信息,但双列的B卡片则有所不同。

A卡片是通过约束布局计算出的size,其attributedframe始终在collectionViewbounds内,因此不会出现无限调用爆栈的情况。

图4

B卡片是通过手动计算宽高得出的size,通过默认的preferredAttributedsFitting:实现得到的attributedframe会超出collectionViewbounds。在Xcode12中,B卡片的preferredAttributedsFitting:在获取到的frame超出collectionViewbounds后,会沿用原来的attributed,放弃新获取的attributed,因此在测算完可见的几个cell后便可直接渲染。

图5

但Xcode13中,B卡片的preferredAttributedsFitting:在获取到的frame超出collectionViewbounds时,会重复调用preferredAttributedsFitting:,导致最终爆栈。

图6

定位到问题点后,通过梳理layout执行布局的各个步骤(见图2),可以看到collectionView在调用完preferredAttributedsFitting:后会调用shouldInvalidateLayout:方法交由layout做更进一步的控制,决定是否更新布局信息。本次修复便通过重写该方法,对真正需要重新布局的A卡片item返回YES,对尺寸固定的B卡片item返回NO。增加判断后,再次运行,页面展示正常,不再出现爆栈崩溃。

总结

由于本次的问题是在新版开发工具中才出现,因此未能及时复现与定位。后续开发中,如果再次遇到类似本地无法复现的问题,应将开发环境本身也作为引发问题的可能因素带入考虑,并尽快对其进行适配。

hi, 我是快手电商的Kaso.Lu

快手电商无线技术团队正在招贤纳士🎉🎉🎉! 我们是公司的核心业务线, 这里云集了各路高手, 也充满了机会与挑战. 伴随着业务的高速发展, 团队也在快速扩张. 欢迎各位高手加入我们, 一起创造世界级的电商产品~

热招岗位: Android/iOS 高级开发, Android/iOS 专家, Java 架构师, 产品经理(电商背景), 测试开发... 大量 HC 等你来呦~

内部推荐请发简历至 >>>我们的邮箱: hr.ec@kuaishou.com <<<, 备注我的花名成功率更高哦~ 😘