前言
AnyView 是一种类型擦除的视图,对于 SwiftUI 容器中包括的异构视图十分便利。在这些情况下,你不需求指定视图层次结构中一切视图的详细类型。经过这种办法,你能够防止运用泛型,然后简化你的代码。
但是,这可能会带来功能损失。假如是 AnyView(基本上是一个包装类型),SwiftUI 将很难确认视图的身份和结构,而且它将从头制作整个视图,这并不是真正高效的。你能够在这个出色的 WWDC 讲演中找到有关 SwiftUI 差异机制的更多细节。
Apple 也屡次说到,咱们应该防止在 ForEach 中运用 AnyView,称其可能会导致功能问题。一个可能产生的情况是无尽的不同视图列表,呈现不同类型的数据(例如聊天、活动动态等)。在本文中,我将运用 Stream 的 SwiftUI 聊天 SDK 进行一些丈量,运用其默许的基于泛型的完成,并将其与运用 AnyView 的修正后的完成进行比较。
测验设置
关于测验设置的几点说明:
- 一切测验和丈量都在 iPhone 11 Pro Max 上进行。
- 为保持一致性,在一切测验中都运用相同的数据集和用户。
- 测验会履行屡次。
- 正在测验的列表具有不同类型的数据(例如图画、视频、GIF、文本等)。
- 在测验不同完成时履行相同的操作(例如,在内容上翻滚三次)。
- 数据以每页 25 个项目的办法获取。
- 咱们将运用动画卡顿仪器配置文件以及这个开源 FPS 计数器。
动画卡顿
苹果建议运用动画卡顿作为衡量运用功能的目标。卡顿基本上是指在屏幕上显现的帧比预期晚的帧。卡顿时刻越长,呈现的毛病和挂起就越明显,然后形成用户体会欠安。例如,假如你有 100 毫秒的卡顿,这意味着此帧显现晚于预期的 100 毫秒,然后运用户能够看到挂起。卡顿能够呈现在提交阶段或渲染阶段。
为了进步咱们运用的功能,咱们需求将这些动画卡顿降到最低(或许更好地脱节它们)。
我还将展示与 FPS(每秒帧数)的比较,因为它通常是开发人员更了解的度量规范之一。当运用 FPS 作为度量规范时,重要的是指定最大帧速率(在这种情况下为 60),并在运用程序没有活动时丢掉值。
阅读数据
首先,让咱们看看在阅读内容时不同的完成会表现怎么。在这个测验中,咱们将经过整个音讯列表三次翻滚。
没有 AnyView
下面是没有泛型完成的动画卡顿记录。
如你所见,有几个动画卡顿,其间 2 个是橙色的,这意味着卡顿持续时刻超过了可接受的延迟时刻 33 毫秒。因此,在这 2 种情况下,将会丢掉一帧。这 2 个卡顿产生在加载新音讯并将其附加到音讯列表时。在加载音讯时进行任何后续翻滚,不会影响功能。
在此测验期间,FPS 值的平均值约为每秒 59 帧。翻滚是流畅且呼应迅速的。
有 AnyView
接下来,让咱们做相同的测验,一起运用 AnyView 包装器。以下是动画卡顿仪器配置文件中的成果。
你能够在此示例中看到一些更多的橙色。有更多的动画卡顿超过了可接受的延迟时刻 33 毫秒。这导致在履行测验时在仪器和视觉上都呈现一些可见的卡顿。
此外,当你再次阅读列表时,功能不会改进(乃至变得更糟)。这是有道理的,因为 SwiftUI 不知道它已经显现过此视图一次(因为它隐藏在 AnyView 下)。因此,它会再次制作它,一起还可能缓存(但不运用)该视图的旧版本。
此测验中的平均 FPS 约为每秒 55 帧,你可能会注意到在翻滚时呈现一些可见的毛病,虽然情况并不那么糟糕。
在阅读数据时修正
咱们能够进行的另一个测验是功能测验 – 向列表发送大量内容并强制更新视图(例如,呼应音讯),一起咱们也阅读数据。这将在较短的时刻距离内触发视图的屡次重绘。
没有 AnyView
在没有 AnyView 包装器的情况下进行测验产生了与惯例翻滚测验类似的成果(58-59 FPS)。这也是预期的,因为 SwiftUI 知道视图的标识和结构。当需求更新视图时,仅对其进行更改(例如,向视图增加另一个反应)。
有 AnyView
当咱们在这种情况下运用 AnyView 时,事情就变得风趣了 – 在短时刻内对屏幕上的视图进行频频更新。
在此场景中,有几个可见的卡顿和挂起,当咱们频频呼应音讯时,FPS 降至 50 以下。因为在几秒钟内强制重绘视图屡次,帧丢掉在这里愈加明显。因为 SwiftUI 不知道这个视图是什么,我假设它每次都会从头开始重绘。其间一些视图适当贵重(例如 GIF),因此从头制作可能是一项适当贵重的操作。
经过运用 AnyView,效果类似于将 id 修饰符的值设置为 UUID() – 这将在产生更改时始终更新视图项目。
分析成果
测验/完成 | 没有 AnyView(FPS) | 有 AnyView(FPS) | 功能退化 |
---|---|---|---|
阅读数据 | 59 | 55 | 10% |
在阅读数据时修正 | 59 | 50 | 16.5% |
这些数字适当依赖于设置,因此不该该被视为板上钉钉的成果,而只是一个指示。
仅阅读数据时,假如你将视图包装在 AnyView 中,则会比不包装时慢大约 10%。假如你在阅读数据时更改数据,则此差异将增加到约 17%,而且这些毛病在这里愈加明显。
为了更好地了解成果,咱们需求深化了解 SwiftUI 的作业原理。在这个关于 SwiftUI 功能的 WWDC 会话中,来自 SwiftUI 团队的 Raj 讨论了列表或表需求提早知道一切标识符。只有在内容解析为稳定数量的行时,才能高效地搜集它们而无需访问一切内容。假如运用条件检查或 AnyView,将无法确认行数,而且有必要提早创立一切视图,这会影响功能。
因此,请尽量防止这样的代码:
ForEach(someData) { someElement in
if someCondition {
SomeView(data: someElement)
}
}
以及像这样的代码:
ForEach(someData) { someElement in
AnyView(SomeView(data: someElement))
}
最终一段代码类似于咱们运用 AnyView 进行测验的办法。这意味着,当列表产生更改时,咱们实际上从头创立了整个列表。这也解释了为什么 AnyView 完成随着时刻的推移变慢 – 每次重绘时都需求从头开始创立更多内容。
总结
总而言之,在这些情景中(包括异构视图的可翻滚列表),最好为容器中的不同视图运用详细类型。这可能听起来更复杂一些,但实际上你能够使其更简略,而不用过多地处理泛型。
但是,这并不意味着运用 AnyView 总是会以这种办法影响功能。例如,假如你有一个菜单,作为几个异构元素的列表,在点击时显现不同的导航目标,而且决定将这些视图包装为 AnyView,我的丈量成果表明与运用其他办法相比,功能没有差异。
在这篇文章中,运用 AnyView 与运用 if-else 句子的不同类型的测验显现出没有明显差异。运用 if-else 导致视图标识丢掉,就像 AnyView 相同,因此在这里没有功能差异是能够预期的。
这也取决于完成的办法 – 你的数据模型,将状况传递到哪里,哪些更新可能会导致视图重绘等等。