一場(chǎng)陟遐自邇的 SwiftUI + CoreData 性能優(yōu)化之旅(下)

概述

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

話雖如此,不過(guò) CoreData 雖好,稍不留神也可能會(huì)讓代碼執(zhí)行速度“蝸行牛步”,這該如何解決呢?

在本篇博文中,您將學(xué)到如下內(nèi)容:

  1. 先談優(yōu)化思路
  2. 循序漸進(jìn)與大刀闊斧
  3. 打完收工

這是兩篇偏向擼碼的博文,里面有較多的源代碼展示,我們會(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-)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容