swift
1.59 s 15.4% 1.31 s closure #6 in LineChartView.generateSingleLineChart(from:)
579.00 ms 5.6% 490.00 ms closure #6 in LineChartView.generateGroupLineChart(from:)
524.00 ms 5.0% 423.00 ms closure #1 in closure #4 in LineChartView.generateLineChart(from:)
269.00 ms 2.6% 221.00 ms closure #1 in closure #6 in LineChartView.generateOtherLineChart(from:)
92.00 ms 0.8% 69.00 ms closure #1 in closure #1 in closure #4 in LineChartView.ChartSectionView.body.getter
1.06 s 21.2% 884.00 ms closure #6 in LineChartView.generateSingleLineChart(from:)
let chartContent = ForEach(data) { model in
// LineMark for data
226 ms LineMark(
43 ms x: .value("Time", model.timestamp),
49 ms y: .value("Value", model.value)
)
166 ms .foregroundStyle(viewModel.getLineColor(for: model.name))
561 ms .lineStyle(StrokeStyle(lineWidth: chartLineWidth, lineCap: .round, lineJoin: .round))
}
325.00 ms 6.5% 259.00 ms closure #1 in closure #4 in LineChartView.generateLineChart(from:)
return Chart {
ForEach(data) { model in
102 ms LineMark(
18 ms x: .value("Time", model.timestamp),
18 ms y: .value("Value", model.value)
)
185 ms .lineStyle(StrokeStyle(lineWidth: chartLineWidth, lineCap: .round, lineJoin: .round))
}
}
306.00 ms 6.1% 261.00 ms closure #6 in LineChartView.generateGroupLineChart(from:)
let chartContent = ForEach(data) { model in
// LineMark for data
58 ms LineMark(
10 ms x: .value("Time", model.timestamp),
6 ms y: .value("Value", model.value)
)
67 ms .foregroundStyle(by: .value("Field", model.name))
158 ms .lineStyle(StrokeStyle(lineWidth: chartLineWidth, lineCap: .round, lineJoin: .round))
}
144.00 ms 2.8% 123.00 ms closure #1 in closure #6 in LineChartView.generateOtherLineChart(from:)
return Chart {
ForEach(data) { model in
25 ms LineMark(
7 ms x: .value("Time", model.timestamp),
6 ms y: .value("Value", model.value)
)
23 ms .foregroundStyle(model.color)
77 ms .lineStyle(StrokeStyle(lineWidth: chartLineWidth, lineCap: .round, lineJoin: .round))
}
}
42.00 ms 0.8% 42.00 ms initializeWithCopy for LineChartData
1. 分析 Time Profiler 的耗时情况
-
闭包内部耗时较高
- 在
generateSingleLineChart
中,ForEach
循环内部的闭包花费了884
毫秒,其中.lineStyle(...)
调用占了 561 毫秒,说明对每个数据点的样式计算非常耗时。 - 同理,
generateLineChart
和generateGroupLineChart
中,也分别花费了259
毫秒和261
毫秒。这表明在遍历数据并生成LineMark
时,SwiftUI
在计算各个修饰符(如lineStyle
、foregroundStyle
)的过程中开销较大。
- 在
-
重复计算和视图重构
- 每次调用这些生成图表的函数时,都需要重新计算
x
轴、y
轴的最小值、最大值、刻度值等,而且对每个数据点都要构建一个新的LineMark
。如果数据点较多,这些计算和视图构造操作叠加起来就会变得非常耗时。
- 每次调用这些生成图表的函数时,都需要重新计算
-
样式计算问题
- 例如在
generateSingleLineChart
中,对每个数据点调用viewModel.getLineColor(for:)
和创建StrokeStyle
都消耗了大量时间。这提示我们,样式的计算和生成可能没有复用,每次都在重新构建。
- 例如在
2. 关于 NavigationLink 导航后返回页面时卡顿现象
- 重绘整个视图
- 当从
LineChartView
跳转到子页面后返回,SwiftUI
会重新调用body
生成视图。即使数据没有改变,所有的闭包(如generateSingleLineChart
、generateGroupLineChart
、generateLineChart
等)都会被重新执行,导致整个图表视图重新计算和绘制。
- 当从
- 视图重构导致的重复计算
- 由于
SwiftUI
的声明式特性,每次视图出现时都会重新计算其内部状态,进而触发大量重复的计算操作(比如对时间范围、轴值和样式的计算),所以返回时会明显卡顿,耗时达到5
到6
秒。
- 由于
3. 优化思路和解决方案
针对上述问题,可以从以下几个方面考虑优化:
a. 数据和样式的预计算与缓存
- 提前计算图表数据范围和样式
- 将
x
轴和y
轴的计算、刻度值生成、以及各个数据点对应的样式等计算移到viewModel
中,提前计算好并缓存。这样在视图构建时,只需直接使用缓存结果,避免重复计算。
- 将
- 缓存 StrokeStyle 和颜色
- 如果
chartLineWidth
、lineCap
、lineJoin
等参数不会变化,可以将生成的StrokeStyle
定义为常量,避免在ForEach
内部重复创建。
- 如果
b. 减少不必要的视图重构
-
局部视图拆分和懒加载
- 将复杂且耗时的图表部分拆分成独立的子视图,利用
SwiftUI
的@StateObject
或@ObservedObject
缓存数据,防止因父视图重新构建而全部重绘。 - 如果页面上图表较多,可以考虑使用
LazyVStack
替换VStack
,使得只有当视图真正需要显示时才进行计算和绘制。
- 将复杂且耗时的图表部分拆分成独立的子视图,利用
-
视图缓存策略
- 当数据不频繁变化时,可以考虑在视图中利用
.drawingGroup()
或其他缓存手段,将渲染结果缓存成图像,在下一次展示时直接使用缓存图像而不是重新绘制所有的线条。
- 当数据不频繁变化时,可以考虑在视图中利用
c. 关注 SwiftUI 的绘图和布局机制
- 审查 Chart 的内部实现
SwiftUI
的Chart
组件可能在处理大量数据点时没有做到最优。如果可能,考虑对数据进行适当的抽样或者合并,减少绘制的元素数量。
- 分析 View 重构触发机制
- 检查是否存在不必要的状态更新或绑定,导致整个
LineChartView
被频繁重构。如果能做到局部状态隔离,只让真正需要更新的部分重绘,也会大大降低耗时。
- 检查是否存在不必要的状态更新或绑定,导致整个
总结
-
问题定位:
- 耗时主要在
ForEach
循环中生成各个LineMark
的过程中,特别是.lineStyle
的计算,以及在视图重构时重复计算各项轴值和样式。 - 导航返回时,因为整个视图重新构建,所有的图表闭包都会再次执行,造成明显卡顿。
- 耗时主要在
-
优化方案:
- 预计算和缓存 :在
viewModel
中提前计算好各个图表需要的数据、刻度、样式等,避免在视图中重复计算。 - 减少重复视图构建 :将耗时较大的图表部分拆分为独立的子视图,并使用懒加载、缓存视图或
.drawingGroup()
技术来减少重绘。 - 调整数据量:如果数据点非常多,可以考虑做数据抽样或聚合,降低绘图时的计算量。
- 预计算和缓存 :在