
概述
自從 SwiftUI 誕生那天起,我們禿頭碼農(nóng)們就仿佛打開(kāi)了一個(gè)全新的擼碼世界,再輔以 CoreData 框架的鼎力相助,打造一款持久存儲(chǔ)支持的 App 就像探囊取物般的 Easy。

話雖如此,不過(guò) CoreData 雖好,稍不留神也可能會(huì)讓代碼執(zhí)行速度“蝸行牛步”,這該如何解決呢?
在本篇博文中,您將學(xué)到如下內(nèi)容:
- 先談優(yōu)化思路
- 循序漸進(jìn)與大刀闊斧
- 打完收工
這是兩篇偏向擼碼的博文,里面有較多的源代碼展示,我們會(huì)循序漸進(jìn)地完成整個(gè)優(yōu)化目標(biāo),希望大家能夠喜歡。
那還等什么呢?讓我們馬上開(kāi)始 CoreData 優(yōu)化大冒險(xiǎn)吧!
Let's go?。。?)
2. 先談優(yōu)化思路
為了能夠進(jìn)一步從整體上鳥(niǎo)瞰全局,是時(shí)候?qū)?MonthCountsView 父視圖的源代碼呈現(xiàn)給大家了:
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("年總計(jì)數(shù):\(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 {
// 計(jì)算年計(jì)數(shù)數(shù)據(jù)
yearsCountsData = counter.calcYearsCountsData()
}
}
回顧一下之前 MonthCountsData 結(jié)構(gòu)的實(shí)現(xiàn),其中有一個(gè) daysCounts: [Int: DayCountsData]? 可選類(lèi)型,它在默認(rèn)情況下并不會(huì)被主動(dòng)填充,我們?yōu)槭裁床话阉闷饋?lái)呢?
我們的思路是:在 MonthCountsView 首次顯示時(shí)計(jì)算該月的月計(jì)數(shù) [Int: DayCountsData] 字典數(shù)據(jù),并將其寫(xiě)回到父視圖 yearsCountsData 對(duì)應(yīng)的月計(jì)數(shù)對(duì)象中去,這樣下次相同 MonthCountsView 視圖再次加入渲染樹(shù)時(shí),我們即可直接使用這個(gè)字典數(shù)據(jù)了。
而且,我們希望月計(jì)數(shù)字典數(shù)據(jù)能夠在后臺(tái)線程里完成,這樣可以進(jìn)一步提高主線程的“絲滑”程度。因?yàn)槠溆?jì)算方法 queryDaysCounts() 已經(jīng)在設(shè)計(jì)時(shí)就支持傳入一個(gè)“可愛(ài)”的托管上下文對(duì)象,這無(wú)疑讓我們后續(xù)的優(yōu)化操作“易如拾芥”:
func queryDaysCounts(year: Int, month: Int, context: NSManagedObjectContext) throws -> [Int: DayCountsData] {
// 實(shí)現(xiàn)從略...
}
3. 循序漸進(jìn)與大刀闊斧
當(dāng)思路已經(jīng)成型,當(dāng)脫發(fā)已成往事,我們就可以起身向最終的目標(biāo)前進(jìn)了。在旅途中,我們要心細(xì)且膽大。這有點(diǎn)兒像開(kāi)車(chē):該慢的時(shí)候一定要慢,而該快的時(shí)候你也要把速度提起來(lái)。
首先,我們?cè)?MonthCountsView 視圖中新增一個(gè)年計(jì)數(shù)綁定,用來(lái)綁定父視圖中的對(duì)應(yīng)數(shù)據(jù):
/// 所有年計(jì)數(shù)記錄的綁定,便于將計(jì)算結(jié)果寫(xiě)回,避免反復(fù)計(jì)算月計(jì)數(shù)數(shù)據(jù)
@Binding var yearsCountsData: [ProjectCounter.YearCountsData]
接著,我們直接刪除之前 MonthCountsView 視圖里 #1 處的變量定義,并增加新的 daysCounts 同名屬性:
@State private var daysCounts = [Int: ProjectCounter.DayCountsData]()
最后,我們讓 MonthCountsView 視圖在顯示時(shí)按需計(jì)算相關(guān)的月計(jì)數(shù)數(shù)據(jù):
.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
// 將計(jì)算結(jié)果作為緩存,寫(xiě)回到父視圖的年計(jì)數(shù)中去
yearsCountsData[yearIndex].monthsCounts?[month]?.daysCounts = daysCounts
}
}
}
}
在上面的代碼里,我們主要做了這樣幾件事:
- 找到當(dāng)前月對(duì)應(yīng)年的計(jì)數(shù)數(shù)據(jù) YearCountsData;
- 如果年計(jì)數(shù)數(shù)據(jù)對(duì)應(yīng)的月數(shù)據(jù)已經(jīng)緩存,我們直接使用它;
- 否則,我們?cè)诤笈_(tái)計(jì)算月計(jì)數(shù)數(shù)據(jù),并在計(jì)算完畢后回到主線程寫(xiě)入年計(jì)數(shù)數(shù)據(jù)的緩存中;
這樣一來(lái),我們的月計(jì)數(shù)數(shù)據(jù)只需在 MonthCountsView 視圖首次顯示時(shí)計(jì)算一次,之后即可享用緩存中現(xiàn)成的數(shù)據(jù)了。
4. 打完收工
回到 MonthCountsView 的父視圖 CounterView 中,我們修改一下 MonthCountsView 的調(diào)用簽名:
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 ?? "")")
}
}
}
}
現(xiàn)在,一切都已準(zhǔn)備就緒,我們?cè)倩氐?Xcode 預(yù)覽中一窺究竟新代碼的表現(xiàn)吧:

值得注意的是,除了 Grid 布局可以從 MonthCountsView 視圖的 daysCounts 緩存受益以外,其中的月計(jì)數(shù)圖表(Chart)同樣也可以得到妥妥地加速,正所謂一石二鳥(niǎo)、一箭雙雕也,棒棒噠!??
總結(jié)
在本篇博文中,我們討論了一個(gè) SwiftUI + CoreData 性能小“瓶頸”的解決思路,并隨后循序漸進(jìn)的將其優(yōu)化于無(wú)形。
感謝觀賞,再會(huì)啦!8-)