起因
里面,使用了分栏式的展示逻辑,即点击列表中的一个示例,在详细页面 detail view 里展示一个相关地图功能。每一个功能都有很丰富的交互,因此每个详细功能页面少则占用10M+,多则100M+的内存才能加载。
由于每个页面都算很“重”的视图,因此避免 SwiftUI 因视图 identity 改变而重新计算,变得尤为重要。
在这个 app 最初设计时,遇到了这样的问题:我有一个 所包装,一旦其被创建,后面渲染的时间是一样的。这样,我们可以只关注 AnyView 和 @ViewBuilder 所产生的包装层的时间差别。
依据我的测试,当我遍历创建整个 app 里的200个左右示例页面,使用
func makeBody() -> AnyView { .init(\(sample.viewName)()) }
// 遍历所有示例并创建视图
var bodies: [AnyView] = []
for sample in SamplesApp.samples {
bodies.append(sample.makeBody())
}
和
@ViewBuilder
func view(for sample: Sample) -> some View {
switch sample {
case is AddPointCloudLayerFromFile:
AddPointCloudLayerFromFileView()
case is SelectFeaturesInFeatureLayer:
SelectFeaturesInFeatureLayerView()
// ...
default:
fatalError("Unknown \(sample.name) sample view generated.")
}
}
// 遍历所有示例并创建视图
var bodies: [any View] = []
for sample in SamplesApp.samples {
bodies.append(view(for: sample))
}
两个方法时 @ViewBuilder 的方法平均时间比 AnyView 要慢5%左右(50ms vs 55ms)。
这样的结果有些出乎我的意料。毕竟,如果理论上创建 AnyView 需要花更多时间来确定其运行时的类型,难道不应该花更多时间吗?
更令人意想不到的是接下来的发现。当我试图分析 @ViewBuilder func view(for sample: Sample) -> some View 这个方法返回的视图类型时,发现其并非如老版本中使用 _ConditionalContent 来包装视图,而是直接使用了 AnyView !即在调试器中,print(type(of: view(for: sample))) 的结果是 AnyView 。在调试器变量区显示的类型则是 <<opaque return type of ArcGIS_Maps_SDK_Samples.ContentView.view(for: ArcGIS_Maps_SDK_Samples.Sample) -> some>>.0。
作为对比,在我第一次测试 @ViewBuilder 的结果时,它的类型是类似于如下的二叉树状结构的。当时我觉得这很合理——相当于用二叉搜索快速确定一个 result builder 的结果类型,应该性能上很不错。
_ConditionalContent<
_ConditionalContent<
_ConditionalContent<
AddRasterFromFileView,
AddSceneLayerFromServiceView
>,
_ConditionalContent<
BrowseBuildingFloorsView,
ClipGeometryView
>
>,
_ConditionalContent<
_ConditionalContent<
CreatePlanarAndGeodeticBuffersView,
CutGeometryView
>,
_ConditionalContent<
DisplayFeatureLayersView,
DisplayMapView
>
>
>,
// …
但是经过这次测试得到意想不到的结果,也能明白为什么 @ViewBuilder 比 AnyView 要慢了。因为最终创建的都是 AnyView 的前提下,@ViewBuilder 多了花在 switch-case 判断的时间,比起直接从示例类型生成视图,相当于额外的时间。
测试3:大量 AnyView
用类似如下视图来测试 AnyView 在列表这种动态计算的视图中的性能影响。这个示例创建 50000 个 HStack 和 AnyView 包装的文本视图。
import SwiftUI
class Model {
static let items50K = (0 ..< 50_000).map { Item(id: $0) }
}
struct Item: Identifiable {
let id: Int
var text: String { String(id) }
}
struct NormalItemView: View {
let item: Item
var body: some View {
HStack { Text(item.text) }
}
}
struct AnyItemView: View {
let item: Item
var body: some View {
AnyView(Text(item.text))
}
}
struct NormalListView: View {
var items: [Item]
var body: some View {
List(items) { item in
NormalItemView(item: item)
}
.listStyle(.plain)
}
}
struct AnyListView: View {
var items: [Item]
var body: some View {
List(items) { item in
AnyItemView(item: item)
}
.listStyle(.plain)
}
}
struct ContentView: View {
var body: some View {
NavigationView {
VStack(spacing: 20) {
NavigationLink {
NormalListView(items: Model.items50K)
} label: { Text("NormalView 50K") }
NavigationLink {
AnyListView(items: Model.items50K)
} label: { Text("AnyView 50K") }
}
}
}
}
明显可以看到,当打开 AnyView 的列表时,有很长一段卡死的时间。但是当 AnyView较少,比如一千个以下时,造成的性能影响并不大,不足以拖慢 app 的运行速度。
结论
AnyView个数不多,对性能影响不太大- 使用
@ViewBuilder替代AnyView,在一些情况下并不能提升性能
SOCIAL SHARE CARD GENERATOR