Realm 多线程通信

公司的 App IM 模块要求用 Realm 做数据库,不可避免的在接收消息和发送消息时,读写数据要放在后台线程去做。这样不会影响聊天页面的 UI 显示。一开始涉及到多线程的时候 Realm 挺多坑的。

Realm 多线程特性

Realm 是基于零拷贝架构,所有对象是鲜活的而且自动更新。如果 Realm 允许对象可在线程间共享,Realm 会无法确保数据的一致性,因为不同的线程会在不确定的什么时间点同时改变对象的数据。这样数据很快就不一致了。一个线程可能需要写入一个数据而另一个线程也打算读取它,反过来也可能。这很快就会变得有问题了,而且你不能够在相信哪个线程能有正确的数据了。

Realm 通过确保每个线程始终拥有 Realm 的一个快照,以便让并发运行变得十分轻松。你可以同时有任意数目的线程访问同一个 Realm 文件,并且由于每个线程都有对应的快照,因此线程之间绝不会产生影响。

您唯一需要注意的一件事情就是不能让多个线程都持有同一个 Realm 对象的实例。如果多个线程需要访问同一个对象,那么它们分别会获取自己所需要的实例(否则在一个线程上发生的更改就会造成其他线程得到不完整或者不一致的数据)。

RLMObject 的未管理实例(unmanaged) 表现的和正常的 NSObject 子类相同,可以安全地跨线程传递。

RLMRealm、RLMObject、RLMResults 或者 RLMArray 受管理实例皆受到线程的限制,这意味着它们只能够在被创建的线程上使用,否则就会抛出异常*。这是 Realm 强制事务版本隔离的一种方法。否则,在不同事务版本中的线程间,通过潜在泛关系图 (potentially extensive relationship graph) 来确定何时传递对象将不可能实现。

在多线程中使用 Realm

在同一个线程中访问 Realm 实例

上文说到 RLMRealm、RLMObject、RLMResults 或者 RLMArray 受管理实例皆受到线程的限制。如果在同一个线程中访问这些,那就不需要担心线程带来的数据错乱等问题。在实际操作中,可以开启一个常驻线程来操作数据库。

在不同的线程中重新获取 Realm 实例

无论是全局的RLMRealm实例,还是RLMObject,在不同的线程访问时都需要重新获取一个实例(每个线程都有一个 Realm 的快照),否则会报错。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在线程 1 中写入一个 dog
thread1 {
Dogs *dog = [[Dogs alloc] init];
dog.name = @"Chubby";

RLMRealm *r = [RLMRealm defaultRealm];
[r transactionWithBlock:^{
[Dogs createOr....:dog];
}];

thread2 {
RLMResults *dogs = [Dogs allObjects];
// 模拟取出刚才写入的 dog
Dog *dog = dogs.firstObject;
[[RLMRealm defaultRealm] transactionWithBlock:^{
// 修改 dog 的名字
dog.name = @"Chub"
}];
}
}

在线程 1 中,dog 已经被 managed,同时与当前线程绑定,如果此时想在另一个线程,必须重新在另一个线程中获取 RLMObject 的快照。

通过 RLMThreadSafeReference 来传递实例

  1. 通过受到线程限制的对象来构造一个 RLMThreadSafeReference
  2. 将此 RLMThreadSafeReference 传递给目标线程或者队列;
  3. 通过在目标 Realm 上调用 -[RLMRealm resolveThreadSafeReference:] 来解析此引用。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
thread1 {
Dog *dog = [Dog new];
dog.name = @"Chubby";
[[RLMRealm defaultRealm] transactionWithBlock:^{
[[RLMRealm defaultRealm] add:dog];
}];

// 这时如果要在线程中传递这只 dog
// 1. 构造 RLMThreadSafeReference 实例
RLMThreadSafeReference *dogRef = [RLMThreadSafeReference
referenceWithThreadConfined:dog];

// 2. 将此 RLMThreadSafeReference 传递给目标线程或者队列
thread2 {
@autoreleasepool {
RLMRealm *r = [RLMRealm defaultRealm];

// 3. 通过 resolve 方法来获取 thread 1 中的 dog
Dog *dog = [r resolveThreadSafeReference:dogRef];

[r transactionWithBlock:^ {
dog.name = @"Chub";
}];
}
}
}

RLMThreadSafeReference 对象最多只能够解析一次。如果 RLMThreadSafeReference 解析失败的话,将会导致 Realm 的原始版本被锁死,直到引用被释放为止。因此,RLMThreadSafeReference 的生命周期应该很短。