查看原文
其他

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

快手大前端技术 快手大前端技术 2022-01-07

导读


本文介绍了一个在Xcode13上遇到的新问题, 并分享了具体的定位及修复过程, 希望能带给大家一些参考和启发。


文 / Kaso.Lu

编辑 / 乔

本文共2146字,预计阅读时间7分钟。


背景


快手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 包均无法复现,怀疑可能是其设备自身有问题(笔者工区有一台7Plus本地无法调试,启动就崩,但 beta 包可以运行)。在内部询问后,发现其他开发同学也遇到了该问题,因此排除了特定设备有误的可能行。


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


修复


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


图2


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


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


图3


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


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


图4


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


图5


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


图6


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


总结


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


欢迎加入

快手主站技术部客户端团队由业界资深的移动端技术专家组成,通过领先的移动技术深耕工程架构、研发工具、动态化、数据治理等多个垂直领域,积极探索创新技术,为亿万用户打造极致体验。团队自2011年成立以来全面赋能快手生态,已经建立起业内领先的大前端技术体系,支撑快手在国内外的亿万用户。


在这里你可以获得:

  • 提升架构设计能力和代码质量

  • 通过大数据解决用户痛点的能力 

  • 持续优化业务架构、挑战高效研发效能

  • 和行业大牛并肩作战


我们期待你的加入!请发简历到:

app-eng-hr@kuaishou.com



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存