一场陟遐自迩的 SwiftUI + CoreData 性能优化之旅(下)

发布于:2025-05-10 ⋅ 阅读:(8) ⋅ 点赞:(0)

在这里插入图片描述

概述

自从 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 性能小“瓶颈”的解决思路,并随后循序渐进的将其优化于无形。

感谢观赏,再会啦!😎


网站公告

今日签到

点亮在社区的每一天
去签到