
GRDB.swift ValueObservation 完全指南数据库值观察、调度、共享与性能优化【免费下载链接】GRDB.swiftA toolkit for SQLite databases, with a focus on application development项目地址: https://gitcode.com/GitHub_Trending/gr/GRDB.swiftValueObservation是 GRDB.swift 中用于追踪数据库请求结果变化的工具它持续监听数据库的插入、更新与删除并在数据库变化时向应用通知全新的值。无论你使用原始 SQL 还是 Query Interface 执行写入乃至通过外键级联动作foreign keys actions或 SQL 触发器 产生的间接变更ValueObservation都能感知。读完本文你将掌握如何在 GRDB.swift 中创建、启动与停止观察理解其调度与共享机制、精确指定被跟踪的数据库区域并学会在实战中规避未检测到的变更与数据库争用等性能陷阱。本文核心文档位于 GRDB/Documentation.docc/Extension/ValueObservation.md对应源码实现位于 GRDB/ValueObservation/ 目录测试用例位于 Tests/GRDBTests/ValueObservation/。ValueObservation 使用入门使用ValueObservation遵循一个固定流程保持数据库连接存活、创建观察、启动观察、适时停止观察。前提保持数据库连接观察的整个生命周期内必须保证一个唯一的数据库连接DatabaseQueue或DatabasePool始终保持打开。观察启动后会将数据库连接强引用起来直到观察停止只要观察处于激活状态连接就不会被释放。从 ValueObservation.swift 中start(in:)的实现可以看到观察最终通过reader._add(observation:scheduling:onChange:)挂载到 reader 上观察期间连接由观察本身持有。第一步创建观察用闭包描述要观察什么值闭包在任意需要的时候被调用以重新获取该值let observation ValueObservation.tracking { db in // Fetch and return the observed value } // 例如观察 [Player]追踪所有玩家 let observation ValueObservation.tracking { db in try Player.fetchAll(db) } // 相同的观察使用简写写法 let observation ValueObservation.tracking(Player.fetchAll)被观察的值没有类型限制一个观察可以执行多个请求、跨多张数据库表也可以使用原始 SQL。例如tracking(_:)的源码注释ValueObservation.swift中就展示了同时统计玩家总数与 Top10 高分玩家的HallOfFame聚合结构以及Int.fetchOne(db, sql: SELECT MAX(score) FROM player)这类原始 SQL 观察。第二步启动观察调用start(in:)即可开始接收变化通知let cancellable observation.start(in: dbQueue) { error in // Handle error } onChange: { (players: [Player]) in print(Fresh players, players) }从 ValueObservation.swift 的start(in:scheduling:onError:onChange:)实现可以看出onError会被拼接进内部事件events.didFail随后调用reader._add将观察交给DatabaseReader的底层观察器DatabaseQueue走ValueWriteOnlyObserverDatabasePool走ValueConcurrentObserver见下文。第三步停止观察调用start返回对象的DatabaseCancellable/cancel()方法即可停止观察当 cancellable 被释放deallocated时会自动取消cancellable.cancel()AnyDatabaseCancellable的实现DatabaseCancellable.swift在deinit中自动调用cancel()所以即使你不手动取消只要持有者释放观察也会自动停止。观察也会在发生错误时停止。转换为 Async Sequence 与 Combine PublisherValueObservation还可以转换为异步序列或 Combine 发布者也可借助配套库 RxGRDB 转为 RxSwift observableAsync sequencedo { for try await players in observation.values(in: dbQueue) { print(Fresh players, players) } } catch { // Handle error }values(in:scheduling:bufferingPolicy:)的实现ValueObservation.swift内部把start包装进AsyncThrowingStream默认调度到协作线程池.task并支持自定义缓冲策略迭代器被释放时观察随之取消。Combine Publisherlet cancellable observation.publisher(in: dbQueue).sink { completion in // Handle completion } receiveValue: { (players: [Player]) in print(Fresh players, players) }publisher(in:scheduling:)ValueObservation.swift默认使用.async(onQueue: .main)调度返回类型为DatabasePublishers.ValueReducer.Value其ValueSubscription内部实现了背压demand管理与取消转发。ValueObservation 的行为特征理解ValueObservation的默认行为是正确使用它的前提先通知初始值观察启动后会先通知一个初始值然后再通知后续变化。只通知已提交到磁盘的变化未提交事务中的修改不会被通知。按被修改的成分触发默认情况下只要被观察值的任一组成部分任一被取出的列、行等被修改就会通知一次新值。这一行为可以通过指定跟踪区域来配置见下文指定被跟踪的区域。默认在主线程异步通知初始值、后续变化与错误默认都通过主 actor 异步调度可通过调度器配置见下文ValueObservation 调度。默认在变化提交后立即取新值特别地在主线程修改数据库会触发主线程上的取数。此行为也可配置。可能合并通知多个后续变化可能被合并为一次通知。可能连续通知相同值你可以用removeDuplicates()过滤掉不想要的重复值。保持连接存活启动观察会强引用数据库连接直到观察停止。停止条件start返回的 cancellable 被取消或释放或观察发生错误时观察停止。ValueObservation 不适合的场景Important: 有一些用例ValueObservation并不适合。例如应用可能需要处理绝对所有的变化并且完全避免合并也可能需要在数据库文件中执行任何进一步修改之前处理变化。这两种场景需要追踪单个事务而非值请改用DatabaseRegionObservation。如果需要在变化提交到磁盘之前处理它们请使用TransactionObserver。从源码结构看ValueConcurrentObserver.swift 与 ValueWriteOnlyObserver.swift两类 observer 都通过实现TransactionObserver协议的databaseDidCommit回调在事务提交后触发取数与通知这正是只通知已提交变化的底层机制也因此决定了它不适合处理提交前或逐事务的场景。ValueObservation 调度Scheduling默认调度主 actor 异步默认情况下初始值、变化与错误都在主 actor 上异步通知// 默认调度 let cancellable observation.start(in: dbQueue) { error in // 该闭包被 MainActor 隔离 } onChange: { value in // 该闭包被 MainActor 隔离 print(Fresh value, value) }可以通过给start()方法增加scheduling参数改变这一行为。.immediate立即同步通知首个值ValueObservationMainActorScheduler/immediate调度器在主 actor 上通知所有值并且观察启动时立即通知第一个值// Immediate 调度在订阅时立即通知初始值 let cancellable observation .start(in: dbQueue, scheduling: .immediate) { error in // 在主 actor 上调用 } onChange: { value in // 在主 actor 上调用 print(Fresh value, value) } // - 到这里 Fresh value 已经被打印它非常适用于图形应用你可以立即配置视图而不必等待初始值异步取回也无需实现空屏或加载屏更不会出现不希望看到的初始动画。注意取首个值的期间用户界面无响应因此immediate调度只适用于非常快的数据库请求其实现位于 ValueObservationScheduler.swiftImmediateValueObservationScheduler.immediateInitialValue()会先断言调用方处于主线程否则触发致命错误ValueObservation must be started from the main thread.返回true表示初始值立即执行scheduleOnMainActor则把后续值DispatchQueue.main.async派发。.async(onQueue:)在指定派发队列异步调度ValueObservationScheduler/async(onQueue:)调度器在你指定的派发队列上异步调度值与错误。务必提供串行队列因为并发队列如DispachQueue.global(qos: .default)会打乱新值通知的顺序// Async 调度在指定派发队列上通知所有值 let myQueue: DispatchQueue let cancellable observation .start(in: dbQueue, scheduling: .async(myQueue)) { error in // 在 myQueue 上异步调用 } onChange: { value in // 在 myQueue 上异步调用 print(Fresh value, value) }对应实现为AsyncValueObservationSchedulerValueObservationScheduler.swiftimmediateInitialValue()返回falseschedule(_:)执行queue.async(execute: action)。.task在协作线程池异步调度ValueObservationScheduler/task调度器在 Swift 协作线程池上异步调度值与错误。当你把ValueObservation转成 async sequence 时会隐式使用它当你打算把共享观察作为 async sequence 消费时也可以显式指定do { for try await players in observation.values(in: dbQueue) { // 在协作线程池上调用 print(Fresh players, players) } } catch { // Handle error } let sharedObservation observation.shared(in: dbQueue, scheduling: .task) do { for try await players in sharedObservation.values() { // 在协作线程池上调用 print(Fresh players, players) } } catch { // Handle error }实现为TaskValueObservationSchedulerValueObservationScheduler.swift它在内部用AsyncStream建立动作通道由一个Task(priority:)循环消费并执行回调从而保证通知顺序与提交顺序一致同时支持task(priority:)指定任务优先级。对取数执行的控制如上所述scheduling参数控制的是 change/error 回调的执行位置。你还可以部分控制数据库取数的执行使用.immediate调度时初始取数总是同步地在主 actor、观察启动时执行以便初始值能被立即通知。使用默认的.async调度时初始取数总是异步执行绝不阻塞主线程。默认情况下新值在数据库变化后立即被取出。特别地在主线程修改数据库也会在主线程触发取数。要改变主线程修改数据库会触发主线程取数这一默认行为、保证新值绝不在主线程取出你需要一个DatabasePool以及用tracking(regions:fetch:)或trackingConstantRegion(_:)创建的优化观察。使用前务必通读这两个方法的文档否则可能写出漏掉部分数据库变化的观察。从实现看这个保证来自ValueConcurrentObserverValueConcurrentObserver.swift对于恒定区域.constantRegion与.constantRegionRecordedFromSelection的观察变化提交后会调用asyncFetch在数据库池的读连接上并发取数并配合fetchingStateMutex维护取数中/需要再取一次的状态机实现变化合并。而ValueWriteOnlyObserver用于DatabaseQueue总是在写连接上取数ValueWriteOnlyObserver.swift无法获得同样的并发取数能力。一个实用的工程模式是应用中使用DatabasePool测试与 Xcode 预览中使用内存DatabaseQueue二者通过共同的协议DatabaseWriter抽象保证开发与生产行为一致。共享 ValueObservation共享一个ValueObservation可以节省数据库资源数据库变化发生时只取一次新值然后通知给共享观察的所有客户端。用shared(in:scheduling:extent:)构建共享观察// SharedValueObservation[Player] let sharedObservation ValueObservation .tracking { db in try Player.fetchAll(db) } .shared(in: dbQueue)ValueObservation与SharedValueObservation几乎相同但后者没有map这类操作符。作为替代你可以使用 Combine APIlet cancellable try sharedObservation .publisher() // 把共享观察转为 Combine Publisher .map { ... } // 使用 Combine 的 map 操作符 .sink(...)共享的关键语义同一实例才共享从 SharedValueObservation.swift 的实现看只有从同一个SharedValueObservation实例启动观察底层订阅才会被共享// 共享 let sharedObservation ValueObservation.tracking { db in ... }.shared(in: dbQueue) let cancellable1 sharedObservation.start(...) let cancellable2 sharedObservation.start(...) // 不共享 let cancellable1 ValueObservation.tracking { db in ... }.shared(in: dbQueue).start(...) let cancellable2 ValueObservation.tracking { db in ... }.shared(in: dbQueue).start(...)extent共享观察的存活范围SharedValueObservationExtentSharedValueObservation.swift决定共享观察的数据库监听何时停止// 默认值订阅数降到 0 时停止数据库观察下一次订阅时重启。 // // 数据库错误可以通过重新订阅共享观察来恢复。 let sharedObservation ValueObservation .tracking { db in try Player.fetchAll(db) } .shared(in: dbQueue, extent: .whileObserved) // 仅在共享观察被释放、且所有订阅被取消时才停止数据库观察。 // // 该 extent 使共享观察无法从数据库错误中恢复。 // 要恢复需要新建一个 SharedValueObservation 实例。 let sharedObservation ValueObservation .tracking { db in try Player.fetchAll(db) } .shared(in: dbQueue, extent: .observationLifetime)从源码看handleCancel在订阅者退出且extent .whileObserved时清空底层观察而handleError则依据 extent 决定错误后是否允许重订阅恢复.whileObserved会重置状态以便下次订阅重启观察.observationLifetime则保留失败结果直接通知。此外SharedValueObservation.start还实现了重入安全即使在上游观察尚未真正启动、从首个值通知回调里再次start也不会重复创建底层观察SharedValueObservation.swift。指定被跟踪的区域Tracked Region标准的tracking(_:)让你观察某个被取值的所有变化但有些场景需要更细粒度的控制。考虑这样的需求只想在某个玩家的score列变化时取得该玩家行。可以用tracking(region:_:fetch:)精确实现let observation ValueObservation.tracking( // 定义被跟踪的数据库区域 //id 为 1 的玩家的 score 列 region: Player.select(\.score).filter(id: 1), // 定义该区域变化时要取什么值 //id 为 1 的玩家 fetch: { db in try Player.fetchOne(db, id: 1) } )该方法让你把**被观察的区域observed regions与被取的值fetched value**完全分离获得最大灵活性。关于可跟踪区域的更多信息参见DatabaseRegionConvertible。源码层面ValueObservation.swifttracking(region:_:fetch:)与tracking(regions:fetch:)会把区域清单存入trackingMode .constantRegion(regions)由DatabaseRegion.union合并所有区域tracking(regions:fetch:)还支持传入多个区域例如同时跟踪player与team两张表。区域可以来自多种来源以下是tracking(regions:fetch:)支持的区域写法// 跟踪整个数据库 let observation ValueObservation.tracking( regions: [.fullDatabase], fetch: { db in ... }) // 跟踪完整的 player 表 let observation ValueObservation.tracking( regions: [Player.all()], fetch: { db in ... }) // 同上使用 Table let observation ValueObservation.tracking( regions: [Table(player)], fetch: { db in ... }) // 跟踪 player 表中 id 为 42 的行 let observation ValueObservation.tracking( regions: [Player.filter(id: 42)], fetch: { db in ... }) // 跟踪 player 表中的 score 列 let observation ValueObservation.tracking( regions: [Player.select(\.score)], fetch: { db in ... }) // 同上使用原始 SQL let observation ValueObservation.tracking( regions: [SQLRequest(SELECT score FROM player)], fetch: { db in ... })注意这些恒定区域的优化观察与tracking(_:)不同它们可以减少数据库争用取新值时不会阻塞数据库写入还可以避免在主线程修改数据库后于主线程取新值。这些调度优化仅在观察从DatabasePool启动时生效从DatabaseQueue启动时优化不生效但通知的值完全相同。这也是应用用 pool、测试用内存 queue这一模式的底层依据。恒定区域 vs 非恒定区域从 ValueObservation.swift 的ValueObservationTrackingMode可以看出GRDB 把观察分为三种跟踪模式constantRegion(regions)恒定区域由tracking(region:_:fetch:)/tracking(regions:fetch:)显式提供constantRegionRecordedFromSelection恒定区域由trackingConstantRegion(_:)从取数闭包中推断记录每次查询实际访问的表/列/行nonConstantRegionRecordedFromSelection非恒定区域tracking(_:)使用每次取数都会重新记录被访问的区域。对于非恒定区域的观察如tracking { db in try Player.fetchOne(db, id: Int.random(in: 1...1000)) }每次取数访问的区域都可能不同因此ValueConcurrentObserver无法并发取数必须在写连接上同步取数并更新跟踪区域ValueConcurrentObserver.swift否则可能漏掉变化。trackingConstantRegion(_:)的适用与禁忌trackingConstantRegion(_:)创建的优化观察其取数闭包必须只访问单一且恒定的数据库区域由表、列、单个行的 rowid 组成该区域之外的一切变化都不会被通知// 跟踪完整的 player 表 let observation ValueObservation.trackingConstantRegion { db - [Player] in try Player.fetchAll(db) } // 跟踪 player 表中 id 为 42 的行 let observation ValueObservation.trackingConstantRegion { db - Player? in try Player.fetchOne(db, key: 42) } // 跟踪 player 表中的 score 列 let observation ValueObservation.trackingConstantRegion { db - Int? in try Int.fetchOne(db, sql: SELECT MAX(score) FROM player) } // 同时跟踪 player 和 team 两张表 let observation ValueObservation.trackingConstantRegion { db - ([Team], [Player]) in let teams try Team.fetchAll(db) let players try Player.fetchAll(db) return (teams, players) }不跟踪恒定区域的观察绝不能使用此方法否则部分变化不会被通知。例如以下观察都不适合trackingConstantRegion(_:)// 并不总是跟踪 player 表中的同一行 let observation ValueObservation.tracking { db - Player in let config try AppConfiguration.find(db) let playerId: Int64 config.favoritePlayerId return try Player.find(db, id: playerId) } // 并不总是跟踪 player 表或并非总是同一批行 let observation ValueObservation.tracking { db - [Player] in let config try AppConfiguration.find(db) let playerIds: [Int64] config.favoritePlayerIds // playerIds 本身会变化而且当它为空时整个 player 表都不再被跟踪。 return try Player.fetchAll(db, ids: playerIds) } // 有时跟踪 food 表有时跟踪 beverage 表 let observation ValueObservation.tracking { db - Int in let config try AppConfiguration.find(db) switch config.selection { case .food: return try Food.fetchCount(db) case .beverage: return try Beverage.fetchCount(db) } }由于只有恒定区域的观察才能获得关键调度优化如新值绝不在主线程取出的保证你始终可以通过两种方式创建恒定区域的优化观察方式一用tracking(regions:fetch:)在创建观察时显式列出所有跟踪区域// 显式跟踪 appConfiguration、food、beverage 三张表的优化观察 let observation ValueObservation.tracking( regions: [ AppConfiguration.all(), Food.all(), Beverage.all(), ], fetch: { db - Int in let config try AppConfiguration.find(db) switch config.selection { case .food: return try Food.fetchCount(db) case .beverage: return try Beverage.fetchCount(db) } })方式二用Database/registerAccess(to:)在取数闭包内扩展跟踪区域// 隐式跟踪 appConfiguration 表显式跟踪 food 和 beverage 的优化观察 let observation ValueObservation.trackingConstantRegion { db - Int in try db.registerAccess(to: Food.all()) try db.registerAccess(to: Beverage.all()) let config try AppConfiguration.find(db) switch config.selection { case .food: return try Food.fetchCount(db) case .beverage: return try Beverage.fetchCount(db) } }应对未检测到的变更Undetected Changes在以下情况下ValueObservation不会取数与通知新值由外部数据库连接执行的变化由非 GRDB 编译执行的 SQLite 语句非DELETE、INSERT、UPDATE带来的变化对数据库模式schema的修改以及对sqlite_master等内部系统表的修改对WITHOUT ROWID表的变化。要想在发生此类未被检测到的变化后让观察通知新值应用需要采取显式动作。例如取消并重启观察或者在写事务中调用Database/notifyChanges(in:)try dbQueue.write { db in // 通知观察数据库中执行了一些变化 try db.notifyChanges(in: .fullDatabase) // 通知观察player 表发生变化 try db.notifyChanges(in: Player.all()) // 等价写法 try db.notifyChanges(in: Table(player)) }notifyChanges(in:)的实现位于 Database.swift它会把区域转换成事件类型通过observationBroker通知所有事务观察者跳过只读事务因为只读事务不会产生提交通知。ValueObservation 性能剖析与优化建议本节进一步描述ValueObservation的运行时行为并为要求苛刻的应用提供优化建议。触发粒度为区域而非值ValueObservation由可能修改被跟踪值的数据库事务触发。精确地说ValueObservation跟踪的是一个DatabaseRegion区域而不是值。例如如果观察的是玩家最高分那么所有影响player表score列的事务任何更新、插入或删除都会触发观察即使最高分本身并未改变。你可以用removeDuplicates()过滤掉这些多余的重复通知。从实现看RemoveDuplicatesreducerRemoveDuplicates.swift在 reducer 内部缓存上一次的值当新旧值按谓词判定为重复时返回nil从而阻止通知。它有两种形态removeDuplicates()要求Reducer.Value: Equatable与removeDuplicates(by:)自定义谓词。当被观察值不适合实现Equatable时一个实用技巧是先观察Row/DatabaseValue这类本身就遵守Equatable的原始值再用map转换// 观察去重后的 Player? let request Player.filter(id: 42) let observation ValueObservation .tracking { db in try Row.fetchOne(db, request) } .removeDuplicates() .map { row in try row.map(Player.init(row:)) }观察会造成数据库争用换句话说活跃的观察会占用受限的数据库资源。当被有影响力的事务触发时观察会取新值从而延迟其他应用组件的数据库读写访问。必要时可以采取以下手段帮助 GRDB 优化观察、降低争用Important:保持观察数量有界。尤其不要独立观察列表中的每一个元素而应该用单个观察观察整个列表。Tip: 尽可能停止观察。例如如果UIViewController需要展示数据库值可以在viewWillAppear启动观察、在viewWillDisappear停止观察。在 SwiftUI 应用中可以借助 GRDBQuery 配套库及其View.queryObservation(_:)方法。Tip: 尽可能共享观察。每次调用ValueObservation.start都会触发独立的刷新。当应用多个组件关心同一个值时考虑用shared(in:scheduling:extent:)共享观察。其收益在 SharedValueObservation.swift 中实现底层只有一份startObservation多个Client共享同一份取数结果。Tip: 当观察需要对取到的原始值做进一步加工时使用map(_:)操作符// 普通观察 let observation ValueObservation.tracking { db - MyValue in let players try Player.fetchAll(db) return computeMyValue(players) } // 优化观察 let observation ValueObservation .tracking { db try Player.fetchAll(db) } .map { players in computeMyValue(players) }map操作符在执行其工作时不阻塞数据库访问也不阻塞主线程。从 Map.swift 的实现看map通过mapReducer把变换函数注入 reducer 的_value阶段——而 reduce 阶段运行在专门的reduceQueue上见 ValueConcurrentObserver.swift与数据库调度队列分离这正是不阻塞数据库访问的来源。requiresWriteAccess属性ValueObservation.swift则用于让取数闭包获得写访问权自动包裹在 savepoint 中代价是可能禁用部分调度优化。Tip: 当观察跟踪恒定区域时使用tracking(regions:fetch:)或trackingConstantRegion(_:)创建优化观察。务必阅读这两个方法的文档否则可能写出漏掉部分数据库变化的观察。截断型 WAL checkpoint 的影响截断型 WAL checkpoint 会影响 ValueObservation。这类 checkpoint 通过Database/checkpoint(_:on:)或PRAGMA wal_checkpoint执行。当观察从DatabasePool启动、而数据库缺少 wal 文件或 wal 文件为空时观察在启动时总会通知两个值即使数据库内容没有变化。这是因为无法创建检测观察启动期间未发生任何变化所需的 wal snapshot。如果应用执行截断型 checkpoint可以通过在启动观察前重建一个非空的 wal 文件来避免此行为——执行任意一个 no-op 事务即可例如创建再删除一张临时表。这一行为与源码中的降级路径完全对应ValueConcurrentObserver在创建 WAL snapshot 失败DatabaseError.SQLITE_ERROR时会退化到asyncStartWithoutWALSnapshotValueConcurrentObserver.swift该路径在启动观察时无条件触发一次databaseDidChange并执行二次取数ValueConcurrentObserver.swift因而必然出现两个初始值而在SQLITE_ENABLE_SNAPSHOT可用时则会通过WALSnapshotTransaction比较initialFetchTransaction.walSnapshot.compare(currentWALSnapshot)来精确判断启动窗口内数据库是否被修改过ValueConcurrentObserver.swift。内部运行机制速览观察周期Fetch → Reduce → Notify两类底层观察器ValueConcurrentObserver用于DatabasePoolValueWriteOnlyObserver用于DatabaseQueue都执行同一个四步观察周期启动观察或检测数据库变化Fetch取数从数据库取原始值Reduce归约把取到的值转换为被观察的值。map与removeDuplicates()等操作符正是通过包装 reducer 在这一阶段工作的Notify通知在数据库变化或出错时调用用户回调。区别在于取数位置ValueWriteOnlyObserver总是从写连接取数这也是它名字的由来因此天然不会漏掉变化但无法并发取数ValueConcurrentObserver则利用DatabasePool的读连接并发取数ValueConcurrentObserver.swift并借助fetchingStateMutex与reduceQueue保证通知顺序与事务顺序一致。通知顺序保证reduceQueue保证了新值通知与事务保持相同顺序ValueConcurrentObserver.swift。它独立于串行化写派发队列因为map、removeDuplicates()等归约计算不应锁住数据库。这也解释了为何scheduling指定的派发队列必须是串行的并发队列会破坏这一顺序保证。调试工具ValueObservation提供了两套调试手段handleEvents(willStart:willFetch:willTrackRegion:databaseDidChange:didReceiveValue:didFail:didCancel:)在各观察事件发生时执行指定闭包ValueObservation.swiftprint(_:to:)打印所有观察事件的日志可自定义输出流ValueObservation.swift。其输出样例清晰地展示了观察生命周期Observe player count: start Observe player count: fetch Observe player count: tracked region: player(*) Observe player count: value: 0 Observe player count: database did change Observe player count: fetch Observe player count: value: 1小结ValueObservation是 GRDB.swift 面向应用开发的响应式数据层核心通过区域跟踪 提交后取数 reducer 归约 调度器通知的流水线把 SQLite 的变更转化为应用可消费的异步值流。使用它的要点可以归纳为启动前根据是否跟踪恒定区域在tracking(_:)、trackingConstantRegion(_:)、tracking(region:_:fetch:)/tracking(regions:fetch:)中选择正确的创建方式启动时根据 UI 需求选择调度器默认主 actor 异步、.immediate、.async(onQueue:)、.task并牢记DatabasePool才能兑现新值绝不在主线程取出的优化承诺运行中用map与removeDuplicates()控制通知内容用shared(in:scheduling:extent:)共享观察减少取数次数特殊场景外部连接写入、schema 变化、WITHOUT ROWID表等变化无法被自动检测需用db.notifyChanges(in:)显式唤醒观察截断型 WAL checkpoint 可能带来双初始值需在启动前重建非空 wal 文件。进一步阅读可在仓库中展开核心实现见 GRDB/ValueObservation/调度器见 ValueObservationScheduler.swift共享观察见 SharedValueObservation.swift行为验证见 Tests/GRDBTests/ValueObservation/ 下的系列测试ValueObservation与其他 GRDB 能力DatabaseRegionObservation、TransactionObserver、Combine 支持等的协同可参考 GRDB/Documentation.docc/ 中的相关文档。【免费下载链接】GRDB.swiftA toolkit for SQLite databases, with a focus on application development项目地址: https://gitcode.com/GitHub_Trending/gr/GRDB.swift创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考