在 SwifUI 中使用 .fetchBatchSize 和 @FetchRequest

Duy*_*Anh 3 core-data swift swiftui

几天来我一直在试图理解和弄清楚它,但仍然无法想出解决方案。

例如,有一个包含 100 万行的核心数据实体,我想在视图中显示“滚动时加载”的所有记录,以提高性能,因为加载时加载 100 万行会很慢,而且浪费。

从文档来看,基本上.fetchBatchSize只会在需要时加载“批量”数据,这是问题的完美解决方案。用户滚动列表,SwiftUI 在需要时批量加载数据。

这是我所拥有的(简化的):

ContextView.swift

struct ContentView: View {
    @FetchRequest private var items: FetchedResults<Item>
    
    init() {
        _items = FetchRequest<Item>(fetchRequest: request())
    }
    
    var body: some View {
        List(items) {
            Text($0.name)
        }
    }
    
    func request() -> NSFetchRequest<Item> {
        let request = Item.fetchRequest()
        request.sortDescriptors = []
        request.fetchBatchSize = 5
        
        return request
    }
}
Run Code Online (Sandbox Code Playgroud)

问题: @FetchRequest 在没有批处理的情况下同时加载所有记录和/或它确实有效,但同时检索所有批次并破坏了批量检索的整个目的。

我尝试实际加载 100 万行,并花费大量时间来显示视图(因为它正在检索和准备所有 100 万行数据)。如果“.fetchBatchSize”有效,它将仅加载前 5 个,并且随着列表滚动而缓慢加载。

*注意:我知道有.fetchLimit& .fetchOffset,但它需要实现一个单独的逻辑 *

mal*_*hal 5

您不能使用fetchBatchSize,因为List需要 SwiftUI 比较的所有objectIDs 来检测插入、删除和移动。这就是为什么如果您设置的批处理大小比没有批处理慢得多,它会加载所有批处理。

不过,您可以禁用includesPropertyValues以防止Core Data 将所有数据加载到其行缓存中。

禁用它后,SQL 查询将变为:

SELECT 0, t0.Z_PK FROM ZITEM t0
Run Code Online (Sandbox Code Playgroud)

而不是默认启用它获取所有字段:

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZTIMESTAMP FROM ZITEM t0
Run Code Online (Sandbox Code Playgroud)

要查看查询,请使用启动参数-com.apple.CoreData.SQLDebug 4(数字越大,日志输出越多)。

您还可以利用 List 的惰性行为,将 移动$0.name到子视图的主体中,以便核心数据仅访问数据库来读取数据,并在对象滚动到屏幕上时触发对象的错误,并且您可以将在日志中看到这一点:

CoreData: details: SQLite: EXPLAIN QUERY PLAN SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZTIMESTAMP, t0.ZTITLE FROM ZITEM t0 WHERE  t0.Z_PK = ? 
     2 0 0 SEARCH t0 USING INTEGER PRIMARY KEY (rowid=?)
CoreData: annotation: fault fulfilled from database for : 0xa17723cef738c1a0 <x-coredata://FA6F121D-805F-4558-AAAC-F5F60A117256/Item/p9778> with row values: <NSSQLRow: 0x600001745b60>{Item 1-9778-1 timestamp=2022-02-24 13:58:39 +0000 title=NULL and to-manys=0x0}
Run Code Online (Sandbox Code Playgroud)

这些改进可能会带来足够大的性能提升,如下所示:

extension Item {
    static func myFetchRequest() -> NSFetchRequest<Item> {
        let fr = Self.fetchRequest()
        fr.includesPropertyValues = false
        fr.sortDescriptors = [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)]
        return fr
    }
}

struct MyView: View {
    @ObservedObject var item: Item
    
    var body: some View {
        Text(item.timestamp!, formatter: itemFormatter)
    }
}

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        fetchRequest: Item.myFetchRequest(),
        animation: .default)
    private var items: FetchedResults<Item>

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    MyView(item: item)
...
Run Code Online (Sandbox Code Playgroud)

如果您使用@SectionedFetchRequest,那么您将需要获取分段中使用的字段,这可以通过保留includesPropertyValues其默认值 true 并设置propertiesToFetch为用于分段的属性名称数组来完成。

例如

extension Item {
    static func myFetchRequest() -> NSFetchRequest<Item> {
        let fr = Self.fetchRequest()
        fr.propertiesToFetch = ["title"]
        fr.sortDescriptors = [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)]
        return fr
    }
}
Run Code Online (Sandbox Code Playgroud)

初始查询结果为:

CoreData: sql: SELECT 1, t0.Z_PK, t0.ZTITLE FROM ZITEM t0 ORDER BY t0.ZTIMESTAMP
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,没有选择timestamp. 当列表滚动并且访问对象时,将执行其他查询来检索所有字段。