使用 Realm 和角色 - Swift SDK
在此页面上
从 Realm Swift SDK 版本 10.39.0 开始,Realm 支持将 Realm 与 Swift Actor 结合使用的内置功能。Realm 的 Actor 支持提供了一种管理线程或调度队列来执行异步工作的替代方案。您可以通过几种不同的方式将 Realm 与 Actor 一起使用:
仅 在具有使用 Actor 隔离的 Realm 的特定 Actor 上使用 Realm
根据应用程序的需求跨 Actor 使用 Realm
若要将所有 Realm 访问权限限制为单个 Actor,则可能需要使用 Actor 隔离的 Realm。这消除了跨 Actor 边界传递数据的需要,并且可以简化数据争用调试。
若要对不同的 Actor 执行不同类型的工作,则可能需要跨 Actor 使用 Realm。例如,您可能想要读取 MainActor 上的对象,但使用后台 Actor 进行大量写入。
有关 Swift actor 的一般信息,请参阅 Apple 的 Actor 文档。
先决条件
要在 Swift Actor 中使用 Realm,您的项目必须:
使用 Realm Swift SDK 10.39.0 或更高版本
使用 Swift 5.8/Xcode 14.3
此外,我们强烈建议在您的项目中启用这些设置:
SWIFT_STRICT_CONCURRENCY=complete
:启用严格的并发检查OTHER_SWIFT_FLAGS=-Xfrontend-enable-actor-data-race-checks
:启用运行时 actor 数据争用检测
关于本页中的示例
本页中的示例使用以下模型:
class Todo: Object { true) var _id: ObjectId (primaryKey: var name: String var owner: String var status: String }
打开使用 Actor 隔离的 Realm
您可以使用 Swift async/await 语法来等待打开 Realm。
使用 try await Realm()
初始化 Realm 会打开使用 MainActor 隔离的 Realm。或者,您可以在使用 await
语法打开 Realm 时明确指定 Actor。
func mainThreadFunction() async throws { // These are identical: the async init produces a // MainActor-isolated Realm if no actor is supplied let realm1 = try await Realm() let realm2 = try await Realm(actor: MainActor.shared) try await useTheRealm(realm: realm1) }
您可以指定默认配置,也可以在打开使用 Actor 隔离的组件隔离 Realm 时自定义配置:
func mainThreadFunction() async throws { let username = "Galadriel" // Customize the default realm config var config = Realm.Configuration.defaultConfiguration config.fileURL!.deleteLastPathComponent() config.fileURL!.appendPathComponent(username) config.fileURL!.appendPathExtension("realm") // Open an actor-isolated realm with a specific configuration let realm = try await Realm(configuration: config, actor: MainActor.shared) try await useTheRealm(realm: realm) }
有关配置 Realm 的更多一般信息,请参阅配置和打开 Realm — Swift SDK。
您可以打开同步 Realm 作为使用 Actor 隔离的 Realm:
func mainThreadFunction() async throws { // Initialize the app client and authenticate a user let app = App(id: APPID) let user = try await app.login(credentials: Credentials.anonymous) // Configure the synced realm var flexSyncConfig = user.flexibleSyncConfiguration(initialSubscriptions: { subs in subs.append(QuerySubscription<Todo>(name: "all_todos"))}) flexSyncConfig.objectTypes = [Todo.self] // Open and use the synced realm let realm = try await Realm(configuration: flexSyncConfig, actor: MainActor.shared, downloadBeforeOpen: .always) try await useTheSyncedRealm(realm: realm) }
有关打开同步 Realm 的更多一般信息,请参阅配置并打开同步 Realm — Swift SDK。
定义自定义 Realm Actor
您可以定义特定 Actor 来管理异步上下文中的 Realm。您可以使用此 Actor 来管理 Realm 访问并执行写入操作。
actor RealmActor { // An implicitly-unwrapped optional is used here to let us pass `self` to // `Realm(actor:)` within `init` var realm: Realm! init() async throws { realm = try await Realm(actor: self) } var count: Int { realm.objects(Todo.self).count } func createTodo(name: String, owner: String, status: String) async throws { try await realm.asyncWrite { realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": name, "owner": owner, "status": status ]) } } func getTodoOwner(forTodoNamed name: String) -> String { let todo = realm.objects(Todo.self).where { $0.name == name }.first! return todo.owner } struct TodoStruct { var id: ObjectId var name, owner, status: String } func getTodoAsStruct(forTodoNamed name: String) -> TodoStruct { let todo = realm.objects(Todo.self).where { $0.name == name }.first! return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status) } func updateTodo(_id: ObjectId, name: String, owner: String, status: String) async throws { try await realm.asyncWrite { realm.create(Todo.self, value: [ "_id": _id, "name": name, "owner": owner, "status": status ], update: .modified) } } func deleteTodo(id: ObjectId) async throws { try await realm.asyncWrite { let todoToDelete = realm.object(ofType: Todo.self, forPrimaryKey: id) realm.delete(todoToDelete!) } } func close() { realm = nil } }
角色隔离式 Realm 可同时与本地角色或全局角色一起使用。
// A simple example of a custom global actor actor BackgroundActor: GlobalActor { static var shared = BackgroundActor() } func backgroundThreadFunction() async throws { // Explicitly specifying the actor is required for anything that is not MainActor let realm = try await Realm(actor: BackgroundActor.shared) try await realm.asyncWrite { _ = realm.create(Todo.self, value: [ "name": "Pledge fealty and service to Gondor", "owner": "Pippin", "status": "In Progress" ]) } // Thread-confined Realms would sometimes throw an exception here, as we // may end up on a different thread after an `await` let todoCount = realm.objects(Todo.self).count print("The number of Realm objects is: \(todoCount)") } func mainThreadFunction() async throws { try await backgroundThreadFunction() }
在隔离函数中同步使用 Realm Actor
当函数被限制为特定 Actor 时,您可以同步使用 Actor 隔离的 Realm。
func createObject(in actor: isolated RealmActor) async throws { // Because this function is isolated to this actor, you can use // realm synchronously in this context without async/await keywords try actor.realm.write { actor.realm.create(Todo.self, value: [ "name": "Keep it secret", "owner": "Frodo", "status": "In Progress" ]) } let taskCount = actor.count print("The actor currently has \(taskCount) tasks") } let actor = try await RealmActor() try await createObject(in: actor)
在异步函数中使用 Realm Actor
当函数不限于特定 Actor 时,您可以将 Realm Actor 与 Swift 的 async/await 语法结合使用。
func createObject() async throws { // Because this function is not isolated to this actor, // you must await operations completed on the actor try await actor.createTodo(name: "Take the ring to Mount Doom", owner: "Frodo", status: "In Progress") let taskCount = await actor.count print("The actor currently has \(taskCount) tasks") } let actor = try await RealmActor() try await createObject()
写入使用 Actor 隔离的 Realm
使用 Actor 隔离的 Realm 可以使用 Swift async/await 语法进行异步写入。使用 try await realm.asyncWrite { ... }
暂停当前任务,获取写锁而不阻塞当前线程,然后调用该区块。Realm 在后台线程上将数据写入磁盘,并在完成后恢复任务。
以上定义的示例 RealmActor
中的此函数显示如何写入使用 Actor 隔离的领 Realm:
func createTodo(name: String, owner: String, status: String) async throws { try await realm.asyncWrite { realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": name, "owner": owner, "status": status ]) } }
您可以使用 Swift 的异步语法来执行此写入:
func createObject() async throws { // Because this function is not isolated to this actor, // you must await operations completed on the actor try await actor.createTodo(name: "Take the ring to Mount Doom", owner: "Frodo", status: "In Progress") let taskCount = await actor.count print("The actor currently has \(taskCount) tasks") } let actor = try await RealmActor() try await createObject()
这不会在等待写入时阻塞调用线程。它不会在调用线程上执行 I/O。对于小规模写入,可以安全地通过 @MainActor
函数使用它,而不会阻塞用户界面。由于复杂性和/或平台资源限制而对应用程序性能产生负面影响的写入操作仍可能受益于在后台线程上完成。
仅使用 Actor 隔离的 Realm 或在 @MainActor
函数中支持异步写入。
跨 Actor 边界传递 Realm 数据
Realm 对象不可 Sendable,也不能直接跨越 Actor 边界。要跨越 Actor 边界传递 Realm 数据,您有两种选择:
将
ThreadSafeReference
传递给 Actor 或从 Actor 传递传递 Sendable 的其他类型,例如直接传递值或通过创建结构体来跨越 Actor 边界进行传递
传递 ThreadSafeReference
您可以在您有权访问对象的 actor 上创建ThreadSafeReference 。 在本例中,我们在 上创建一个ThreadSafeReference
MainActor
。然后,将ThreadSafeReference
传递给目标 actor。
// We can pass a thread-safe reference to an object to update it on a different actor. let todo = todoCollection.where { $0.name == "Arrive safely in Bree" }.first! let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo)
在目标 Actor 上,您必须在写事务中 resolve()
引用之后,才能使用它。这将检索该 Actor 本地对象的版本。
actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReference<Todo>) throws { let realm = try! Realm() try realm.write { // Resolve the thread safe reference on the Actor where you want to use it. // Then, do something with the object. let todoOnActor = realm.resolve(tsr) realm.delete(todoOnActor!) } } }
传递可发送类型
虽然 Realm 对象不 Sendable,但您可以通过跨 Actor 边界传递 Sendable 类型来解决此问题。您可以使用一些策略来传递 Sendable 类型并跨 Actor 边界处理数据:
传递 Sendable Realm 类型或基元值而不是完整的 Realm 对象
传递对象的主键并在另一个 Actor 上查询该对象
创建 Realm 对象的 Sendable 表示形式,例如结构体
传递 Sendable Realm 类型和基元值
如果您只需要 Realm 对象中的一条信息(例如 String
或 Int
,则可以直接跨 Actor 传递该值,而不是传递 Realm 对象。有关 Realm Sendable 类型的完整列表,请参阅 Sendable、不 Sendable 和线程限制类型。
func mainThreadFunction() async throws { // Create an object in an actor-isolated realm. // Pass primitive data to the actor instead of // creating the object here and passing the object. let actor = try await RealmActor() try await actor.createTodo(name: "Prepare fireworks for birthday party", owner: "Gandalf", status: "In Progress") // Later, get information off the actor-confined realm let todoOwner = await actor.getTodoOwner(forTodoNamed: "Prepare fireworks for birthday party") }
传递主键并在另一个 Actor 上的查询对象
若要在另一个 Actor 上使用 Realm 对象,您可以共享主键并在要使用它的 Actor 上查询它。
// Execute code on a specific actor - in this case, the @MainActor func mainThreadFunction() async throws { // Create an object off the main actor func createObject(in actor: isolated BackgroundActor) async throws -> ObjectId { let realm = try await Realm(actor: actor) let newTodo = try await realm.asyncWrite { return realm.create(Todo.self, value: [ "name": "Pledge fealty and service to Gondor", "owner": "Pippin", "status": "In Progress" ]) } // Share the todo's primary key so we can easily query for it on another actor return newTodo._id } // Initialize an actor where you want to perform background work let actor = BackgroundActor() let newTodoId = try await createObject(in: actor) let realm = try await Realm() let todoOnMainActor = realm.object(ofType: Todo.self, forPrimaryKey: newTodoId) }
创建对象的表示形式
如果您需要使用多个简单值,但不希望产生传递 ThreadSafeReferences
或查询不同 Actor 上的对象的开销,则可以创建数据的结构体或其他 Sendable 表示以跨 Actor 边界传递。
例如,您的 Actor 可能有一个函数来创建 Realm 对象的结构表示形式。
struct TodoStruct { var id: ObjectId var name, owner, status: String } func getTodoAsStruct(forTodoNamed name: String) -> TodoStruct { let todo = realm.objects(Todo.self).where { $0.name == name }.first! return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status) }
然后,您可以调用一个函数,将数据作为另一个 Actor 的结构体来获取。
func mainThreadFunction() async throws { // Create an object in an actor-isolated realm. let actor = try await RealmActor() try await actor.createTodo(name: "Leave the ring on the mantle", owner: "Bilbo", status: "In Progress") // Get information as a struct or other Sendable type. let todoAsStruct = await actor.getTodoAsStruct(forTodoNamed: "Leave the ring on the mantle") }
观察不同的 Actor 的通知
您可以使用 Swift async/await 语法观察使用 Actor 隔离的 Realm 上的通知。
调用 await object.observe(on: Actor)
或 await collection.observe(on: Actor)
会注册一个块,以便在每次对象或集合发生变更时进行调用。
SDK 在给定 Actor 的执行器上异步调用该块。
对于在不同线程或不同进程中执行的写事务,当 Realm (自动)刷新到包含更改的版本时,SDK 会调用该块。对于本地写入,SDK 在写事务提交后的某个时间点调用该块。
与其他 Realm 通知一样,您只能观察由 Realm 管理的对象或集合。只要您想要监视更新,就必须保留返回的口令。
如果您需要手动推进主线程或另一个 Actor 上观察到的 Realm 的状态,则调用 await realm.asyncRefresh()
。这会更新 Realm 管理的 Realm 对象和未完成的对象,使其指向最新的数据并提供所有适用的通知。
观察限制
无法调用 .observe()
方法:
在写事务中
当包含的 Realm 为只读状态时
从 Actor 外部进入 Actor 限制的 Realm
注册集合变更监听器
SDK 在每次写事务后调用收集通知块,其中包括:
从集合中删除对象。
将对象插入至集合中。
修改集合中对象的任何托管属性。这包括将属性设置为其现有值的自赋值。
重要
顺序很重要
在集合通知处理程序中,始终按以下顺序应用变更:删除、插入和修改。在删除操作之前处理插入操作可能会导致意外行为。
这些通知提供有关发生更改的执行者的信息。 与非执行者隔离的集合通知一样,它们也提供了一个change
参数,用于报告在写事务(write transaction)期间删除、添加或修改了哪些对象。 此RealmCollectionChange解析为一个索引路径大量,您可以将其传递给UITableView
的批处理更新方法。
// Create a simple actor actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReference<Todo>) throws { let realm = try! Realm() try realm.write { // Resolve the thread safe reference on the Actor where you want to use it. // Then, do something with the object. let todoOnActor = realm.resolve(tsr) realm.delete(todoOnActor!) } } } // Execute some code on a different actor - in this case, the MainActor func mainThreadFunction() async throws { let backgroundActor = BackgroundActor() let realm = try! await Realm() // Create a todo item so there is something to observe try await realm.asyncWrite { realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": "Arrive safely in Bree", "owner": "Merry", "status": "In Progress" ]) } // Get the collection of todos on the current actor let todoCollection = realm.objects(Todo.self) // Register a notification token, providing the actor where you want to observe changes. // This is only required if you want to observe on a different actor. let token = await todoCollection.observe(on: backgroundActor, { actor, changes in print("A change occurred on actor: \(actor)") switch changes { case .initial: print("The initial value of the changed object was: \(changes)") case .update(_, let deletions, let insertions, let modifications): if !deletions.isEmpty { print("An object was deleted: \(changes)") } else if !insertions.isEmpty { print("An object was inserted: \(changes)") } else if !modifications.isEmpty { print("An object was modified: \(changes)") } case .error(let error): print("An error occurred: \(error.localizedDescription)") } }) // Update an object to trigger the notification. // This example triggers a notification that the object is deleted. // We can pass a thread-safe reference to an object to update it on a different actor. let todo = todoCollection.where { $0.name == "Arrive safely in Bree" }.first! let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) // Invalidate the token when done observing token.invalidate() }
注册对象变更侦听器
在每次写事务之后,SDK 都会调用对象通知块:
删除对象。
修改对象的任何托管属性。这包括将属性设置为其现有值的自赋值。
该块会传递与所请求的 Actor 隔离的对象的副本,以及有关更改内容的信息。此对象可以安全地用于该 Actor。
默认情况下,只有对象属性的直接更改才会产生通知。对链接对象的更改不会生成通知。如果传入非零、非空的键路径数组,则只有对这些键路径标识的属性的更改才会产生更改通知。键路径可遍历链接属性,以接收有关链接对象更改的信息。
// Execute some code on a specific actor - in this case, the MainActor func mainThreadFunction() async throws { // Initialize an instance of another actor // where you want to do background work let backgroundActor = BackgroundActor() // Create a todo item so there is something to observe let realm = try! await Realm() let scourTheShire = try await realm.asyncWrite { return realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": "Scour the Shire", "owner": "Merry", "status": "In Progress" ]) } // Register a notification token, providing the actor let token = await scourTheShire.observe(on: backgroundActor, { actor, change in print("A change occurred on actor: \(actor)") switch change { case .change(let object, let properties): for property in properties { print("Property '\(property.name)' of object \(object) changed to '\(property.newValue!)'") } case .error(let error): print("An error occurred: \(error)") case .deleted: print("The object was deleted.") } }) // Update the object to trigger the notification. // This triggers a notification that the object's `status` property has been changed. try await realm.asyncWrite { scourTheShire.status = "Complete" } // Invalidate the token when done observing token.invalidate() }