概述
自从 SwiftUI 诞生那天起,我们秃头码农们就仿佛打开了一个全新的撸码世界,再辅以 CoreData 框架的鼎力相助,打造一款持久存储支持的 App 就像探囊取物般的 Easy。
话虽如此,不过 CoreData 虽好,稍不留神也可能会让代码执行速度“蜗行牛步”,这该如何解决呢?
在本篇博文中,您将学到如下内容:
这是两篇偏向撸码的博文,里面有较多的源代码展示,我们会循序渐进地完成整个优化目标,希望大家能够喜欢。
那还等什么呢?让我们马上开始 CoreData 优化大冒险吧!
Let’s go!!!😉
2. 先谈优化思路
为了能够进一步从整体上鸟瞰全局,是时候将 MonthCountsView 父视图的源代码呈现给大家了:
struct CounterView: View {
@Environment(\.managedObjectContext) var context
let counter: ProjectCounter
@State private var yearsCountsData = [ProjectCounter.YearCountsData]()
LazyVStack {
ForEach(yearsCountsData) { yearData in
VStack {
HStack {
Text(verbatim: "\(yearData.year)年")
.font(.title.weight(.heavy))
Spacer()
Text("年总计数:\(yearData.totalCount)\(counter.unit ?? "")")
.fontWeight(.bold)
.foregroundStyle(counter.nature.data.color)
}
if let monthsCounts = yearData.monthsCountSortedAry {
ForEach(monthsCounts) { monthData in
DisclosureGroup {
MonthCountsView(yearsCountsData: $yearsCountsData, counter: counter, year: monthData.year, month: monthData.month)
} label: {
HStack {
Text("\(monthData.month)月")
Spacer()
Text("\(monthData.totalCount)\(counter.unit ?? "")")
}
}
}
}
}
}
}
.task {
// 计算年计数数据
yearsCountsData = counter.calcYearsCountsData()
}
}
回顾一下之前 MonthCountsData 结构的实现,其中有一个 daysCounts: [Int: DayCountsData]? 可选类型,它在默认情况下并不会被主动填充,我们为什么不把它利用起来呢?
我们的思路是:在 MonthCountsView 首次显示时计算该月的月计数 [Int: DayCountsData] 字典数据,并将其写回到父视图 yearsCountsData 对应的月计数对象中去,这样下次相同 MonthCountsView 视图再次加入渲染树时,我们即可直接使用这个字典数据了。
而且,我们希望月计数字典数据能够在后台线程里完成,这样可以进一步提高主线程的“丝滑”程度。因为其计算方法 queryDaysCounts() 已经在设计时就支持传入一个“可爱”的托管上下文对象,这无疑让我们后续的优化操作“易如拾芥”:
func queryDaysCounts(year: Int, month: Int, context: NSManagedObjectContext) throws -> [Int: DayCountsData] {
// 实现从略...
}
在将 CoreData 的托管对象从后台线程传入主线程时,要特别小心,否则可能会成为“池鱼林木”。更多与此相关的介绍,请小伙伴们移步如下链接观赏精彩的内容:
3. 循序渐进与大刀阔斧
当思路已经成型,当脱发已成往事,我们就可以起身向最终的目标前进了。在旅途中,我们要心细且胆大。这有点儿像开车:该慢的时候一定要慢,而该快的时候你也要把速度提起来。
首先,我们在 MonthCountsView 视图中新增一个年计数绑定,用来绑定父视图中的对应数据:
/// 所有年计数记录的绑定,便于将计算结果写回,避免反复计算月计数数据
@Binding var yearsCountsData: [ProjectCounter.YearCountsData]
接着,我们直接删除之前 MonthCountsView 视图里 #1 处的变量定义,并增加新的 daysCounts 同名属性:
@State private var daysCounts = [Int: ProjectCounter.DayCountsData]()
最后,我们让 MonthCountsView 视图在显示时按需计算相关的月计数数据:
.task {
let yearIndex = yearsCountsData.firstIndex { $0.year == year}!
if let monthData = yearsCountsData[yearIndex].monthsCounts?[month], let daysCounts = monthData.daysCounts {
self.daysCounts = daysCounts
} else {
let container = Model.shared.controller.container
container.performBackgroundTask { bgContext in
let daysCounts = try! counter.queryDaysCounts(year: year, month: month, context: bgContext)
DispatchQueue.main.async {
self.daysCounts = daysCounts
// 将计算结果作为缓存,写回到父视图的年计数中去
yearsCountsData[yearIndex].monthsCounts?[month]?.daysCounts = daysCounts
}
}
}
}
在上面的代码里,我们主要做了这样几件事:
- 找到当前月对应年的计数数据 YearCountsData;
- 如果年计数数据对应的月数据已经缓存,我们直接使用它;
- 否则,我们在后台计算月计数数据,并在计算完毕后回到主线程写入年计数数据的缓存中;
这样一来,我们的月计数数据只需在 MonthCountsView 视图首次显示时计算一次,之后即可享用缓存中现成的数据了。
4. 打完收工
回到 MonthCountsView 的父视图 CounterView 中,我们修改一下 MonthCountsView 的调用签名:
if let monthsCounts = yearData.monthsCountSortedAry {
ForEach(monthsCounts) { monthData in
DisclosureGroup {
MonthCountsView(yearsCountsData: $yearsCountsData, counter: counter, year: monthData.year, month: monthData.month)
} label: {
HStack {
Text("\(monthData.month)月")
Spacer()
Text("\(monthData.totalCount)\(counter.unit ?? "")")
}
}
}
}
现在,一切都已准备就绪,我们再回到 Xcode 预览中一窥究竟新代码的表现吧:
值得注意的是,除了 Grid 布局可以从 MonthCountsView 视图的 daysCounts 缓存受益以外,其中的月计数图表(Chart)同样也可以得到妥妥地加速,正所谓一石二鸟、一箭双雕也,棒棒哒!💯
想要进一步系统地学习 Swift 开发的小伙伴们,可以来我的《Swift 语言开发精讲》专栏逛一逛哦:
总结
在本篇博文中,我们讨论了一个 SwiftUI + CoreData 性能小“瓶颈”的解决思路,并随后循序渐进的将其优化于无形。
感谢观赏,再会啦!😎