You are reading the documentation for an older version of Realm. You can view the latest documentation instead.

如果您的应用中只打算使用纯 Swift 进行开发,那么您应当考虑使用 Realm 的 Swfit 版本。 注意:不能同时使用 Objective‑C 版本和 Swift 版本的 Realm,两者是不可互操作的。

Objective‑C版本的 Realm 能够让您以一种安全、耐用以及迅捷的方式来高效地编写应用的数据模型层,如下例所示:

// 定义模型的做法和定义常规 Objective‑C 类的做法类似
@interface Dog : RLMObject
@property NSString *name;
@property NSData   *picture;
@property NSInteger age;
@end
@implementation Dog
@end
RLM_ARRAY_TYPE(Dog)
@interface Person : RLMObject
@property NSString             *name;
@property RLMArray<Dog *><Dog> *dogs;
@end
@implementation Person
@end

// 使用的方法和常规 Objective‑C 对象的使用方法类似
Dog *mydog = [[Dog alloc] init];
mydog.name = @"大黄";
mydog.age = 1;
mydog.picture = nil; // 属性的值可以为空
NSLog(@"狗狗的名字: %@", mydog.name);

// 检索 Realm 数据库,找到小于 2 岁 的所有狗狗
RLMResults<Dog *> *puppies = [Dog objectsWhere:@"age < 2"];
puppies.count; // => 0 因为目前还没有任何狗狗被添加到了 Realm 数据库中

// 数据持久化操作十分简单
RLMRealm *realm = [RLMRealm defaultRealm];
[realm transactionWithBlock:^{
  [realm addObject:mydog];
}];

// 检索结果会实时更新
puppies.count; // => 1

// 可以在任何一个线程中执行检索操作
dispatch_async(dispatch_queue_create("background", 0), ^{
  @autoreleasepool {
    Dog *theDog = [[Dog objectsWhere:@"age == 1"] firstObject];
    RLMRealm *realm = [RLMRealm defaultRealm];
    [realm beginWriteTransaction];
    theDog.age = 3;
    [realm commitWriteTransaction];
  }
});

如果您的应用正在使用 Core Data 并打算换用 Realm 的话,我们最近发布了一篇关于如何执行转换的文章,点击此处查看!

从这里开始

下载 Realm Objective‑C 或者在GitHub上查看源码!

准备工作

  • 使用 Realm 构建应用的基本要求:iOS 7 及其以上版本, macOS 10.9 及其以上版本,此外 Realm 支持 tvOS 和 watchOS 的所有版本。
  • 需要使用 Xcode 8.0 或者以后的版本。Realm Objective‑C 2.3.0 是最后一个支持 Swift 2.x 及 Xcode 7.3 的版本。

安装

注意:动态框架与 iOS 7 不兼容,要支持 iOS 7 的话请查看“静态框架”。

  1. 下载最新的Realm发行版本,并解压;
  2. 前往Xcode 工程的”General”设置项中,从ios/dynamic/osx/tvos/或者watchos/中将’Realm.framework’拖曳到”Embedded Binaries”选项中。确认Copy items if needed被选中(除非您的项目中需要在多个平台中使用 Realm),点击Finish按钮;
  3. 在单元测试 Target 的”Build Settings”中,在”Framework Search Paths”中添加Realm.framework的上级目录;
  4. 如果希望使用 Swift 加载 Realm,请拖动Swift/RLMSupport.swift文件到 Xcode 工程的文件导航栏中并选中Copy items if needed
  5. 如果在 iOS、watchOS 或者 tvOS 项目中使用 Realm,请在您应用目标的”Build Phases”中,创建一个新的”Run Script Phase”,并将

    bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework/strip-frameworks.sh"

    这条脚本复制到文本框中。 因为要绕过APP商店提交的bug,这一步在打包通用设备的二进制发布版本时是必须的。

  1. 安装CocoaPods 0.39.0 或者更高版本
  2. 运行 pod repo update,以确保 CocoaPods 能够获取到 Realm 的最新版本
  3. 在您的Podfile中,添加pod 'Realm'到您的 app 目标中,添加pod 'Realm/Headers'到您的测试目标中;
  4. 在终端运行pod install
  5. 采用 CocoaPods 生成的.xcworkspace来运行工程!
  6. 如果需要在 Swift 当中使用的话,将位于 Swift/RLMSupport.swift 的这个文件拖动到您 Xcode 项目的文件导航器当中,检查以确保 Copy items if needed 选项已被勾选。
  1. 安装 Carthage 0.17.0 或者更高版本;
  2. 在Carthage 中添加github "realm/realm-cocoa"
  3. 运行 carthage update
  4. Carthage/Build/ 目录下对应平台文件夹中,将 Realm.framework 拖曳到您 Xcode 工程”General”设置项的”Linked Frameworks and Libraries”选项卡中;
  5. iOS/tvOS/watchOS: 在您应用目标的“Build Phases”设置选项卡中,点击“+”按钮并选择“New Run Script Phase”。在新建的Run Script中,填写:

/usr/local/bin/carthage copy-frameworks

在“Input Files”内添加您想要使用的框架路径,例如:

$(SRCROOT)/Carthage/Build/iOS/Realm.framework

因为要绕过APP商店提交的bug,这一步在打包通用设备的二进制发布版本时是必须的。 16. 如果需要在 Swift 当中使用的话,将位于 Swift/RLMSupport.swift 的这个文件拖动到您 Xcode 项目的文件导航器当中,检查以确保 Copy items if needed 选项已被勾选。

  1. 下载 Realm 的最新版本并解压;
  2. Realm.frameworkios/static/ 文件夹拖曳到您 Xcode 项目中的文件导航器当中。确保 Copy items if needed 选中然后单击 Finish
  3. 在 Xcode 文件导航器中选择您的项目,然后选择您的应用目标,进入到** Build Phases** 选项卡中。在 Link Binary with Libraries 中单击 + 号然后添加 libc++.tbd 以及 libz.tbd
  4. 如果你在用 Swift 来使用 Realm,那么将位于 Swift/RLMSupport.swift 的文件拖曳进您 Xcode 项目中的文件导航器当中,确保 Copy items if needed 选中。

导入 Realm 框架

在您的 Objective-C 源文件的顶部,使用 #import <Realm/Realm.h> 来导入 Objective-C 版本的 Realm,以便让其能够在代码中使用。在您的 Swift 源文件的顶部(如果有的话),使用 import Realm。这就是开始使用 Realm 之前您所需要做的步骤!

在 Swift 当中使用 Objective‑C 版本的 Realm

如果您的项目上完全基于 Swift 构建的话,那么您应当考虑使用 Swift 版本的 Realm

Objective‑C 版本的 Realm 的设计目标包含了能够在 Objective‑C 和 Swift 混编的项目中使用。在 Swift 中使用此 API 的话,您可以实现所有与在 Objective‑C 中使用 Realm 所能够完成的功能,例如定义模型以及使用 Realm 的 Objective‑C API。 然而,与纯 Objective‑C 项目相比,您仍然需要对某些部分进行一些小小的改变:

RLMSupport.swift

我们建议您将 Swift/RLMSupport.swift 文件一同编译进去(这个文件您同样能够在我们的发行版本压缩包中找到)。

这个文件为 Objective-C 版本的 Realm 集合类型中引入了 Sequence 一致性,并且重新暴露了一些不能够从 Swift 中进行原生访问的 Objective-C 方法,例如可变参数 (variadic arguments)。

Objective‑C 版本的 Realm 默认情况下不包含这个文件,因为这会强制让所有使用 Objective‑C 版本 Realm 的用户去包含额外的 Swift 动态库,不管他们有没有在项目中使用 Swift!

RLMArray 属性

在 Objective‑C 中,我们依赖协议一致性,从而让 Realm 能够明白 RLMArray 对多关系 中包含的对象类型。在 Swift 中,这种类型的语法是不可能实现的。因此,您应当用下列语法形式来声明您的 RLMArray 属性:

class Person: RLMObject {
  dynamic var dogs = RLMArray(objectClassName: Dog.className())
}

这等同于 Objective‑C 中的声明:

@interface Person : RLMObject
@property RLMArray<Dog *><Dog> *dogs;
@end

tvOS

由于在 tvOS 中向 Doucments 目录写入数据是被禁止的,因此默认的 Realm 路径位置将被设置为 NSCachesDirectory 。然而,要注意的是 tvOS 会随时清除 Caches 目录下的文件,因此我们建议您将 Realm 数据库作为一个新的缓存机制使用,而不是用其来存储重要的用户数据。

如果您想要在 tvOS 应用和 TV 服务扩展(例如 Top Shelf 扩展)之间共享 Realm 文件的话,您必须要使用应用程序组共享容器当中的 Library/Caches/ 目录。

RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration];
configuration.fileURL = [[[NSFileManager defaultManager]
    containerURLForSecurityApplicationGroupIdentifier:@"group.io.realm.examples.extension"]
    URLByAppendingPathComponent:@"Library/Caches/default.realm"];

您同样可以在您应用中加入预构建的 Realm 文件。不过,一定要确保遵循 App Store 应用上架指南,保证应用大小在 200 MB 以内。

您可以浏览我们的 tvOS 示例项目 ,以此来展示简单 tvOS 应用是如何使用 Realm 进行离线缓存以及预加载数据的。

Realm浏览器/数据库管理器

我们还提供了一个名为 Realm Browser 的独立的Mac应用以便 对.realm数据库进行读取和编辑。

Realm Browser

您可以使用菜单中的Tools(工具) > Generate demo database(生成演示数据库)来生成一个有样本数据的测试数据库。

如果您需要寻找您应用的Realm文件,请查看StackOverflow上的这个答案来获取详细信息。

您可以从Mac App Store安装Realm Browser。

Xcode 插件

我们的Xcode插件令 Realm 模型的创建更加方便。

安装 Realm 插件的最简单方式是通过点击”RealmPlugin”文件夹下的Alcatraz。您也可以手动进行安装:打开release zip 中的plugin/RealmPlugin.xcodeproj并进行编译,重启 Xcode之后插件即可生效。如果您使用 Xcode 菜单来建立一个新文件(File > New > File… — or ⌘N) ,您就可以看到有一个新建Realm模型的选项。

API手册

您能查询我们的 完整版API手册 ,里面包含了所有类和方法等信息。

示例

您可以在release zip中的examples/目录下查看 iOS 和 OS X 版本的示例程序。它们演示了Realm的很多功能和特性,例如数据库迁移(migration)、如何与UITableViewController’s一起使用、加密(encryption)、命令行工具等等。

获得帮助

  • 编码过程中遇到了问题? 在 StackOverflow 上提问,我们会经常在上面查看以及回答问题!
  • 发现了 BUG? 可以直接在GitHub repo提交给我们。如果可以的话,请给我们提供您所使用的 Realm 版本号、完整的日志记录、Realm 文件以及您的当前项目,以便我们能够重现您所发现的问题。
  • 希望我们新增功能? 可以直接在GitHub repo提交给我们。请告诉我们需要实现何种功能和特性,以及新增这些功能的理由。

如果您在使用崩溃检测 SDK (诸如 Crashlytics 或者 HockeyApp) 的话,请确保开启了日志收集功能。Realm 会在抛出异常以及发生致命错误的时候会记录下元数据信息(不是用户数据),这些信息可以在出现问题的时候有效地帮助您进行解决。

数据模型(Model)

Realm数据模型是基于标准 Objective‑C 类来进行定义的,使用属性来完成模型的具体定义。

通过简单的继承 RLMObject 或者一个已经存在的模型类,您就可以创建一个新的 Realm 数据模型对象。

Realm模型对象在形式上基本上与其他 Objective‑C 对象相同 - 您可以给它们添加您自己的方法(method)和协议(protocol),和在其他对象中使用类似。

主要的限制是某个对象只能在其被创建的那个线程中使用, 并且您无法访问任何存储属性的实例变量(ivar)。

如果您安装了我们的Xcode插件 ,那么可在”New File…“对话框中会有一个很漂亮的模板,可用来创建接口(interface)和执行(implementation)文件。

您只需要为对象的类型列表添加目标类型的属性,或者RLMArray,就可以创建数据关系(relationship)和嵌套数据结构(nested data structure)。

#import <Realm/Realm.h>

@class Person;

// 狗狗的数据模型
@interface Dog : RLMObject
@property NSString *name;
@property Person   *owner;
@end
RLM_ARRAY_TYPE(Dog) // 定义RLMArray<Dog>

// 狗狗主人的数据模型
@interface Person : RLMObject
@property NSString      *name;
@property NSDate        *birthdate;
@property RLMArray<Dog> *dogs;
@end
RLM_ARRAY_TYPE(Person) // 定义RLMArray<Person>

// 实现文件
@implementation Dog
@end // 暂时没用

@implementation Person
@end // 暂时没用

由于 Realm 中定义的所有模型在程序启动时就会被解析,所以即使代码中没有调用,它们都需要被初始化。

在 Swift 中使用 Realm 的时候,Swift.reflect(_:) 函数可用于确定您模型中的信息,这需要确保 init() 已被成功调用。这意味着所有非可选的属性必须添加一个默认值。

通过RLMObject 可查看更多细节。

支持的类型

Realm支持以下的属性类型:BOOLboolintNSIntegerlonglong longfloatdoubleNSStringNSDateNSData 以及 被特殊类型标记的 NSNumber

CGFloat 属性的支持被取消了,因为它不具备平台独立性。

您可以使用RLMArray<Object *><Object>RLMObject的子类来建立诸如一对多、一对一之类的关系模型。

在 Xcode 7 以及之后的版本中,RLMArray支持编译时的 Objective‑C 泛型(generics)。下面是不同属性定义方法的意义以及用途:

  • RLMArray: 属性类型。
  • <Object *>: 属性的特别化(generic specialization),这可以阻止在编译时使用错误对象类型的数组。
  • <Object>: 此RLMArray遵守的协议,可以让 Realm 知晓如何在运行时确定数据模型的架构。

关系(Relationships)

RLMObject 能够借助 RLMObject 以及 RLMArray属性来和另一个 RLMObject 建立联系。 RLMArray 的接口和 NSArray 非常类似,在 RLMArray 中的对象能够通过索引下标(indexed subscripting)进行访问。 与 NSArray 所不同的是,RLMArray 的类型是固定的,其中只能存放简单的 RLMObject 子类类型。 要了解更详细的信息,请参阅 RLMArray

假设现在您已经定义好了 Person 数据模型(见上文),让我们创建另一个名为 Dog 的数据模型:

// Dog.h
@interface Dog : RLMObject
@property NSString *name;
@end

对一(To-One)关系

对于多对一(many-to-one)或者一对一(one-to-one)关系来说,只需要声明一个 RLMObject 子类类型的属性即可:

// Dog.h
@interface Dog : RLMObject
// 其余属性声明...
@property Person *owner;
@end

您可以非常简单的通过这个属性完成关系的绑定:

Person *jim = [[Person alloc] init];
Dog    *rex = [[Dog alloc] init];
rex.owner = jim;

当使用 RLMObject 属性的时候,您可以通过正常的属性访问语法来访问嵌套属性。比如说,rex.owner.address.country会依次读取对象的属性,然后自动从 Relam 中匹配所需的每一个对象。

对多(To-Many)关系

通过 RLMArray 类型的属性您可以定义一个对多关系。RLMArray中可以包含简单类型的RLMObject,其接口与NSMutableArray非常类似。

RLMArray 可能会包含多个相同 Realm 对象的引用,即便对象带有主键也是如此。例如,您或许会创建一个空的 RLMArray,然后连续三次向其中插入同一个对象;当使用 0、1、2 的索引来访问元素的时候,RLMArray 将会返回对应的对象,而所返回的这三个对象都是同一个对象。

如果要给我们的 Person 数据模型添加一个“dogs”属性,以便能够和多个“dogs”建立关系,也就是表明一个「人」可以养多条「狗」,那么我们首先需要定义一个 RLMArray<Dog> 类型。通过对应数据模型接口文件下的宏命令即可完成:

//Dog.h
@interface Dog : RLMObject
// 属性声明...
@end

RLM_ARRAY_TYPE(Dog) // 定义一个 RLMArray<Dog> 类型

RLM_ARRAY_TYPE 宏创建了一个协议,从而允许 RLMArray<Dog> 语法的使用。如果该宏没有放置在模型接口的底部的话,您或许需要提前声明该模型类。

接下来您就能定义RLMArray<Dog>类型的属性了:

```objc
// Person.h
@interface Person : RLMObject
// 其余的属性声明...
@property RLMArray<Dog *><Dog> *dogs;
@end

您可以和之前一样,对 RLMArray 属性进行访问和赋值:

// jim 是 rex 以及所有名字叫“Fido”的狗狗的主人
RLMResults<Dog *> *someDogs = [Dog objectsWhere:@"name contains 'Fido'"];
[jim.dogs addObjects:someDogs];
[jim.dogs addObject:rex];

注意,虽然可以给 RLMArray 属性赋值为 nil,但是这仅用于“清空”数组,而不是用以移除数组。这意味着您总是可以向一个 RLMArray 属性中添加对象,即使其被置为了 nil

RLMArray 属性将确保其当中的插入次序不会被扰乱。

反向关系(Inverse Relationship)

链接是单向性的。因此,如果对多关系属性 Person.dogs 链接了一个 Dog 实例,而这个实例的对一关系属性 Dog.owner 又链接到了对应的这个 Person 实例,那么实际上这些链接仍然是互相独立的。为 Person 实例的 dogs 属性添加一个新的 Dog 实例,并不会将这个 Dog 实例的 owner 属性自动设置为该 Person。但是由于手动同步双向关系会很容易出错,并且这个操作还非常得复杂、冗余,因此 Realm 提供了“链接对象 (linking objects)” 属性来表示这些反向关系。

借助链接对象属性,您可以通过指定的属性来获取所有链接到指定对象的对象。例如,一个 Dog 对象可以拥有一个名为 owners 的链接对象属性,这个属性中包含了某些 Person 对象,而这些 Person 对象在其 dogs 属性中包含了这一个确定的 Dog 对象。您可以将 owners 属性设置为 RLMLinkingObjects 类型,然后重写 +[RLMObject linkingObjectsProperties] 来指明关系,说明 ownders 中包含了 Person 模型对象。

@interface Dog : RLMObject
@property NSString *name;
@property NSInteger age;
@property (readonly) RLMLinkingObjects *owners;
@end

@implementation Dog
+ (NSDictionary *)linkingObjectsProperties {
    return @{
        @"owners": [RLMPropertyDescriptor descriptorWithClass:Person.class propertyName:@"dogs"],
    };
}
@end

可空属性(Optional Properties)

通常情况下,NSString *NSData * 以及 NSDate * 属性可以设置为 nil。如果你不需要实现此功能,你可以重写您的 RLMObject 子类的 +requiredProperties 方法。

比如对于以下的模型定义来说,如果尝试给 name 属性设置为 nil 将会抛出一个异常,但是将 birthday 属性设置为 nil 却是允许的:

@interface Person : RLMObject
@property NSString *name;
@property NSDate *birthday;
@end

@implementation Person
+ (NSArray *)requiredProperties {
    return @[@"name"];
}
@end

存储可空数字目前已经可以通过 NSNumber * 属性完成。

由于 Realm 对不同类型的数字采取了不同的存储格式,因此设置可空的数字属性必须是 RLMIntRLMFloatRLMDouble 或者 RLMBool 类型。所有赋给属性的值都会被转换为其特定的类型。

请注意,NSDecimalNumber 的值只能分配给类型为 RLMDouble 的 Realm 属性,此外 Realm 将会存储近似于双精度浮点的数值,而不是存储基本的十进制数值。

比如说,如果我们存储一个用户的年龄(age)而不是存储他们的生日,同时还要允许当您不知道该用户的年龄的时候将 age 属性设置为 nil

@interface Person : RLMObject
@property NSString *name;
@property NSNumber<RLMInt> *age;
@end

@implementation Person
+ (NSArray *)requiredProperties {
    return @[@"name"];
}
@end

RLMProperty 的子类属性始终都可以为 nil,因此这些类型不能够放在 requiredProperties中,并且 RLMArray 不支持存储 nil 值。

简单说明

这个表格提供了关于声明模型属性的简易参考:

类型 非可选值形式 可选值形式
Bool @property BOOL value; @property NSNumber<RLMBool> *value;
Int @property int value; @property NSNumber<RLMInt> *value;
Float @property float value; @property NSNumber<RLMFloat> *value;
Double @property double value; @property NSNumber<RLMDouble> *value;
String @property NSString *value; 1 @property NSString *value;
Data @property NSData *value; 1 @property NSData *value;
Date @property NSDate *value; 1 @property NSDate *value;
Object n/a: 必须是可选值 @property Object *value;
List @property RLMArray<Object *><Object> *value; n/a: 必须是非可选值
LinkingObjects @property (readonly) RLMLinkingObjects<Object *> *value; 2 n/a: 必须是非可选值

1) Objective‑C 引用类型的必需属性必须要声明在联合体当中:

@implementation MyModel
+ (NSArray *)requiredProperties {
    return @[@"value"];
}
@end

2) 链接对象属性必须连带 +linkingObjectsProperties 方法一同声明:

@implementation MyModel
+ (NSDictionary *)linkingObjectsProperties {
    return @{ @"property": [RLMPropertyDescriptor descriptorWithClass:Class.class propertyName:@"link"] };
}
@end

属性特性(attributes)

注意由于 Realm 在自己的引擎内部有很好的语义解释系统,所以 Objective‑C 的许多属性特性将被忽略,如nonatomic, atomic, strong, copyweak 等。 因此为了避免误解,我们推荐您在编写数据模型的时候不要使用任何的属性特性。 当然,如果您已经设置了这些属性特性,那么在 RLMObject 对象被写入 Realm 数据库前,这些特性会一直生效。 无论 RLMObject 对象是否受到 Realm 管理,您为其编写的自定义 getter 和 setter 方法都能正常工作。

如果您在 Swift 中使用 Objective-C 版本的 Realm 的话,模型的属性前面需要加上 dynamic var,这是为了让这些属性能够被底层数据库数据所访问。

索引属性(Indexed Properties)

重写 +indexedProperties 方法可以为数据模型中需要添加索引的属性建立索引:

@interface Book : RLMObject
@property float price;
@property NSString *title;
@end

@implementation Book
+ (NSArray *)indexedProperties {
  return @[@"title"];
}
@end

Realm 支持字符串、整数、布尔值以及 NSDate 属性作为索引。

对属性进行索引可以减少插入操作的性能耗费,加快比较检索的速度(比如说 = 以及 IN 操作符)。

属性默认值

重写+defaultPropertyValues 可以每次在对象创建之后为其提供默认值。

@interface Book : RLMObject
@property float price;
@property NSString *title;
@end

@implementation Book
+ (NSDictionary *)defaultPropertyValues {
    return @{@"price" : @0, @"title": @""};
}
@end

对象的自更新特性

RLMObject 实例是底层数据的动态表现,其会进行自动更新,这意味着对象不需要进行刷新。修改某个对象的属性会立刻影响到其他所有指向同一个对象的实例。

Dog *myDog = [[Dog alloc] init];
myDog.name = @"小白";
myDog.age = 1;

[realm transactionWithBlock:^{
  [realm addObject:myDog];
}];

Dog *myPuppy = [[Dog objectsWhere:@"age == 1"] firstObject];
[realm transactionWithBlock:^{
  myPuppy.age = 2;
}];

myDog.age; // => 2

RLMObject 的这个特性不仅让 Realm 保证速度和效率,它同时还让代码更加简洁、更为灵活。比如说,如果您的 UI 代码是基于某个特定的 Realm 对象来现实的,那么在触发 UI 重绘之前,您不用担心数据的刷新或者重新检索等问题。

您也可以查看 Realm 通知 一节以确认 Realm 数据何时被更新,比如说由此来决定应用 UI 何时需要被更新。此外,还可以使用 键值编码,当某个 RLMObject 的特定属性发生更新时去发送通知。

主键(Primary Keys)

重写 +primaryKey 可以设置模型的主键。声明主键之后,对象将允许进行查询,并且更新速度更加高效,而这也会要求每个对象保持唯一性。 一旦带有主键的对象被添加到 Realm 之后,该对象的主键将不可修改。

@interface Person : RLMObject
@property NSInteger id;
@property NSString *name;
@end

@implementation Person
+ (NSString *)primaryKey {
    return @"id";
}
@end

忽略属性(Ignored Properties)

重写 +ignoredProperties 可以防止 Realm 存储数据模型的某个属性。Realm 将不会干涉这些属性的常规操作,它们将由成员变量(ivar)提供支持,并且您能够轻易重写它们的 setter 和 getter。

@interface Person : RLMObject
@property NSInteger tmpID;
@property (readonly) NSString *name; // 只读属性将被自动忽略
@property NSString *firstName;
@property NSString *lastName;
@end

@implementation Person
+ (NSArray *)ignoredProperties {
    return @[@"tmpID"];
}
- (NSString *)name {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
@end

忽略属性的行为与 Objective-C 或者 Swift 类当中的普通对象_相似_。它们并不支持任何一种 Realm 特定的功能。例如,无法通过查询来检索忽略属性,也无法实现自动更新,即便另一个相同对象的实例的忽略属性值发生了变更。此外忽略属性发生更改的时候也不会触发通知,尽管仍然可以使用 KVO 来实现简直观察。

模型继承

Realm 允许模型能够生成更多的子类,也允许跨模型进行代码复用,但是由于某些 Cocoa 特性使得运行时中丰富的类多态无法使用。以下是可以完成的操作:

  • 父类中的类方法,实例方法和属性可以被它的子类所继承
  • 子类中可以在方法以及函数中使用父类作为参数

以下是不能完成的:

  • 多态类之间的转换(例如子类转换成子类,子类转换成父类,父类转换成子类等)
  • 同时对多个类进行检索
  • 多类容器 (RLMArray 以及 RLMResults)。

向 Realm 中增加此特性已经在规划当中,并且我们暂时提供了一些代码示例,以便能够对更常见的模式进行处理。

另外,如果您的代码实现允许的话,我们建议您使用以下模式,也就是使用类组合模式来构建子类,以便能够包含其他类中的相关逻辑:

// 基础模型
@interface Animal : RLMObject
@property NSInteger age;
@end
@implementation Animal
@end

// 包含有 Animal 的模型
@interface Duck : RLMObject
@property Animal *animal;
@property NSString *name;
@end
@implementation Duck
@end

@interface Frog : RLMObject
@property Animal *animal;
@property NSDate *dateProp;
@end
@implementation Frog
@end

// 用法
Duck *duck =  [[Duck alloc] initWithValue:@{@"animal" : @{@"age" : @(3)}, @"name" : @"Gustav" }];

集合

Realm 拥有一系列能够帮助表示一组对象的类型,我们称之为「Realm 集合」:

  1. RLMResults类,表示从检索 中所返回的对象集合。
  2. RLMArray类,表示模型中的对多关系
  3. RLMLinkingObjects类,表示模型中的反向关系
  4. RLMCollection 协议,定义了所有 Realm 集合所需要遵守的常用接口。

Realm 集合实现了 RLMCollection 协议,这确保它们能够保持一致。这个协议继承自 NSFastEnumeration,因此它应当与其他 Foundation 当中的集合用法一致。 其他常用的 Realm 集合 API 也在这个协议当中进行了声明,例如其中包括检索、排序以及聚合操作。 RLMArray拥有额外的修改操作,这些操作不在协议接口当中有定义,例如添加和删除对象。

使用 RLMCollection 协议,您可以编写能够操作任意 Realm 集合的泛型代码:

@implementation MyObject
- (void)operateOnCollection:(id<RLMCollection>)collection {
  // collection 既可以是 RLMResults,也可以是 RLMArray
  NSLog(@"对集合 %@s 进行操作", collection.objectClassName);
}
@end

对象存储

对对象的所有更改(添加,修改和删除)都必须通过写入事务(transaction)完成。

Realm 的对象可以被实例化并且作为unmanaged对象使用(也就是还未添加到 Realm 数据库中的对象),和其他常规Objective‑C对象无异。

但是如果您想要在多个线程中共享对象,或者在应用重启后重复使用对象,那么您必须将其添加到 Realm 数据库中。向 Realm 数据库中添加对象的操作必须在写入事务中完成。

这是因为写入事务将会产生不可忽略的性能消耗,因此你应当检视你的代码以确保写入事务的次数尽可能少。

Realm 写入操作是_同步以及阻塞的_,并不会异步运行。如果线程 A 开始了一个写入操作,接着在线程 A 结束之前,线程 B 又对同一个 Realm 数据库开始了写入操作,那么线程 A 必须在线程 B 的写入操作进行之前,完成自己的写入操作,并将事务提交。写入操作总会在 beginWrite() 操作进行的时候自动刷新,因此重复写入 (overlapping write) 并不会产生竞争条件。

由于写入事务像其余硬盘读写操作一样,会出现失败的情况,因此 -[RLMRealm transactionWithBlock:] 以及 -[RLMRealm commitWriteTransaction] 可以选择加上 NSError 指针参数 因此你可以处理和恢复诸如硬盘空间溢出之类的错误。此外,其他的错误都无法进行恢复。简单起见,我们的代码示例并不会处理这些错误,但是您应当在您应用当中注意到这些问题。

创建对象

当定义完数据模型之后,您可以将您的 RLMObject 子类实例化,然后向 Realm 中添加新的实例。我们以下面这个简单的模型为例:

// 狗狗的数据模型
@interface Dog : RLMObject
@property NSString *name;
@property NSInteger age;
@end

// 实现文件
@implementation Dog
@end

我们可以用多种方法创建一个新的对象:

// (1) 创建一个狗狗对象,然后设置其属性
Dog *myDog = [[Dog alloc] init];
myDog.name = @"大黄";
myDog.age = 10;

// (2) 通过字典创建狗狗对象
Dog *myOtherDog = [[Dog alloc] initWithValue:@{@"name" : @"豆豆", @"age" : @3}];

// (3) 通过数组创建狗狗对象
Dog *myThirdDog = [[Dog alloc] initWithValue:@[@"豆豆", @3]];
  1. 使用指定初始化器(designated initializer)创建对象是最简单的方式。请注意,所有的必需属性都必须在对象添加到 Realm 前被赋值。
  2. 通过使用恰当的键值,对象还可以通过字典完成创建。
  3. 最后,RLMObject 子类还可以通过数组完成实例化,数组中的值必须和数据模型中对应属性的次序相同。

嵌套属性(Nested Object)

如果某个对象中有 RLMObject 或者 RLMArray 类型的属性,那么通过使用嵌套的数组或者字典便可以对这些属性递归地进行设置。您只需要简单的用表示其属性的字典或者数组替换每个对象即可:

// 这里我们就可以使用已存在的狗狗对象来完成初始化
Person *person1 = [[Person alloc] initWithValue:@[@"李四", @30, @[aDog, anotherDog]]];

// 还可以使用多重嵌套
Person *person2 = [[Person alloc] initWithValue:@[@"李四", @30, @[@[@"小黑", @5],
                                                                 @[@"旺财", @6]]]];

即使是数组以及字典的多重嵌套,Realm 也能够轻松完成对象的创建。注意 RLMArray 只能够包含 RLMObject 类型,不能包含诸如NSString之类的基础类型。

添加数据

向 Realm 中添加数据的步骤如下:

// 创建对象
Person *author = [[Person alloc] init];
author.name    = @"大卫·福斯特·华莱士";

// 获取默认的 Realm 实例
RLMRealm *realm = [RLMRealm defaultRealm];
// 每个线程只需要使用一次即可

// 通过事务将数据添加到 Realm 中
[realm beginWriteTransaction];
[realm addObject:author];
[realm commitWriteTransaction];

等您将某个对象添加到 Realm 数据库之后,您可以继续使用它,并且您对其做的任何更改都会被保存(必须在一个写入事务当中完成)。当写入事务提交之后,使用相同 Realm 数据源的其他线程才能够对这个对象进行更改。

请注意,如果在进程中存在多个写入操作的话,那么单个写入操作将会阻塞其余的写入操作,并且还会锁定该操作所在的当前线程。

这个特性与其他持久化解决方案类似,我们建议您使用该方案常规的最佳做法:将写入操作转移到一个独立的线程中执行。

由于 Realm 采用了 MVCC 设计架构,读取操作 并不会 因为写入事务正在进行而受到影响。除非您需要立即使用多个线程来同时执行写入操作,不然您应当采用批量化的写入事务,而不是采用多次少量的写入事务。

查看RLMRealmRLMObject来获得更多内容。

更新数据

Realm 提供了一系列用以更新数据的方式,这些方式都有着各自所适应的情景。请选择最符合您当前需求的方式来使用:

内容直接更新

您可以在写入事务中通过设置某个对象的属性从而完成对象的更新操作。

// 在一个事务中更新对象
[realm beginWriteTransaction];
author.name = @"托马斯·品钦";
[realm commitWriteTransaction];

通过主键更新

如果您的数据模型中设置了主键的话,那么您可以使用+[RLMObject createOrUpdateInRealm:withValue:]来更新对象,或者当对象不存在时插入新的对象。

// 创建一个带有主键的“书籍”对象,作为事先存储的书籍
Book *cheeseBook = [[Book alloc] init];
cheeseBook.title = @"奶酪食谱";
cheeseBook.price = @9000;
cheeseBook.id = @1;

// 通过 id = 1 更新该书籍
[realm beginWriteTransaction];
[Book createOrUpdateInRealm:realm withValue:cheeseBook];
[realm commitWriteTransaction];

如果主键 id 为1的 Book 对象已经存在于数据库当中了,那么对象就会简单地进行更新。而如果不在数据库中存在的话,那么这个操作将会创建一个新的 Book 对象并添加到数据库当中。

您同时通过传递您想要更新值的集合,从而更新带有主键的某个对象的部分值,比如说如下所示:

// 假设带有主键值 `1` 的“书籍”对象已经存在
[realm beginWriteTransaction];
[Book createOrUpdateInRealm:realm withValue:@{@"id": @1, @"price": @9000.0f}];
// 这本书的`title`属性不会被改变
[realm commitWriteTransaction];

如果对象没有定义主键的话,那么您不能够调用出现在本章的这些方法(也就是那些以 OrUpdate 结尾的方法)。

请注意,当对象更新的时候,NSNull 仍然会被认为是可选属性 的有效值。如果您提供了一个属性值为 NSNull 的字典,那么这些都会应用到您的对象当中,并且对应的属性都将为空。为了确保不出现任何意外的数据丢失,请在使用此方法的时候再三确认只提供了您想要进行更新的属性。

键值编码

RLMObjectRLMResult 以及 RLMArray 都遵守键值编码(Key-Value Coding)(KVC)机制。 当您在运行时才能决定哪个属性需要更新的时候,这个方法是最有用的。

将 KVC 应用在集合当中是大量更新对象的极佳方式,这样就可以不用经常遍历集合,为每个项目创建一个访问器了。

RLMResults<Person *> *persons = [Person allObjects];
[[RLMRealm defaultRealm] transactionWithBlock:^{
  [[persons firstObject] setValue:@YES forKeyPath:@"isFirst"];
  // 将每个人的 planet 属性设置为“地球”
  [persons setValue:@"地球" forKeyPath:@"planet"];
}];

删除数据

通过在写入事务中将要删除的对象传递给 -[RLMRealm deleteObject:] 方法,即可完成删除操作。

// 存储在 Realm 中的 cheeseBook 对象

// 在事务中删除一个对象
[realm beginWriteTransaction];
[realm deleteObject:cheeseBook];
[realm commitWriteTransaction];

您也能够删除存储在 Realm 中的所有数据。注意,Realm 文件的大小不会被改变,因为它会保留空间以供日后快速存储数据。

// 从 Realm 中删除所有数据
[realm beginWriteTransaction];
[realm deleteAllObjects];
[realm commitWriteTransaction];

查询

通过查询操作,Realm 将会返回包含 RLMObject 集合的RLMResults实例。RLMResults 的表现和 NSArray 十分相似,并且包含在 RLMResults 中的对象能够通过索引下标进行访问。与 NSArray 不同的是,RLMResults 需要指定类型,并且其当中只能包含RLMObject 子类类型的属性。

所有的查询(包括查询和属性访问)在 Realm 中都是延迟加载的,只有当属性被访问时,才能够读取相应的数据。

查询结果并不是数据的拷贝:修改查询结果(在写入事务中)会直接修改硬盘上的数据。同样地,您可以直接通过包含在RLMResults中的RLMObject对象完成遍历关系图的操作。

除非查询结果被使用,否则检索的执行将会被推迟。这意味着链接几个不同的临时 {RLMResults} 来进行排序和匹配数据,不会执行额外的工作,例如处理中间状态。

一旦检索执行之后,或者 通知模块 被添加之后, RLMResults 将随时保持更新,接收 Realm 中,在后台线程上执行的检索操作中可能所做的更改。

从 Realm 中检索对象的最基本方法是+[RLMObject allObjects],这个方法将会返回带有所有RLMObjectRLMResults实例,并且这个实例的类型将是默认 Realm 数据库中被查询的子类类型。

RLMResults<Dog *> *dogs = [Dog allObjects]; // 从默认的 Realm 数据库中,检索所有狗狗

条件查询(Filtering)

如果您熟悉NSPredicate的话,那么您就能很容易掌握其在 Realm 中的查询方法。RLMObjects、RLMRealm、RLMArray 以及 RLMResults 都提供了方法,允许您通过简单地传递一个 NSPredicate 实例、断言字符串或者断言格式化字符串来完成查询这顶RLMObject实例的操作,正如您在 NSArray 中执行查询的哪样。

比如说,下面的例子就展示了如何通过从默认的 Realm 数据库中调用 [RLMObject objectsWhere:] 方法来检索所有棕黄色,并且以“大”开头命名的狗狗的:

// 使用断言字符串查询
RLMResults<Dog *> *tanDogs = [Dog objectsWhere:@"color = '棕黄色' AND name BEGINSWITH '大'"];

// 使用 NSPredicate 查询
NSPredicate *pred = [NSPredicate predicateWithFormat:@"color = %@ AND name BEGINSWITH %@",
                                                     @"棕黄色", @"大"];
tanDogs = [Dog objectsWithPredicate:pred];

查看苹果的断言编程指南来获取更多关于断言查询和NSPredicate Cheatsheet的使用信息。 Realm 支持许多常见的断言:

  • 比较操作数(comparison operand)可以是属性名称或者某个常量,但至少有一个操作数必须是属性名称;
  • 比较操作符 ==<=<>=>!=, 以及 BETWEEN 支持 int, long, long long, float, double, 以及 NSDate 属性类型的比较,比如说 age == 45
  • 相等比较 ==以及!=,比如说[Employee objectsWhere:@"company == %@", company]
  • 比较操作符 == and != 支持布尔属性;
  • 对于 NSString 和 NSData 属性来说,我们支持 ==!=BEGINSWITHCONTAINS 以及 ENDSWITH 操作符,比如说 name CONTAINS ‘Ja’
  • 对于 NSString 属性而言,LIKE 操作符可以比较左手属性 (left hand property) 以及右手表达式 (right hand expression):我们允许使用 ?* 通配符,其中 ? 将可以匹配 1 个字符,而 * 则用以匹配 0 个或多个字符。例如,value LIKE '?bc*' 将可以匹配出注入 “abcde” 以及 “cbc” 之类的字符串。
  • 字符串支持忽略大小写的比较方式,比如说 name CONTAINS[c] ‘Ja’ ,注意到其中字符的大小写将被忽略;注意大小写只有字符 “A-Z” 和 “a-z” 会被忽略。可以与 [d] 修饰符结合使用。
  • 字符串之间的比较支持忽略变音符号,比如说 name BEGINSWITH[d] ‘e’ 可以匹配到 étoile。可以与 [c] 修饰符结合使用。
  • Realm 支持以下复合操作符:“AND”“OR” 以及 “NOT”。比如说 name BEGINSWITH ‘J’ AND age >= 32
  • 包含操作符 IN,比如说 name IN {‘Lisa’, ‘Spike’, ‘Hachi’}
  • ==!=支持与 nil 比较,比如说 [Company objectsWhere:@"ceo == nil"]。注意到这只适用于有关系的对象,这里 ceo 是 Company 模型的一个属性。
  • 通过 ==, != 进行空值比较,比如说 [Company objectsWhere:@"ceo == nil"]; 注意,Realm 将 nil 视为一个特殊的值而不是“缺失值”,不像 SQL 那样 nil 等于自身。
  • ANY 比较,比如说 ANY student.age < 21
  • RLMArray 以及 RLMResults 属性支持集合表达式:@count@min@max@sum 以及 @avg,例如 [Company objectsWhere:@"[email protected] > 5"] 用以寻找所有超过 5 名雇员的公司。
  • 支持子查询,不过有限制:
    • @count 是唯一能应用在 SUBQUERY 表达式中的操作符
    • SUBQUERY(…)[email protected] 表达式必须与常量进行比较
    • 相关子查询目前还不支持

要了解关于断言的更多详情,请查看[RLMObject objectsWhere:] 的详细信息。

排序

RLMResults 允许您指定一个排序标准,从而可以根据键值路径(key path)、属性或者多个排序描述符(sort descriptors)进行排序。比如说,下列代码将上面例子中返回的狗狗根据名字升序进行排序:

// 排序名字以“大”开头的棕黄色狗狗
RLMResults<Dog *> *sortedDogs = [[Dog objectsWhere:@"color = '棕黄色' AND name BEGINSWITH '大'"]
                               sortedResultsUsingKeyPath:@"name" ascending:YES];

键值路径也可以是对一关系的其中一个属性:

RLMResults<Person *> *dogOwners = [Person allObjects];
RLMResults<Person *> *ownersByDogAge = [dogOwners sortedResultsUsingKeyPath:@"dog.age" ascending:YES];

请注意,sortedResultsUsingKeyPath: 以及 sortedResultsUsingProperty: 不支持将多个属性用作排序基准,此外也不能链式调用(只有最后一个 sortedResults... 会被调用)。如果要按多个属性进行排序的话,请使用 sortedResultsUsingDescriptors: 方法,然后向其中传递多个 RLMSortDescriptor 对象。

关于排序的更多信息,请参见:

注意到,只有当检索操作排序过的时候,Results 的次序只能保证不变。出于性能考虑,插入后的顺序不能保证稳定。如果您需要保证插入数据后的顺序,在这里有一些解决方案。

链式查询

Realm 查询引擎一个特性就是它能够通过非常小的事务开销来执行链式查询(chain queries),而不需要像传统数据库那样为每个成功的查询创建一个不同的数据库服务器访问。

比如说,如果我们想获得获得棕黄色狗狗的查询结果,并且在这个查询结果的基础上再获得名字以“大”开头的棕黄色狗狗,那么您可以像下列方式那样将两个查询链接起来:

RLMResults<Dog *> *tanDogs = [Dog objectsWhere:@"color = '棕黄色'"];
RLMResults<Dog *> *tanDogsWithBNames = [tanDogs objectsWhere:@"name BEGINSWITH '大'"];

自动更新

RLMResults 是底层数据的动态表现,其会进行自动更新,这意味着检索到的结果不能进行重复检索。它们会反映出当前线程上 Realm 的当前状态,包括在当前线程上的写事务当中。唯一的例外是,当使用 for...in 遍历时,因为当遍历开始的时候,总会全部遍历完所有匹配该检索的对象,即使某些对象在遍历过程中被过滤器修改或者删除。

RLMResults<Dog *> *puppies = [Dog objectsInRealm:realm where:@"age < 2"];
puppies.count; // => 0

[realm transactionWithBlock:^{
  [Dog createInRealm:realm withValue:@{@"name": @"大黄", @"age": @1}];
}];

puppies.count; // => 1

这对所有的 RLMResults 都有影响,不管其是匹配查询的还是链式查询所检索出来的。

RLMResults 的这个特性不仅让 Realm 保证速度和效率,它同时还让代码更加简洁、更为灵活。比如说,如果您的视图控制器是基于查询结果而现实的,您可以将 RLMResults 存储为一个属性,这样就无需在每次访问前都要刷新数据以确保数据最新了。

您也可以查看 Realm 通知 一节以确认 Realm 数据何时被更新,比如说由此来决定应用 UI 何时需要被更新,这样就无必重新检索 RLMResults 了。

由于检索结果是自动更新的,因此不要迷信 index 以及 count 会一直保持静止。RLMResults 数据不变的唯一情况就是处于快速枚举的过程当中,这允许修改匹配检索的对象:

[realm beginWriteTransaction];
for (Person *person in [Person objectsInRealm:realm where:@"age == 10"]) {
  person.age++;
}
[realm commitWriteTransaction];

此外,还可以使用 键值编码 来对 RLMResults 执行操作。

查询结果限制

大多数其他数据库技术都提供了从检索中对结果进行“分页”的能力(例如 SQLite 中的 “LIMIT” 关键字)。这通常是很有必要的,可以避免一次性从硬盘中读取太多的数据,或者将太多查询结果加载到内存当中。

由于 Realm 中的检索是惰性的,因此这行这种分页行为是没有必要的。因为 Realm 只会在检索到的结果被明确访问时,才会从其中加载对象。

如果由于 UI 相关或者其他代码实现相关的原因导致您需要从检索中获取一个特定的对象子集,这和获取 RLMResults 对象一样简单,只需要读出您所需要的对象即可。

// 循环前 5 个 Dog 对象
// 限制从磁盘读取的对象数量
RLMResults<Dog *> *dogs = [Dog allObjects];
for (NSInteger i = 0; i < 5; i++) {
  Dog *dog = dogs[i];
  // ...
}

Realm 数据库

默认的 Realm 数据库

您可能很早就已经注意到,我们总是通过调用 [RLMRealm defaultRealm] 来初始化以及访问我们的 realm 变量。这个方法将会返回一个 RLMRealm 对象,并指向您应用的 Documents (iOS) 或者 Application Support (OS X)文件夹下的一个名为“default.realm”的文件。

许多 Realm API 中的方法都支持两种默认的数据库访问方式,一种是 RLMRealm 实例,另一种是访问默认 Realm 数据库的便捷版本。例如 [RLMObject allObjects] 等同于 [RLMObject allObjectsInRealm:[RLMRealm defaultRealm]]

请注意,默认的 Realm 构造方法和便利方法都不允许进行错误处理;您应当在初始化 Realm 数据库一定不能失败的情况下使用这些方法。参考错误处理一节获取更多信息。

Realm 配置

通过RLMRealmConfiguration您可以配置诸如 Realm 文件在何处存储之类的信息。

配置同时也可以在每次您需要使用 Realm 实例的时候传递给[RLMRealm realmWithConfiguration:config error:&err],或者您也可以通过 [RLMRealmConfiguration setDefaultConfiguration:config] 来为默认的 Realm 数据库进行配置。

比如说,假设有这样一个应用,用户必须登录到您的网站后台才能够使用,然后您希望这个应用支持快速帐号切换功能。 您可以为每个帐号创建一个特有的 Realm 文件,通过对默认配置进行更改,就可以直接使用默认的 Realm 数据库来直接访问了,如下所示:

@implementation SomeClass
+ (void)setDefaultRealmForUser:(NSString *)username {
  RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];

  // 使用默认的目录,但是使用用户名来替换默认的文件名
  config.fileURL = [[[config.fileURL URLByDeletingLastPathComponent]
                      URLByAppendingPathComponent:username]
                      URLByAppendingPathExtension:@"realm"];

  // 将这个配置应用到默认的 Realm 数据库当中
  [RLMRealmConfiguration setDefaultConfiguration:config];
}
@end

其他的realm数据库

有的时候,在不同位置存储多个 Realm 数据库是十分有用的。 例如,如果您需要将您应用的某些数据打包到一个 Realm 文件中,作为主要 Realm 数据库的扩展。 您可以像以下代码这样做:

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];

// 获取需要打包文件的 URL 路径
config.fileURL = [[NSBundle mainBundle] URLForResource:@"MyBundledData" withExtension:@"realm"];
// 以只读模式打开文件,因为应用数据包并不可写
config.readOnly = YES;

// 通过配置打开 Realm 数据库
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];

// 从打包的 Realm 数据库中读取某些数据
RLMResults<Dog *> *dogs = [Dog objectsInRealm:realm where:@"age > 5"];

请注意,使用自定义 URL 路径来初始化 Realm 数据库需要拥有路径所在位置的写入权限。 通常存储可写 Realm 文件的地方是位于 iOS 上的“Documents”文件夹以及位于 OS X 上的“Application Support”文件夹。 具体情况,请遵循苹果的 iOS 数据存储指南, 它推荐将文件存储在<Application_Home>/Library/Caches目录下。

内存数据库

通常情况下,Realm 数据库是存储在硬盘中的,但是您能够通过设置 inMemoryIdentifier 而不是设置RLMRealmConfiguration 中的 fileURL 属性,以创建一个完全在内存中运行的数据库。

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.inMemoryIdentifier = @"MyInMemoryRealm";
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];

内存数据库在每次程序运行期间都不会保存数据。但是,这不会妨碍到 Realm 的其他功能,包括查询、关系以及线程安全。 假如您需要灵活的数据读写但又不想储存数据的话,那么内存数据库对您来说一定是一个不错的选择。

内存数据库会在临时文件夹中创建多个文件,用来协调处理诸如跨进程通知之类的事务。 实际上没有任何的数据会被写入到这些文件当中,除非操作系统由于内存过满需要清除磁盘上的多余空间。

注意: 当所有具备特定标识符的Realm 内存数据库实例超出可用范围,但是却没有被引用的话,那么_ Realm 内存数据库中的所有数据都会被删除_。我们建议您在应用的生命周期内,保持对 Realm 内存数据库的强引用。(对于存储在硬盘当中的 Realm 数据库而言,这并不是必须的操作。)

错误处理

和所有硬盘读写操作一样,当资源受限的时候创建一个 RLMRealm 实例可能会出现失败的情况。在实际情况中,这只会当首次在指定线程中创建 Realm 对象的时候发生。从相同线程中后续访问 Realm 数据库将会重复使用缓存的实例,不会导致失败。

要处理在指定线程中初次 Realm 数据库导致的错误, 给 error 参数提供一个 NSError 指针:

NSError *error = nil;

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
if (!realm) {
  // 错误处理
}

异步打开 Realm 数据库

如果在打开 Realm 数据库的时候,需要执行耗时操作的话(例如执行 迁移压缩 或者下载可同步 Realm 数据库中的远程内容),那么应该是用 asyncOpen API 来执行所需的操作,以便在 Realm 被发送给指定队列之前,在后台线程中处于可用状态。例如:

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
  // 潜在的冗长数据迁移操作
};
[RLMRealm asyncOpenWithConfiguration:config
                       callbackQueue:dispatch_get_main_queue()
                            callback:^(RLMRealm *realm, NSError *error) {
  if (realm) {
    // Realm 成功打开,迁移已在后台线程中完成
  } else if (error) {
    // 处理打开 Realm 时所发生的错误
  }
}];

此外,可同步数据库会等待所有的远程内容都处于可用状态,然后再开始执行下载和本地应用操作。例如:

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.syncConfiguration = [[RLMSyncConfiguration alloc] initWithUser:user realmURL:realmURL];
[RLMRealm asyncOpenWithConfiguration:config
                       callbackQueue:dispatch_get_main_queue()
                            callback:^(RLMRealm *realm, NSError *error) {
  if (realm) {
    // Realm 成功打开,所有的远程数据都可用
  } else if (error) {
    // 处理在打开或者下载 Realm 内容时所发生的错误
  }
}];

在Realm数据库间拷贝数据

拷贝 Realm 对象到另一个 Realm 数据库十分简单,只需将原始对象传递给+[RLMObject createInRealm:withValue:]。例如, [MyRLMObjectSubclass createInRealm:otherRealm withValue:originalObjectInstance].

请注意,+[RLMObject createInRealm:withValue:] 不支持处理循环对象图 (cyclical object graphs)。请不要将包含双向关系(无论是直接双向关联还是间接双向关联)的对象传递进来。

查找 Realm 文件

如果您不知道如何寻找应用的 Realm 文件,那么请查看这个StackOverflow回答来获取详细信息.

附加的 Realm 文件

除了标准的 .realm 文件之外,Realm 同样还会为其内部操作生成和维护一些额外的文件和文件夹。

  • .realm.lock - 对资源进行锁定的文件
  • .realm.management - 存放进程锁文件 (Interprocess Lock) 的文件夹
  • .realm.note - 用于通知的命名管道

这些文件对 .realm 数据库文件本身不会造成任何影响,即时它们所依赖的数据库文件被删除或者被替换掉也不会引发任何错误。

提交 Realm 问题 的时候,请一定要确保提交这些附加文件,因为它们包含了诸多的有效信息可以帮助我们进行 Debug。

在应用中建立 Realm 数据库

为了能够使您的用户在应用第一次启动时就能够直接使用一些初始数据,一种通常的做法就是为应用配置初始化数据。具体步骤是:

  1. 首先,定位 Realm 的所在位置。您应该使用与最终版本相同的数据模型来创建 Realm 数据库,并将您想要打包的数据放置到您的应用当中。由于 Realm 文件是跨平台的,因此您能够测试您的OS X app (查看我们的JSONImport example)或者在模拟器中运行您的 iOS app;
  2. 在生成 Realm 文件的代码处,您需要结尾对文件进行压缩拷贝(参见 -[RLMRealm writeCopyToPath:error:])。 这有助于减少 Realm 的文件体积,让您发布的应用体积更小;
  3. 将您最终的 Realm 文件的压缩拷贝拖懂到您最终应用的 Xcode 项目导航栏中;
  4. 前往您应用的 Xcode Build Phase 选项卡,添加 Realm 文件到 “Copy Bundle Resources” 当中;
  5. 这样,您就能够在您的应用中使用这个打包好的 Realm 数据库了。 您能通过使用[[NSBundle mainBundle] pathForResource:ofType:]来得到数据库路径;
  6. 如果打包的 Realm 文件包含有您不想修改的固定数据,您也能通过为RLMRealmConfiguration 对象设置 readOnly = true 选项,这样就可以将其从包路径直接打开了。 如果您打算修改初始数据的话,您可以通过[[NSFileManager defaultManager] copyItemAtPath:toPath:error:],将这个打包的文件拷贝到应用的 Document 文件夹下。

您能够参考我们的迁移例程应用来学习如何使用打包好的 Realm 文件。

类的子集(Class Subsets)

在某些情况下,您可能想要对哪个类能够存储在指定 Realm 数据库中做出限制。 例如,如果有两个团队分别负责开发您应用中的不同部分,并且同时在应用内部使用了 Realm 数据库,那么您肯定不希望为它们协调进行数据迁移。 您可以通过设置您的 RLMRealmConfigurationobjectClasses 属性来对类做出限制。

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.objectClasses = @[MyClass.class, MyOtherClass.class];
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];

压缩 Realm 数据库

Realm 的架构决定了 Realm 文件大小总是比其所包含的最新状态数据要大。至于为什么这种架构能使 Realm 拥有良好的性能、并发性和安全性优势,请参阅我们文档中的 线程 部分。

另外,为了避免频繁地进行系统调用导致文件大小的扩张,Realm 文件通常在运行时不会缩减其大小,而是以不同的尺寸增量来进行增长,并且新的数据会被写入到文件的可用空间内,这个可用空间会受到追踪。

然而存在这样一种情况,Realm 文件当中的很大一部分完全被可用空间所占据。所以在这个版本中,我们在 Realm 的配置对象中添加了一个 shouldCompactOnLaunch 闭包属性,从而确定 Realm 是否应该在返回实例对象之前,进行压缩。例如:

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.shouldCompactOnLaunch = ^BOOL(NSUInteger totalBytes, NSUInteger usedBytes){
  // totalBytes 指的是硬盘上文件的大小(以字节为单位)(数据 + 可用空间)
  // usedBytes 指的是文件中数据所使用的字节数

  // 如果文件的大小超过 100 MB,并且已用空间低于 50%
  NSUInteger oneHundredMB = 100 * 1024 * 1024;
  return (totalBytes > oneHundredMB) && (usedBytes / totalBytes) < 0.5;
};

NSError *error = nil;
// 如果配置闭包的条件满足,那么 Realm 就会在首次打开的过程中进行压缩
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
if (error) {
  // 处理打开 Realm 或者压缩时产生的错误
}

在引擎内部,压缩操作将会读取 Realm 文件的全部内容,然后在另一个位置将这些数据重写为另一个新的文件,然后将原始文件替换掉。根据文件中的数据量,这个操作很可能会花费大量时间。

我们鼓励您大量尝试这个操作,以便在执行压缩和 Realm 文件大小之间找到良好的平衡点。

最后,如果另一个进程正在访问 Realm 的话,即便配置闭包的条件符合,那么压缩过程也会被跳过。这是因为在 Realm 被访问的时候,是无法安全执行压缩操作的。

可同步 Realm 数据库不支持设置 shouldCompactOnLaunch 闭包。这是因为压缩不会保留事务日志,而为了进行同步,事务日志又是必须保留的。

删除 Realm 文件

在某些情况下,例如清除缓存、重置整个应用数据的时候,可能会需要将 Realm 文件从硬盘上被完全移除。

由于 Realm 会尽量在非必需的时候,避免将数据拷贝到内存当中。由单个 Realm 数据库所管理的全部对象都包含有对硬盘上文件的引用,并且会在文件被安全删除之前完成释放。这其中包括了所有从 Realm 读取(或者添加到 Realm)的对象,以及所有的 RLMArrayRLMResults,还有 RLMThreadSafeReference 对象, 以及 RLMRealm 本身。

在实际当中,这意味着 Realm 的删除操作应该在应用启动后、Realm 开启之前完成,或者在显式声明的自动释放池当中打开 Realm 后完成。这样才能确保所有的 Realm 对象都能成功释放。

最后,尽管这不是必要的,您应当删除辅助的 Realm 文件 以及主要的 Realm 文件以确保所有相关文件得到了完全清理。

@autoreleasepool {
  // 所有 Realm 的使用操作
}
NSFileManager *manager = [NSFileManager defaultManager];
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
NSArray<NSURL *> *realmFileURLs = @[
  config.fileURL,
  [config.fileURL URLByAppendingPathExtension:@"lock"],
  [config.fileURL URLByAppendingPathExtension:@"note"],
  [config.fileURL URLByAppendingPathExtension:@"management"]
];
for (NSURL *URL in realmFileURLs) {
  NSError *error = nil;
  [manager removeItemAtURL:URL error:&error];
  if (error) {
    // 处理错误
  }
}

在后台应用刷新中使用 Realm

在 iOS 8 以上的系统中,当设备锁定的时候,应用中的文件会自动使用 NSFileProtection 进行加密。当设备锁定,并且您的 Realm 文件的 NSFileProtection 属性被设为『加密』(这个默认情况)的时候,如果您的应用视图执行任何涉及 Realm 的操作,那么就会抛出一条 open() failed: Operation not permitted 的异常。

为了处理这种情况,确认将文件保护属性降级为一个不太严格的、允许即使在设备锁定时都可以访问文件的属性,例如:NSFileProtectionCompleteUntilFirstUserAuthentication,并确保这个属性同时应用到了 Realm 文件本身以及其辅助文件上面。

如果您选择不完整的 iOS 文件加密的话,我们鼓励您使用 Realm 自己的内置加密来确保您的数据安全。

由于辅助文件有时会进行延迟创建和删除操作,我们建议您将文件保护属性应用到包含 Realm 文件的父级文件夹上面。这将确保该属性被恰当地应用到所有相关的 Realm 文件上面,而无需关心它们的创建时间。

RLMRealm *realm = [RLMRealm defaultRealm];

// 获取我们的 Realm 文件的父级目录
NSString *folderPath = realm.configuration.fileURL.URLByDeletingLastPathComponent.path;

// 解除这个目录的保护
[[NSFileManager defaultManager] setAttributes:@{NSFileProtectionKey: NSFileProtectionNone}
                                 ofItemAtPath:folderPath error:nil];

线程(Threading)

在单个线程中,你只需要将所有东西看做是普通的对象即可,无需考虑并行或者多线程处理的问题。线程锁定、资源协调访问都是不需要的(即时它们同时被其他线程所修改),唯一的修改操作就是包含在写事务中的操作。

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

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

检视其他线程上的变化

在主 UI 线程中(或者任何一个位于 runloop 中的线程),对象会在 runloop 的每次循环过程中自行获取其他线程造成的更改。其余时候您只能够对快照进行操作,因此单个方法中得到的数据将始终不变,无需担心其他线程会对其造成影响。

当您第一次打开 Realm 数据库的时候,它会根据最近成功的写事务提交操作来更新当前状态,并且在刷新之前都将一直保持在当前版本。Realm 会自每个 runloop 循环的开始自动进行刷新,除非 RLMRealmautorefresh 属性设置为 NO。如果某个线程没有 runloop 的话(通常是因为它们被放到了后台进程当中),那么 -[RLMRealm refresh] 方法必须手动调用,以确保让事务维持在最新的状态当中。

Realm 同样也会在写入事务提交(-[RLMRealm commitWriteTransaction])的时候刷新。

如果定期刷新 Realm 失败的话,就可能会导致某些事务的版本变为“锁定(pinned)”状态,阻止 Realm 重用该版本的硬盘空间,从而导致文件尺寸变大。查看我们的 当前的限制以获取关于此情况的更多信息。

跨线程传递实例

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

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

Realm 提供了一个机制,通过以下三个步骤来保证受到线程限制的实例能够安全传递:

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

举个例子:

Person *person = [Person new];
person.name = @"Jane";
[realm transactionWithBlock:^{
  [realm addObject:person];
}];
RLMThreadSafeReference *personRef = [RLMThreadSafeReference
  referenceWithThreadConfined:person];

dispatch_async(queue, ^{
  @autoreleasepool {
    RLMRealm *realm = [RLMRealm realmWithConfiguration:realm.configuration
                                                 error:nil];
    Person *person = [realm resolveThreadSafeReference:personRef];
    if (!person) {
      return; // person 已被删除
    }
    [realm transactionWithBlock:^{
      person.name = @"Jane Doe";
    }];
  }
});

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

* 这些类型的某些属性和方法可以在任意线程中进行访问:

  • RLMRealm: 所有的属性、类方法和构造器;all properties, class methods, and initializers.
  • RLMObject: isInvalidatedobjectSchemarealm,以及所有的类方法和构造器;
  • RLMResults: objectClassNamerealm
  • RLMArray: isInvalidatedobjectClassNamerealm

跨线程使用数据库

为了在不同的线程中使用同一个 Realm 文件,您需要为您应用的每一个线程初始化一个新的Realm 实例。 只要您指定的配置是相同的,那么所有的 Realm 实例都将会指向硬盘上的同一个文件。

我们还 不支持 跨线程共享Realm 实例。 Realm 实例要访问相同的 Realm 文件还必须使用相同的 RLMRealmConfiguration

当写入大量数据的时候,在一个单独事务中通过批量执行多次数据修改操作是非常高效的。事务也可以使用 Grand Central Dispatch(GCD) 在后台运行,以防止阻塞主线程。 RLMRealm 对象并不是线程安全的,并且它也不能够跨线程共享,因此您必须要为每一个您想要执行读取或者写入操作的线程或者 dispatch 队列创建一个 Realm 实例。 这里有一个在后台队列中插入百万数据的例子:

dispatch_async(queue, ^{
  @autoreleasepool {
    // 在这个线程中获取 Realm 和表实例
    RLMRealm *realm = [RLMRealm defaultRealm];

    // 通过开启写入操作将写入闭包分成多个微小部分
    for (NSInteger idx1 = 0; idx1 < 1000; idx1++) {
      [realm beginWriteTransaction];

      // 通过字典插入行,忽略属性次序
      for (NSInteger idx2 = 0; idx2 < 1000; idx2++) {
        [Person createInRealm:realm
                    withValue:@{@"name"      :  randomString,
                                @"birthdate" : randomDate}];
      }

      // 提交写入事务以确保数据在其他线程可用
      [realm commitWriteTransaction];
    }
  }
});

JSON

Realm 没有提供对 JSON 的直接支持,但是您可以使用 [NSJSONSerialization JSONObjectWithData:options:error:] 的输出完成从 JSON 中添加 RLMObject 的操作。 由此所产生的 KVC 兼容的对象可以使用创建和更新对象的 标准 API 来添加/更新 RLMObject

// 表示城市的一个 Realm 对象
@interface City : RLMObject
@property NSString *name;
@property NSInteger cityId;
// 其它空余的属性…
@end
@implementation City
@end // 并不需要

NSData *data = [@"{\"name\": \"旧金山\", \"cityId\": 123}" dataUsingEncoding: NSUTF8StringEncoding];
RLMRealm *realm = [RLMRealm defaultRealm];

// 从包含 JSON 的 NSData 中插入数据
[realm transactionWithBlock:^{
  id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
  [City createOrUpdateInRealm:realm withValue:json];
}];

如果在 JSON 中包含了嵌套的对象或者数组的话,这些数据都将被自动映射到对一以及对多关系——参见嵌套对象 一节获取详情。

当使用这种方法在 Realm 中插入或者更新 JSON 数据的时候,要注意 Realm 需要确保 JSON 的属性名和类型能够与 RLMObject 属性完全匹配。例如:

  • float 属性应当使用 float 进行初始化——您也可以选择 NSNumbers
  • NSDateNSData 属性无法从字符串进行自动推断,而应该在传递给 [RLMObject createOrUpdateInRealm:withValue:] 之前转换为适当的类型。
  • 如果 JSON 中的属性是 null (例如:NSNull) 提供给了一个必需属性的话,那么会抛出异常。
  • 如果某个必需属性在插入操作中没有提供的话,那么会抛出异常。
  • Realm 将会忽略 JSON 中没有以 RLMObject 定义的任何属性。

如果您的 JSON 模式和您的 Realm 对象无法完全一致的话,那么我们推荐您使用第三方的模型映射框架,这样可以对您的 JSON 进行转换。 Objective‑C 有一系列积极维护的能够与 Realm 协同工作的模型映射框架,其中的一部分已经在 realm-cocoa 仓库 当中列出来了。

通知(Notification)

通过调用 addNotificationBlock 方法进行通知注册后,无论哪个 RLMRealmRLMResultsRLMArray 还是 RLMLinkingObjects 对象更新,都可以得到通知。

一旦持有某个引用被返回的通知令牌,那么通知就会被触发。 您应当在类注册的时候保持对此令牌的强引用,以便能够进行更新,因为当通知令牌被销毁的时候,通知会被自动解除注册。

此外,还可以通过 键值观察 来观察某个单独 RLMObject 对象的变化。

通知是如何传递的

通知总是会在它们最初所注册的线程上进行传递。该线程必须包含有当前正在运行的 Run Loop。 如果您想要在子线程上而不是在主线程上注册通知,并且该线程没有 Run Loop 的话,那么您需要负责启动 Run Loop。

当每个相关的写操作事务提交之后,通知处理闭包将会被异步调用,无论这个写操作事务是在哪个线程还是哪个进程上进行的。

如果某个写操作事务当中包含了 Realm 的版本升级操作的话,那么通知处理闭包可能会_同步_调用。当 Realm 更新到最新版本、或者被观察的 Realm 实体被修改或者被删除的时候,都可能会触发通知。这种通知将会在当前写操作事务的上下文档中运行,这意味着尝试在通知处理闭包当中进行写操作事务将会导致 Realm 抛出异常。如果您的应用需要使用此功能,并且很有可能会遇到这种情况的话,那么您可以使用 -[RLMRealm isInWriteTransaction] 来判断当前是否处于写操作事务当中。

由于通知是通过 Run Loop 进行传递的,因此通知的传递很可能会被 Run Loop 上的其他活动所延迟。当通知不能即时触发的时候,多个写入事务可能会被合并到单独的一个通知当中。

Realm 通知

通知处理闭包可以在整个 Realm 数据库上进行注册。当相关 Realm 的写操作事务被提交之后,无论这个事务发生在何种线程或者何种进程之中,这个通知处理闭包都将会被触发:

// 获取 Realm 通知
token = [realm addNotificationBlock:^(NSString *notification, RLMRealm * realm) {
    [myViewController updateUI];
}];

// 随后
[token stop];

集合通知

集合通知 (Collection notifications) 与 Realm 通知有着略微的不同,其在细粒层级 (fine-grained level) 中描述了发生何种变化。包括自上一次通知以来出现的:已插入对象已删除对象,或者已修改对象的索引。

集合通知是异步触发的,首先它会在初始结果出现的时候触发,随后当某个写入事务改变了集合中的所有或者某个对象的时候,通知都会再次触发。

这些变化可以通过传递到通知闭包当中的 RLMCollectionChange 参数访问到。这个对象当中包含了受 deletionsinsertionsmodifications 状态所影响的索引信息。

前两个状态:删除 (deletions)插入 (insertions) 记录了对象当进入或离开集合时的其所在索引数据。当您向 Realm 中添加对象或者从 Realm 中删除对象的时候会触发这个状态变化。 对于 RLMResults 也是适用的,当您适用特定的值进行匹配的时候,其中的对象就会发生变化,这样就可以知道某个对象现在是否匹配当前的这个查询。 对于基于 RLMArrayRLMLinkingObjects 以及 RLMResults 衍生出来的集合来说,当关系中的对象被添加或者删除的时候,也会触发这个状态变化。

当某个对象的属性发生变化的时候,您都能够获得到 修改 (modifications) 的通知,要注意此时这个对象仍然还位于集合当中。 当对一关系对多关系 发生变化时也会触发这个状态变化,但是对于 反向关系 来说则不会触发这个变化。

@interface Dog : RLMObject
@property NSString *name;
@property NSData   *picture;
@property NSInteger age;
@end

@implementation Dog
@end

RLM_ARRAY_TYPE(Dog)
@interface Person : RLMObject
@property NSString             *name;
@property RLMArray<Dog *><Dog> *dogs;
@end

@implementation Person
@end

因此,假设您通过上述给定的模型代码,对 Dog Owner 列表建立了观察。那么当以下情况发生的时候,就会收到相匹配的 Person 对象发生修改状态的通知:=

  • 您修改了 Personname 属性;
  • 您向 Persondogs 属性当中添加或者移除了一个 Dog 对象;
  • 您修改了属于某个 PersonDogage 属性。

这使得在您的 UI 当中单独控制动画以及视觉效果更新成为了可能,而不是在每次通知发生的时候肆意重载各种东西。

- (void)viewDidLoad {
  [super viewDidLoad];

  // 观察 RLMResults 通知
  __weak typeof(self) weakSelf = self;
  self.notificationToken = [[Person objectsWhere:@"age > 5"] addNotificationBlock:^(RLMResults<Person *> *results, RLMCollectionChange *changes, NSError *error) {
    if (error) {
      NSLog(@"Failed to open Realm on background worker: %@", error);
      return;
    }

    UITableView *tableView = weakSelf.tableView;
    // 对于变化信息来说,检索的初次运行将会传递 nil
    if (!changes) {
      [tableView reloadData];
      return;
    }

    // 检索结果被改变,因此将它们应用到 UITableView 当中
    [tableView beginUpdates];
    [tableView deleteRowsAtIndexPaths:[changes deletionsInSection:0]
                     withRowAnimation:UITableViewRowAnimationAutomatic];
    [tableView insertRowsAtIndexPaths:[changes insertionsInSection:0]
                     withRowAnimation:UITableViewRowAnimationAutomatic];
    [tableView reloadRowsAtIndexPaths:[changes modificationsInSection:0]
                     withRowAnimation:UITableViewRowAnimationAutomatic];
    [tableView endUpdates];
  }];
  }
- (void)dealloc {
  [self.notificationToken stop];
  }

对象通知

Realm 支持对象级别的通知。您可以在特定的 Realm 对象上进行通知的注册,这样就可以在此对象被删除时、或者该对象所管理的属性值被修改时(同样也适用于当对象所管理的属性值被设置为既有值的时候),获取相应的通知。

只有由 Realm 数据库所管理的对象才能够进行通知注册。

对于在不同线程或者不同进程当中所值行的写操作事务而言,当管理对象的 Realm 数据库(自动)更新到新版本时,通知处理闭包将会被调用。对于本地的写操作事务而言,将在事务提交之后的某个时间被调用。

通知处理闭包拥有三个参数。第一个参数 deletedBOOL 类型,表示该对象是否已被删除。如果返回的值是 YES,那么其他参数将为 nil,并且此闭包将不会被再次调用。

第二个参数 changes 是一个包含 RLMPropertyChange 对象的 NSArray 类型。这个对象包含有被修改的属性名(以字符串的形式提供)、修改前的属性值以及当前的属性值。

第三个参数则是 NSError。如果有涉及到该对象的错误发生,那么 NSError 就会包含相关的错误信息,且 changes 参数将为空,deleted 的值将为 NO,且这个闭包将不会被再次调用。

// skip validation
@interface RLMStepCounter : RLMObject
@property NSInteger steps;
@end

@implementation RLMStepCounter
@end

// ...

RLMStepCounter *counter = [[RLMStepCounter alloc] init];
counter.steps = 0;
RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
[realm addObject:counter];
[realm commitWriteTransaction];

__block RLMNotificationToken *token = [counter addNotificationBlock:^(BOOL deleted,
                                                                      NSArray<RLMPropertyChange *> *changes,
                                                                      NSError *error) {
    if (deleted) {
        NSLog(@"The object was deleted.");
    } else if (error) {
        NSLog(@"An error occurred: %@", error);
    } else {
        for (RLMPropertyChange *property in changes) {
            if ([property.name isEqualToString:@"steps"] && [property.value integerValue] > 1000) {
                NSLog(@"Congratulations, you've exceeded 1000 steps.");
                [token stop];
                token = nil;
            }
        }

    }
}];

界面驱动更新

Realm 的通知总是以异步的方式进行传递,因此这些操作永远不会阻塞主 UI 线程,也不会导致应用卡顿。然而,当变更_切实需要_在主线程进行同步传递,并能够立即反映在 UI 的时候,我们也提供了相应的解决方案。我们将这些事务称之为界面驱动更新 (Interface-Driven writes)。

例如,假设用户需要向表视图当中插入一个项目。理想状况下,UI 将会将这个操作用动画表现出来,然后当用户完成动作的时候立即启动相应的操作。

然而,此插入操作的 Realm 变更通知将会延时一段时间才进行传递,它会先将一个对象添加到表视图背后所关联的集合当中,随后再尝试去在 UI 当中插入一个新的项目。这种双重插入将会导致 UI 与后台数据之间的数据不一致,而这往往会导致应用崩溃!💥NSInternalInconsistencyException💥

借助 -[RLMRealm commitWriteTransactionWithoutNotifying:error:]Realm.commitWrite(withoutNotifying:),这样在执行界面驱动更新的时候,通过传递通知闭包的通知令牌的方式,这样就不会对变更操作执行二次响应。

这项特性是非常有用的,尤其是在需要大量同步的 Realm 当中使用细粒化集合通知 (fine-grained collection notification) 的时候,因为界面驱动更新依赖于控制应用何时该执行变更的完整状态。对于同步型 Realm 而言,无论 Realm 是否进行了同步,变更都能够成功应用,而这种变更往往会在应用生命周期当中的任何时候发生。

// Observe RLMResults Notifications
__weak typeof(self) weakSelf = self;
self.notificationToken = [self.collection addNotificationBlock:^(RLMResults<Item *> *results, RLMCollectionChange *changes, NSError *error) {
  if (error) {
    NSLog(@"Failed to open Realm on background worker: %@", error);
    return;
  }

  UITableView *tableView = weakSelf.tableView;
  // Initial run of the query will pass nil for the change information
  if (!changes) {
    [tableView reloadData];
    return;
  }

  // Query results have changed, so apply them to the UITableView
  [tableView beginUpdates];
  [tableView deleteRowsAtIndexPaths:[changes deletionsInSection:0]
                   withRowAnimation:UITableViewRowAnimationAutomatic];
  [tableView insertRowsAtIndexPaths:[changes insertionsInSection:0]
                   withRowAnimation:UITableViewRowAnimationAutomatic];
  [tableView reloadRowsAtIndexPaths:[changes modificationsInSection:0]
                   withRowAnimation:UITableViewRowAnimationAutomatic];
  [tableView endUpdates];
}];

- (void)insertItem {
  // Perform an interface-driven write on the main thread:
  [self.collection.realm beginWriteTransaction];
  [self.collection insertObject:[Item new] atIndex:0];
  // And mirror it instantly in the UI
  [tableView insertRowsAtIndexPaths:[NSIndexPath indexPathForRow:0 inSection:0]
                   withRowAnimation:UITableViewRowAnimationAutomatic];
  // Making sure the change notification doesn't apply the change a second time
  [self.collection.realm commitWriteTransactionWithoutNotifying:@[token]];
}

通知 APIs

关于通知的更多信息,请参阅下列 API 文档:

键值观察(Key-Value Observation, KVO)

Realm 对象的大多数属性都遵从 KVO 机制。 所有 RLMObject 子类的持久化(persisted)存储(未被忽略)的属性都是遵循 KVO 机制的,并且 RLMObject 以及 RLMArray无效的(invalidated) 属性也同样遵循(然而 RLMLinkingObjects 属性并不能使用 KVO 进行观察)。

观察 RLMObject 子类的某个未管理实例的属性的方法就如同观察其他 NSObject 子类 一样,不过要注意的是,当观察者(observer)存在的时候,您不能够使用诸如 [realm addObject:obj] 此类的方法向 Realm 数据库中添加对象。

观察已管理对象(也就是那些已经被添加到 Realm 当中的对象)属性的方法有些许不同。 对于已管理对象来说,有三种能够改变其属性值的方法:直接赋值修改;调用 [realm refresh] 方法或者当另一个线程提交了写入事务之后,Realm 自行进行了更新;当另一个线程发生了改变,但还未刷新的时候,在当前进程调用 [realm beginWriteTransaction]

在后面两种情况中,所有在另一个线程的写入事务中进行的修改都将会立即实现,并且会立刻发送 KVO 通知。 所有的中间过程都会被抛弃掉,因此如果在写入事务中您将一个属性从 1 递增到 10,那么在主线程您只会得到一个属性从 1 直接变到 10 的通知。 由于属性的值可以不在写入事务中发生改变,甚至还可以作为写入事务开始的一部分,因此我们不推荐在 -observeValueForKeyPath:ofObject:change:context: 中尝试修改已管理 Realm 对象的值。

NSMutableArray 属性不同,观察 RLMArray 属性值的改变并不需要使用 -mutableArrayValueForKey: 方法,虽然这个方法适合未写入 Realm 数据库中的数据。 相反,您可以直接调用 RLMArray 中的修改方法,任何观察该属性的对象都将会得到通知。

在我们的例程应用ReactiveCocoa from Objective‑CReactKit from Swift中,您可以找到关于使用 Realm KVO机制的简要例子。

数据迁移(Migration)

当您使用任意一个数据库时,您随时都可能打算修改您的数据模型。 由于 Realm 的数据模型是以标准的 Objective‑C 类来定义的,这使得修改模型就像修改其他的 Objective‑C 类一样方便。 例如,假设我们有如下 Person 模型:

@interface Person : RLMObject
@property NSString *firstName;
@property NSString *lastName;
@property int age;
@end

假如我们想要更新数据模型,给它添加一个 fullname 属性, 而不是将“姓”和“名”分离开来。 为此我们只需要改变一下代码即可,范例如下:

@interface Person : RLMObject
@property NSString *fullName;
@property int age;
@end

在这个时候如果您在数据模型更新之前就已经保存了数据的话,那么 Realm 就会注意到代码和硬盘上数据不匹配。 每当这时,您必须进行数据迁移,否则当您试图打开这个文件的话 Realm 就会抛出错误。

注意在进行迁移的时候, default property values 并不适用于新的对象,也不适用于既有对象的新属性。我们认为这是一个 Bug,我们会在 #1793 对其保持关注。

进行迁移

定义迁移操作

通过设置 RLMRealmConfiguration.schemaVersion 以及 RLMRealmConfiguration.migrationBlock 可以定义一个迁移操作以及与之关联的架构版本。 迁移闭包将会提供提供相应的逻辑操作,以让数据模型从之前的架构转换到新的架构中来。 每当通过配置创建完一个 RLMRealm 之后,迁移闭包将会在迁移需要的时候,将给定的架构版本应用到更新 RLMRealm 操作中。

例如,假设我们想要把上面所声明 Person 数据模型进行迁移。如下所示是最简单的数据迁移的必需流程:

// 在 [AppDelegate didFinishLaunchingWithOptions:] 中进行配置

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
// 设置新的架构版本。这个版本号必须高于之前所用的版本号(如果您之前从未设置过架构版本,那么这个版本号设置为 0)
config.schemaVersion = 1;

// 设置闭包,这个闭包将会在打开低于上面所设置版本号的 Realm 数据库的时候被自动调用
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
  // 目前我们还未进行数据迁移,因此 oldSchemaVersion == 0
  if (oldSchemaVersion < 1) {
    // 什么都不要做!Realm 会自行检测新增和需要移除的属性,然后自动更新硬盘上的数据库架构
  }
};

// 告诉 Realm 为默认的 Realm 数据库使用这个新的配置对象
[RLMRealmConfiguration setDefaultConfiguration:config];

// 现在我们已经告诉了 Realm 如何处理架构的变化,打开文件之后将会自动执行迁移
[RLMRealm defaultRealm];

我们最起码需要做的,是使用一个空的闭包来更新版本,以表明这个架构已经被 Realm 升级(自动)完毕。

值的更新

虽然这个迁移操作是最精简的了,但是我们需要让这个闭包能够自行计算新的属性(这里指的是 fullName),这样才有意义。 在迁移闭包中,我们能够调用[RLMMigration enumerateObjects:block:] 来枚举特定类型的每个 RLMObject 对象,然后执行必要的迁移逻辑。注意,对枚举中每个已存在的 RLMObject 实例来说,应该是通过访问 oldObject 对象进行访问,而更新之后的实例应该通过 newObject 进行访问:

// 在 [AppDelegate didFinishLaunchingWithOptions:] 中进行配置

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
  // 目前我们还未进行数据迁移,因此 oldSchemaVersion == 0
  if (oldSchemaVersion < 1) {
    // enumerateObjects:block: 方法遍历了存储在 Realm 文件中的每一个“Person”对象
    [migration enumerateObjects:Person.className
                          block:^(RLMObject *oldObject, RLMObject *newObject) {

      // 将名字进行合并,存放在 fullName 域中
      newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
                                         oldObject[@"firstName"],
                                         oldObject[@"lastName"]];
    }];
  }
};
[RLMRealmConfiguration setDefaultConfiguration:config];

一旦迁移成功结束,Realm 文件和其中的所有对象都可被您的应用正常访问。

属性重命名

在迁移过程中对类中某个属性进行重命名操作,比起拷贝值和保留关系来说要更为高效。

要在迁移过程中对某个属性就进行重命名的话,请确保您的新模型当中的这个属性是一个全新的名字,它的名字不能和原有模型当中的名字重合。

如果新的属性拥有不同的可空性或者索引设置的话,这些配置会在重命名操作期间生效。

下面是一个例子,展示了您该如何将 PersonyearsSinceBirth 属性重命名为 age 属性:

// 在您的 [AppDelegate didFinishLaunchingWithOptions:] 当中使用

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
  // 我们目前还没有迁移任何东西,因此 oldSchemaVersion == 0
  if (oldSchemaVersion < 1) {
  	 // 重命名操作应该在调用 `enumerateObjects:` 之外完成
    [migration renamePropertyForClass:Person.className oldName:@"yearsSinceBirth" newName:@"age"];
  }
};
[RLMRealmConfiguration setDefaultConfiguration:config];

添加更多版本

假如说现在我们有两个先前版本的 Person 类:

// v0
// @interface Person : RLMObject
// @property NSString *firstName;
// @property NSString *lastName;
// @property int age;
// @end

// v1
// @interface Person : RLMObject
// @property NSString *fullName; // 新属性
// @property int age;
// @end

// v2
@interface Person : RLMObject
@property NSString *fullName;
@property NSString *email;   // 新属性
@property int age;
@end

我们的迁移闭包里面的逻辑大致如下:

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 2;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
  // enumerateObjects:block: 遍历了存储在 Realm 文件中的每一个“Person”对象
  [migration enumerateObjects:Person.className
                        block:^(RLMObject *oldObject, RLMObject *newObject) {
    // 只有当 Realm 数据库的架构版本为 0 的时候,才添加 “fullName” 属性
    if (oldSchemaVersion < 1) {
      newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
                                oldObject[@"firstName"],
                                oldObject[@"lastName"]];
    }

    // 只有当 Realm 数据库的架构版本为 0 或者 1 的时候,才添加“email”属性
    if (oldSchemaVersion < 2) {
      newObject[@"email"] = @"";
    }
  }];
};
[RLMRealmConfiguration setDefaultConfiguration:config];

// 现在我们已经成功更新了架构版本并且提供了迁移闭包,打开旧有的 Realm 数据库会自动执行此数据迁移,然后成功进行访问
[RLMRealm defaultRealm];

要了解关于数据架构迁移如何实现的更完整信息,请参考我们的迁移例程应用

线性迁移(Linear Migrations)

假如说,我们的应用有两个用户: JP和Tim。

JP经常更新应用,但Tim却经常跳过某些版本。

所以JP可能下载过这个应用的每一个版本,并且一步一步地跟着更新构架:第一次下载更新后,数据库架构从v0更新到v1;第二次架构从v1更新到v2……以此类推,井然有序。

相反,Tim很有可能直接从v0版本直接跳到了v2版本。

因此,您应该使用非嵌套的 if (oldSchemaVersion < X) 结构来构造您的数据库迁移模块,以确保无论用户在使用哪个版本的架构,都能完成必需的更新。

当您的用户不按套路出牌,跳过有些更新版本的时候,另一种情况也会发生。

假如您在v2里删掉了一个“email”属性,然后在v3里又把它重新引进了。假如有个用户从v1直接跳到v3,那 Realm 不会自动检测到v2的这个删除操作,因为存储的数据构架和代码中的构架吻合。

这会导致 Tim 的 Person 对象有一个 v3 的 email 属性,但里面的内容却是 v1 的。

这个看起来没什么大问题,但是假如两者的内部存储类型不同(比如说: 从 ISO email 标准格式变成了自定义格式),那麻烦就大了。为了避免这种不必要的麻烦,我们推荐您在 if (oldSchemaVersion < 3) 语句中,清空所有的 email 属性。

加密(Encryption)

Realm 的加密 API 目前支持 iOS、OS X 以及 WatchKit 平台,但 不支持 watchOS 平台,因为 Realm 加密机制使用的 <mach/mach.h> 以及 <mach/exc.h> API 被标记为__WATCHOS_PROHIBITED` 了。

我们针对这个问题提交了一个 radar 请求:rdar://22063654

translation missing: cn.documentation.cocoa.alert

Realm 支持在创建 Realm 数据库时采用64位的密钥对数据库文件进行 AES-256+SHA2 加密。

// 产生随机密钥
NSMutableData *key = [NSMutableData dataWithLength:64];
(void)SecRandomCopyBytes(kSecRandomDefault, key.length, (uint8_t *)key.mutableBytes);

// 打开加密文件
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.encryptionKey = key;
NSError *error = nil;
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
if (!realm) {
     // 如果密钥错误,`error` 会提示数据库不可访问
    NSLog(@"Error opening realm: %@", error);
}

// 和往常一样使用 Realm 即可
RLMResults<Dog *> *dogs = [Dog objectsInRealm:realm where:@"name contains 'Fido'"];

这样硬盘上的数据都能都采用AES-256来进行加密和解密,并用 SHA-2 HMAC 来进行验证。 每次您要获取一个 Realm 实例时,您都需要提供一次相同的密钥。

我们的加密例程应用展示了如何产生密钥并将其安全地存放到钥匙串当中,然后用其加密 Realm。

加密过的 Realm 只会带来很少的额外资源占用(通常最多只会比平常慢10%)。

测试与调试

配置默认的 Realm 数据库

使用和测试 Realm 应用的最简单方法就是使用 默认的 Realm 数据库了。 为了避免在测试中覆盖了应用数据或者泄露,您只需要为每项测试将默认的 Realm 数据库设置为新文件即可。

// 一个基本的测试类,每个使用 Realm 进行的测试都应当继承自该类,而不是直接继承自 XCTestCase 类
@interface TestCaseBase : XCTestCase
@end

@implementation TestCaseBase
- (void)setUp {
  [super setUp];

  // 使用当前测试名标识的内存 Realm 数据库。
  // 这确保了每个测试都不会从别的测试或者应用本身中访问或者修改数据,并且由于它们是内存数据库,因此无需对其进行清理。
  RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
  config.inMemoryIdentifier = self.name;
  [RLMRealmConfiguration setDefaultConfiguration:config];
}
@end

注入(injecting) Realm 实例

另一种测试使用 Realm 代码的方式是让所有您打算进行测试的方法以参数的方式获取 RLMRealm 实例,这样您就可以在应用运行和测试时将不同的 Realm 文件传递进去。 例如,假设您的应用拥有一个从 JSON API 中获取用户配置文件的 GET 方法,然后您想要对其进行测试,确保本地配置文件能够正常创建:

// 应用中使用的代码

@implementation ClassBeingTested
+ (void)updateUserFromServer {
  NSURL *url = [NSURL URLWithString:@"http://myapi.example.com/user"];
  [[[NSURLSession sharedSession] dataTaskWithURL:url
                               completionHandler:^(NSData *data,
                                                   NSURLResponse *response,
                                                   NSError *error) {
    [self createOrUpdateUserInRealm:[RLMRealm defaultRealm] withData:data];
  }] resume];
}

+ (void)createOrUpdateUserInRealm:(RLMRealm *)realm withData:(NSData *)data {
  id object = [NSJSONSerialization JSONObjectWithData:data options:(NSJSONReadingOptions)nil error:nil];
  [realm transactionWithBlock:^{
    [User createOrUpdateInRealm:realm withValue:object];
  }];
}
@end

// 测试中使用的代码

@implementation UnitTests
- (void)testThatUserIsUpdatedFromServer {
  RLMRealm *testRealm = [RLMRealm realmWithURL:kTestRealmURL];
  NSData *jsonData = [@"{\"email\": \"[email protected]\"}"
                      dataUsingEncoding:NSUTF8StringEncoding];
  [ClassBeingTested createOrUpdateUserInRealm:testRealm withData:jsonData];
  User *expectedUser = [User new];
  expectedUser.email = @"[email protected]";
  XCTAssertEqualObjects([User allObjectsInRealm:testRealm][0],
                        expectedUser,
                        @"用户信息未能从服务器正常更新");
}
@end

调试

调试您的 Realm 应用是非常简单的,借助 LLDB 以及 Realm浏览器 的帮助来实时查看您应用中的数据。

我们的 Xcode 插件带有 LLDB 脚本,可以在 Xcode 的用户界面中检查已管理的 RLMObjectRLMResult 以及 RLMArray 对象,而不只是简单地显示 nil 或者 0

Xcode截图

注意: 这一功能只支持Objective‑C。Swift的支持版本仍在计划中。

避免在测试目标中将 Realm 数据库和测试代码相链接

如果您以动态框架的方式运行 Realm 的话,那么 您需要确保单元测试目标能否正确识别 Realm。您可以通过向您单元测试的 “Framework Search Paths” 中添加 Realm.framework 的上级目录来完成此功能。

如果您的测试失败提示消息为 "Object type '...' not persisted in Realm",那么这很可能是因为您直接将 Realm 框架直接链接到单元测试目标上了,这恰恰是应该避免的。从您的测试目标中解除 Realm 的链接就可以解决这个问题。

同样地,您应当确保您的数据模型类文件只在您的应用或者框架目标中进行了编译,千万不要将它们置入到您的单元测试目标当中。否则,这些类会在测试的过程中被复制,这往往会为调试问题时带来麻烦(可参考此问题 了解更多信息)。

您也要确保所有您需要进行测试的代码能够供单元测试目标访问(需要使用 public 访问修饰符或者 @testable标识符)。关于更多信息,请参阅这个 Stack Overflow 回答。

当前版本的限制

这个列表列出了目前我们最常见的一些限制情况。

如果您想要查看完整的问题列表,请参阅 GitHub issues

基本的限制

Realm 致力于平衡数据库读取的灵活性和性能。为了实现这个目标,在 Realm 中所存储的信息的各个方面都有基本的限制。例如:

  1. 类名称的长度最大只能存储 57 个 UTF8 字符。
  2. 属性名称的长度最大只能支持 63 个 UTF8 字符。
  3. NSData 以及 NSString 属性不能保存超过 16 MB 大小的数据。如果要存储大量的数据,可通过将其分解为16MB 大小的块,或者直接存储在文件系统中,然后将文件路径存储在 Realm 中。如果您的应用试图存储一个大于 16MB 的单一属性,系统将在运行时抛出异常。
  4. 每个单独的 Realm 文件大小无法超过应用在 iOS 系统中所被允许使用的内存量——这个量对于每个设备而言都是不同的,并且还取决于当时内存空间的碎片化情况(关于此问题有一个相关的 Radar:rdar://17119975)。如果您需要存储海量数据的话,那么可以选择使用多个 Realm 文件并进行映射。
  5. 对字符串进行排序以及不区分大小写查询只支持“基础拉丁字符集”、“拉丁字符补充集”、“拉丁文扩展字符集 A” 以及”拉丁文扩展字符集 B“(UTF-8 的范围在 0~591 之间)。

线程

尽管 Realm 文件可以被多个线程同时访问,但是您不能直接跨线程传递 Realms、Realm 对象、查询和查询结果。如果您需要跨线程传递 Realm 对象的话,您可以使用 RLMThreadSafeReference API。有关 Realm 线程的更多知识,可以参阅线程一节。

Realm 对象的 Setter 以及 Getter 不能被重载

因为 Realm 在底层数据库中重写了 setters 和 getters 方法,所以您不可以在您的对象上再对其进行重写。 一个简单的替代方法就是:创建一个新的 Realm 忽略属性,该属性的访问起可以被重写, 并且可以调用其他的 getter 和 setter 方法。

文件大小以及版本跟踪

一般来说 Realm 数据库比 SQLite 数据库在硬盘上占用的空间更少。如果您的 Realm 文件大小超出了您的想象,这可能是因为您数据库中的 RLMRealm 中包含了旧版本数据。

为了使您的数据有相同的显示方式,Realm 只在循环迭代开始的时候才更新数据版本。这意味着,如果您从 Realm 读取了一些数据并进行了在一个锁定的线程中进行长时间的运行,然后在其他线程进行读写 Realm 数据库的话,那么版本将不会被更新,Realm 将保存中间版本的数据,但是这些数据已经没有用了,这导致了文件大小的增长。这部分空间会在下次写入操作时被重复利用。这些操作可以通过设置 shouldCompactOnLaunch 或者通过调用 writeCopyToPath:error: 来实现。

为了避免这个问题,您可以调用invalidate,来告诉 Realm 您不再需要那些拷贝到 Realm 的数据了。这可以使我们不必跟踪这些对象的中间版本。在下次出现新版本时,再进行版本更新。

您可能在 Realm 使用Grand Central Dispatch时也发现了这个问题。在 dispatch 结束后自动释放调度队列(dispatch queue)时,调度队列(dispatch queue)没有随着程序释放。这造成了直到 RLMRealm`` 对象被释放后,Realm 中间版本的数据空间才会被再利用。为了避免这个问题,您应该在 dispatch 队列中,使用一个显式的自动调度队列(dispatch queue)。

Realm 没有自动增长属性

Realm 没有线程/进程安全的自动增长属性机制,这在其他数据库中常常用来产生主键。然而,在绝大多数情况下,对于主键来说,我们需要的是一个唯一的、自动生成的值,因此没有必要使用顺序的、连续的、整数的 ID 作为主键。

在这种情况下,一个独一无二的字符串主键通常就能满足需求了。一个常见的模式是将默认的属性值设置为 [[NSUUID UUID] UUIDString] 以产生一个唯一的字符串 ID。

自动增长属性另一种常见的动机是为了维持插入之后的顺序。在某些情况下,这可以通过向某个 RLMArray 中添加对象,或者使用 [NSDate date] 默认值的 createdAt 属性。

使用 Realm API 来构造 Swift 属性

如果您打算编写 Swift 应用的话,那么对于 Swift 应用的类和结构体当中所定义的属性而言,其值可以使用 Realm API 来进行构造。例如:

class SomeSwiftType {
  let persons = RLMPerson.allObjects(in: RLMRealm.default())
  // ...
}

如果您使用类似的属性来定义相关的类型的话,您需要注意:如果在 Realm 配置操作完成之前,就去调用这些新构造的类型很可能会导致问题的发生。

例如,如果您在 applicationDidFinishLaunching() 当中对默认的 Realm 配置操作设置了迁移代码,但是您又在 applicationDidFinishLaunching() 运行之前创建了 SomeSwiftType 的实例,这个时候您的 Realm 正在进行迁移操作,这样您就会在 Realm 被正确配置之前对原有的 Realm 进行了访问。

为了避免此类现象的发生,您可以选择:

  1. 推迟初始化任何用到 Realm API 属性的类型,直到应用完成 Realm 配置。
  2. 使用 Swift 的 lazy 关键字来定义相关属性。这允许您能够在应用生命周期的任一时段安全地构建相关的类型,只要您不要在应用完成 Realm 配置之前尝试去访问这些惰性属性即可。
  3. 仅使用明确获取用户定义配置的 Realm API 来构造属性。这样,您就能确保您所使用的配置能够在打开 Realm 之前正确完成设置。

加密的 Realm 不能同时被多个进程访问

这其中包括了 iOS 应用扩展。要解决这个问题,请使用未加密的 Realm,这样才能够跨进程共享。您可以使用 Security 和 CommonCrypto 系统框架来对 Realm 对象存储在 NSData 属性当中的数据进行加密。

我们正在跟踪以解决这方面的限制,包括了 Realm Cocoa 问题追踪(#1693) 以及 Realm 核心问题追踪(#1845)。

-[NSPredicate evaluateWithObject:] 无法使用 Realm 集合,因为它们是非集合对象

由于在 NSPredicate 的内部实现中存在一些过度约束的检查,因此某些 NSPredicate 的 API 与 Realm 的集合类型并不兼容。例如,在谓词子查询试图对 Realm 集合进行迭代操作时,-[NSPredicate evaluateWithObject:] 将会抛出异常。

Apple 已经明确了这个问题:(rdar://31252694)。

如果您需要在应用中解决此问题的话,可以将 PR #4770 中的补丁集成进去,在执行任何谓词赋值 (predicate evaluation) 操作之前,调用一次 RLMWorkaroundRadar31252694()

小技巧

我们将一些小技巧集中在一起,展示如何使用 Realm 来完成一些特定的任务。我们将会定期添加这些小技巧,因此请时常回顾。如果您有想看到的示例的话,请在 Github 上开启一个 issue。

同步

Realm 移动端平台 (Mobile Platfom) 对 Realm 移动端数据的功能进行了扩展,从而使跨设备间的自动数据同步成为了可能。为了实现这个功能,我们新增了一系列类型和类来支持这些 Realm 文件的同步操作;这些新增的内容将会被添加到既有的 Realm 移动端数据库当中,以下是其相关的介绍。

用户

Realm 对象服务器当中的核心对象便是与可同步 Realm 数据库相关联的 Realm 用户 (RLMSyncUser) 了。User 可以通过用户名/密码或者通过多种第三方身份验证来完成共享 Realm 数据库的身份验证。

创建用户和用户登录需要完成两件事情:

  • 需要能连接到 Realm 对象服务器的 URL;
  • 用于进行验证的证书,能够描述用户适合使用该机制(比如说:用户名/密码、访问密钥等)。

创建服务器 URL

认证服务器 URL 简单地由 NSURL 所表示,它代表了 Realm 对象服务器的地址。

NSURL *serverURL = [NSURL URLWithString:@"http://my.realmServer.com:9080"];

欲了解关于服务器 URL 的更多内容,请参阅认证文档

认证

认证 (authentication) 用于建立用户的合法身份,并完成登录操作。请参阅我们的 Realm 对象服务器认证文档,来了解 Realm 移动端平台所支持的认证方式。

某个给定用户的_证书信息_表示的是 RLMSyncCredentials 的值,可以通过以下几种方式来构建:

  • 提供有效的用户名/密码组合
  • 提供从受支持的第三方身份验证服务那里所获取的令牌
  • 提供令牌以及自定义的身份验证提供器(参见自定义身份认证

用户名和密码认证是完全由 Realm 对象服务器所管理的,这样可以让您完全控制应用中的用户管理操作。对于其他身份验证方法而言,您的应用将负责登录外部服务,并获取身份验证令牌。

以下是一些不同的认证方式的配置,通过配置证书来实现。

用户名/密码
RLMSyncCredentials *usernameCredentials = [RLMSyncCredentials credentialsWithUsername:@"username"
                                                                             password:@"password"
                                                                             register:NO];

注意到这个这工厂方法要获取一个布尔值参数,从而决定是否允许此新用户完成注册,或者是否允许此用户登录。如果新用户所注册的用户名与现有用户发生冲突的话,那么就会报错,如果用户登录的用户名不存在的话,那么也会记录下来。

Google
RLMSyncCredentials *googleCredentials = [RLMSyncCredentials credentialsWithGoogleToken:@"Google token"];
Facebook
RLMSyncCredentials *facebookCredentials = [RLMSyncCredentials credentialsWithFacebookToken:@"Facebook token"];
Apple CloudKit
RLMSyncCredentials *cloudKitCredentials = [RLMSyncCredentials credentialsWithCloudKitToken:@"CloudKit token"];
自定义身份认证

Realm 对象服务器支持使用第三方身份认证提供器。这允许用户能够使用遗留数据库或者 API 进行身份验证,也支持集成 Realm 对象服务器目前不支持的提供器。关于如何编写自定义身份验证提供器的相关信息,请参阅对象服务器手册的这一部分

RLMSyncCredentials *credentials = [[RLMSyncCredentials alloc] initWithCustomToken:@"custom token" provider:@"myauth" userInfo:nil];

附加信息可以通过第三个参数传递给自定义证书构造器当中。关于这方面的详细信息,请参阅 API reference

用户认证

要创建用户的话,调用 +[RLMSyncUser logIn] 即可。

这个工厂方法将会构造一个用户,然后通过异步操作登录到 Realm 对象服务器上,如果登录成功,那么就通过一个回调闭包提供一个可用的用户对象,以供后续使用。如果登录失败的话,那么回调闭包当中传递的就将是一个错误对象。

[RLMSyncUser logInWithCredentials:usernameCredentials
                    authServerURL:serverURL
                     onCompletion:^(RLMSyncUser *user, NSError *error) {
  if (user) {
    // can now open a synchronized RLMRealm with this user
  } else if (error) {
    // handle error
  }
}];

Realm 移动端平台允许应用同时存在多个独立用户。

试想,我们可能需要实现一个支持多账户的 e-mail 客户端应用。这样的话多个用户可以在任意指定的时间完成认证,如果要获取当前可用的所有活跃用户的话,那么可以使用 +[RLMSyncUser all] 来获取。

注销

如果用户想要退出他们的账户,那么您可以调用 -[RLMSyncUser logOut] 方法。所有未决的本地更改会继续与对象服务进行完全同步。随后,所有的本地已同步数据都将从设备上清除掉。

管理用户

currentUser 方法用于检索当前用户,也就是最后登录并且证书尚未过期的那个用户。如果找不到当前用户的话,这个方法就会返回 nil。如果存在多个已登录用户的话,将会抛出异常。

RLMSyncUser *user = [RLMSyncUser currentUser];

如果存在多个用户登录的情况,您可以使用 allUsers 来获取对应的用户对象字典。

NSDictionary<NSString *, RLMSyncUser *> *allUsers = [RLMSyncUser allUsers];

如果没有用户登录的话,返回的字典内容将为空。

管理员用户

管理员用户是 Realm 对象服务器中定义的用户,具备对 ROS 实例上所有的 Realm 数据库的访问、管理权限。要知道用户是否是_管理员_用户,可以查看 RLMSyncUser.isAdmin 属性,它也能反映出用户上次成功登录时的状态。

[RLMSyncUser logInWithCredentials:usernameCredentials
                    authServerURL:serverURL
                     onCompletion:^(RLMSyncUser *user, NSError *error) {
  if (user) {
    // 如果用户是 ROS 实例上的管理员的话,
    // 那么这个用户可以打开一个可同步 Realm
    NSLog(@"User is admin: %@", @(user.isAdmin));
  } else if (error) {
    // handle error
  }
}];

打开可同步的 Realm 数据库

您可以使用相同的 RLMRealmConfiguration 来创建可同步 Realm 数据库,此外也可以使用相应的工厂方法来创建独立的 Realm 数据库。

可同步 Realm 数据库必须要在 RLMRealmConfiguration 当中配置 RLMSyncConfiguration 类型的 syncConfiguration 属性。相应的,RLMRealmConfiguration 可以通过 RLMSyncUser 以及表示可同步 Realm 数据库位置的 Realm URL 来进行构建。这个 URL 或许可能会包含波浪线 (~),它将用来表示用户的唯一标识符。

例如,假设以下都代表了相应的 Realm URL,表示了一个名为 “Widgets” 的 Realm 数据库,并且每个用户都有对应的版本:realms://acme.example.com/~/widgets。假设有一个用户的唯一标识符为 5917268。那么这个 “Widgets” Realm 数据库的 Realm URL 就会扩展为:realms://acme.example.com/5917268/widgets。假设另一个用户的唯一标识符为 8581230。那么对应的 Realm URL 就会扩展为:realms://acme.example.com/8581230/widgets。这允许应用能够自动根据用户的标识符来构建相应的 URL,这样就不用去担心不同用户的 URL 该如何去管理了。

注意: URL 不能以 “.realm” 文件扩展名结尾。URL 应该包含名称的主要部分(例如上例中的 “widgets”),这样 Realm 将创建所有相关的文件和文件夹来存储数据。

可同步 Realm 数据库不能够配置 inMemoryIdentifier 或者 fileURL。一旦对这两个属性进行配置,都会自行导致 syncConfiguration 属性配置为 nil。这时候框架将会负责将数据库缓存或者存储到磁盘上。

下面这个例子展示了可同步 Realm 数据库是如何通过给定的用户对象和 Realm URL 完成开启操作的:

RLMSyncUser *user;

// Create the configuration
NSURL *syncServerURL = [NSURL URLWithString: @"realm://localhost:9080/~/userRealm"];
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.syncConfiguration = [[RLMSyncConfiguration alloc] initWithUser:user realmURL:syncServerURL];

// Open the remote Realm
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];
// Any changes made to this Realm will be synced across all devices!

同步会话Sync Sessions

可同步 Realm 数据库与 Realm 对象服务器之间的连接是通过 RLMSyncSession 对象来表示的。可以通过表示用户的 RLMSyncUser 对象,使用 +[RLMSyncUser allSessions] 或者 -[RLMSyncUser sessionForURL:] API 来获取相关的会话 (session) 对象,表示由特定用户所打开的 Realm 数据库连接。

基本操作

底层会话的状态可以通过 state 属性检索得到,这样便可以检查会话是处于激活状态、离线状态还是异常状态。

如果会话处于激活状态的话,那么 configuration 属性当中将包含有 RLMSyncConfiguration 值,可用于打开 Realm 数据库的另一个实例(比如说在不同的线程上执行此操作)。

进度通知

会话 (session) 对象允许您的应用监视向 Realm 对象服务器上传或者下载的会话状态,只需要在会话对象上注册_进度通知闭包 (progress notification block)_即可。

最初注册线程 Runloop 上的同步子系统将会定期调用进度通知闭包。如果没有 Runloop 存在,那么就会自行创建一个。实际上,这意味着您可以在这里 GCD 的后台队列中注册这些闭包,并且这些闭包也可以正常工作。可以在会话对象上同时注册多个所需的闭包。闭包可以配置为报告上传进度,也可以配置为报告下载进度。

每次当闭包被调用之后,它就会接收到当钱已经传输的字节数,以及可传输字节的总量(其定义为:已传输的字节数 + 等待传输的字节数)。

当闭包注册之后,注册方法便会返回一个令牌对象 (token)。在令牌上调用 -stop 方法可以停止对此闭包的监听和通知。如果该闭包已经被注销掉,那么调用 stop 方法将不会做任何事情。注意,注册方法很有可能会返回空的令牌,特别是当通知闭包永远不会再次运行的时候(例如,会话处于致命错误状态,或者进度已经完成或无法完成)。

还存在两种类型的闭包。首先,闭包可以配置为_无限期通知 (report indefinitely)_。这些闭包将永远保持激活状态,除非用户明确声明停止这些闭包的活动,此外这些闭包将始终通知最新的可传输字节数。这类闭包可以用于控制网络指示器之类的 UI,例如在上传或者下载操作正在进行时所进行的 UI 颜色变化和 UI 出现等操作。

RLMSyncSession *session = [[RLMSyncUser currentUser] sessionForURL:realmURL];
void(^workBlock)(NSUInteger, NSUInteger) = ^(NSUInteger downloaded, NSUInteger downloadable) {
  if ((double)downloaded/(double)downloadable >= 1) {
    [viewController hideActivityIndicator];
  } else {
    [viewController showActivityIndicator];
  }
};
RLMProgressNotificationToken *token;
token = [session addProgressNotificationForDirection:RLMSyncProgressDirectionDownload
                                                mode:RLMSyncProgressReportIndefinitely
                                               block:workBlock];

// Much later...
[token stop];

闭包也可以被配置为_为当前未完成的工作而报告进度_。这些闭包在注册之后,捕获当前已传输字节,并会还会汇报相应的进度百分比。一旦已传输字节数达到或者超过所设定的初始值,那么闭包将会自动注销自身。这种类型的闭包可以用于追踪下载进度的进度条,比如说当用户登录时下载可同步 Realm 数据库时的操作,这样就可以让用户知道本地副本更新需要耗费多少时间。

RLMSyncSession *session = [[RLMSyncUser currentUser] sessionForURL:realmURL];
RLMProgressNotificationToken *token;
void(^workBlock)(NSUInteger, NSUInteger) = ^(NSUInteger uploaded, NSUInteger uploadable) {
  double progress = ((double)uploaded/(double)uploadable);
  progress = progress > 1 ? 1 : progress;
  [viewController updateProgressBarWithFraction:progress];
  if (progress == 1) {
    [viewController hideProgressBar];
    [token stop];
  }
};
token = [session addProgressNotificationForDirection:RLMSyncProgressDirectionUpload
                                                mode:RLMSyncProgressForCurrentlyOutstandingWork
                                               block:workBlock];

访问控制

Realm 移动端平台提供了灵活的访问控制机制,从而可以指定哪些用户可以访问此 Realm 文件并进行同步。这个功能可以用于多人协作类的应用当中,其中有多个用户同时向同一个 Realm 写入数据。此外也可以用于在发布者/订阅者场景当中的数据共享,其中某个用户拥有写入权限,而其他用户只拥有读取权限。

对于指定 Realm 数据库而言,有三个主要的访问级别(权限):

  • mayRead 表示允许用户从 Realm 中读取数据;
  • mayWrite 表示允许用户向 Realm 中写入数据;
  • mayManage 表示允许用户修改 Realm。

除非明确指明权限,否则只有 Realm 数据库的所有者才能够访问。唯一的例外是管理员用户:他们将拥有服务器上所有 Realm 数据库的所有权限。

欲了解更多关于访问控制方面的内容,请参见 Realm 对象服务器文档中的访问控制一节。

可管理的 Realm 数据库

所有访问级别的管理操作都可以通过写入可管理的 Realm 数据库 (Management Realm) 来执行。可管理的 Realm 数据库可以像常规可同步 Realm 文件那样进行读写操作。然而,Realm 对象服务器会专门为此 Realm 数据库当中所做的更改做出相应的反应。权限变更对象可以添加到此 Realm 数据库当中,从而达到修改 Realm 文件的访问控制设置的目的。

要获取某个指定用户的可管理 Realm 数据库,可以调用 -[RLMSyncUser managementRealmWithError:] 方法。

权限修改

如果要修改 Realm 文件的访问控制设置的话,向可管理的 Realm 数据库 中添加 RLMSyncPermissionChange 对象即可。

要修改 Realm 文件的访问控制设置的话,可以通过以下两种方法来执行:PermissionChange_以及_PermissionOffer/Response

PermissionChange

RLMSyncPermissionChange 对象允许您通过向可管理 Realm 数据库中写入可修改对象,来直接控制可同步 Realm 数据库的访问设置。

RLMSyncPermissionChange *permissionChange = [RLMSyncPermissionChange permissionChangeWithRealmURL:realmURL    // The remote Realm URL on which to apply the changes
                                                                                           userID:anotherUser // The user ID for which these permission changes should be applied
                                                                                             read:@YES        // Grant read access
                                                                                            write:@YES        // Grant write access
                                                                                           manage:@NO];       // Grant management access

[managementRealm transactionWithBlock:^{
  [managementRealm addObject:permissionChange];
}];

要对用户所管理的所有 Realm 应用权限变更的话,需要将 realmURL 的值指定为 *。要对所有通过对象服务器授权的同步用户应用权限变更的话,需要将 userID 的值指定为 *

一旦对象服务器执行了编码在 RLMSyncPermissionChange 对象当中的操作,它就会设置该对象的 statusstatusMessage 属性。

要获取权限变更操作的通知结果,只需要像其他 Realm 对象那样对执行变更的对象进行观察即可(例如,使用 KVO 或者集合通知)。关于这方面的详细信息,请参阅通知

以下示例演示了如何用权限变更操作的状态来通知应用:

token = [permissionChange addNotificationBlock:^(BOOL deleted,
                                                 NSArray<RLMPropertyChange *> *changes,
                                                 NSError *error) {
  switch (permissionChange.status) {
    case RLMSyncManagementObjectStatusNotProcessed:
      break; // handle case
    case RLMSyncManagementObjectStatusSuccess:
      break; // handle case
    case RLMSyncManagementObjectStatusError:
      break; // handle case
  }
  NSLog(@"%@", permissionChange.statusMessage); // contains error or informational message
}];
PermissionOffer/Response

RLMSyncPermissionOfferRLMSyncPermissionOfferResponse 这两个类允许您在用户之间共享 Realm 数据库,这两个类全部属于客户端的 API——因此无需任何的服务器代码配合。要共享 Realm 的话涉及以下步骤:

  1. 在用户间共享管理的 Realm 数据库当中创建一个 RLMSyncPermissionOffer 对象。
  2. 等待 Realm 对象服务器对 offer 进行同步处理,这会对 offer 对象的 token 属性进行填充。
  3. 通过任意一种方法,将令牌发送给其他用户。
  4. 令牌接收者在其管理的 Realm 当中创建一个 RLMSyncPermissionOfferResponse 对象。
  5. 等待服务器同步和处理 response,这会对 response 对象的 realmUrl 属性进行填充。
  6. 令牌接收者现在就可以访问 URL 上的共享 Realm 数据库了。
RLMSyncPermissionOffer *shareOffer = [RLMSyncPermissionOffer permissionOfferWithRealmURL:realmURL
                                                                               expiresAt:nil
                                                                                    read:YES
                                                                                   write:YES
                                                                                  manage:YES];

// 添加到管理 Realm 数据库当中
[managementRealm transactionWithBlock:^{
  [managementRealm addObject:shareOffer];
}];

// 等待 offer 处理完成
RLMNotificationToken *shareOfferNotificationToken = [shareOffer addNotificationBlock:^(BOOL deleted, NSArray<RLMPropertyChange *> *changes, NSError *error) {
  if (deleted == NO && error == nil && shareOffer.status == RLMSyncManagementObjectStatusSuccess && shareOffer.token) {
    // 给其他用户发送 `token`
  }
}];

权限交换类似,通过 readwritemanage 参数可以对所提供的 realmURL 中的 Realm 数据库访问权限进行空值。expiresaT 参数将控制令牌何时过期。如果您没有将任何值传递给 expiresAt 的话,或者传递了 nil,那么 offer 将永远不会过期。请注意,处理 offer 令牌的用户在令牌过期后_不会_丧失访问权限。

一旦用户取得了令牌,它们就可以使用它来创建一个 RLMSyncPermissionOfferResponse 对象:

// 根据接收到的令牌来创建 response
RLMSyncPermissionOfferResponse *response = [RLMSyncPermissionOfferResponse permissionOfferResponseWithToken:token];

// 向管理 Realm 数据库中添加 response
[managementRealm transactionWithBlock:^{
  [managementRealm addObject:response];
}];

// 等待服务器处理
RLMNotificationToken *acceptShareNotificationToken = [response addNotificationBlock:^(BOOL deleted, NSArray<RLMPropertyChange *> *changes, NSError *error) {
  if (deleted == NO && error == nil && response.status == RLMSyncManagementObjectStatusSuccess && response.realmUrl) {
    // 用户现在可以访问 `response.realmUrl` 处的 Realm 数据库了
  }
}];

Permissions granted by RLMSyncPermissionOffer 所授予的权限是累积的:如果用户已经具备了 write 访问权限,然后在被授予 read 权限,那么它是不会丢失写入权限的。

通过从管理 Realm 数据库中删除 RLMSyncPermissionOffer 对象,或者将 expiresAt 属性设置为以前的日期,就可以撤销用户的权限。这将阻止新用户接收该 offer,但是_不会_撤销之前使用过的用户权限。

日志

同步子系统支持多种日志记录级别,在应用开发过程当中是非常有帮助的。可以通过对 RLMSyncManager 单例上的 logLevel 属性进行设置,从而选择所需的日志记录详尽度:

Realm 支持多种日志级别,我们可以通过同步管理器 (Sync Manager) 来进行配置选择,如下所示:

[[RLMSyncManager sharedManager] setLogLevel:RLMSyncLogLevelOff];

在任何可同步 Realm 数据库被打开之前,就必须设置日志级别。在首次可同步 Realm 数据库打开之后,对日志级别的修改将不起任何作用。

关于日志级别的更多信息,请参阅 Realm 对象服务器配置文档

错误报告

某些与同步相关的 API 在执行异步操作时可能会发生失败的情况。这些 API 将会包含有一个 completion 闭包,从而可以接受错误参数;如果错误参数被传递进去则说明操作失败。这样便可以通过检查错误以获取更详细的信息。

我们同时强烈建议RLMSyncManager 单例配置一个错误处理器。涉及全局同步子系统 (global synchronization subsystem) 的错误或者某些特定的会话(表示所打开的用于与服务器同步的 Realm 会话)将会通过此错误处理器进行报告。当错误发生之后,错误处理器就会被调用,因此便可以通过表示错误的对象来进行处理,此外还有表示发生错误会话的 RLMSyncSession 对象(如果适用的话)。

[[RLMSyncManager sharedManager] setErrorHandler:^(NSError *error, RLMSyncSession *session) {
  // handle error
}];

错误

Realm 移动端平台的错误由 NSError 对象表示,其 domain 为 RLMSyncErrorDomain。 请参考 RLMSyncError 以及 RLMSyncAuthError 的定义来查找错误代码以及其相关的含义。

客户端重置

如果 Realm 对象服务器发生了崩溃并且需要进行备份恢复的话,那么应用可能需要对给定的可同步 Realm 数据库实施_客户端重置 (client reset)_操作。当所操作的 Realm 本地版本号比服务器上的 Realm 版本号大的时候,那么就需要执行这个操作(例如,当 Realm 对象服务器备份之后应用执行了变更操作,但是这些操作并没有在服务器崩溃并恢复之前同步回服务器上)。

客户端重置的步骤如下所示:创建本地 Realm 文件的备份拷贝,然后将本地的 Realm 文件删除。接下来应用连接到 Realm 对象服务器并打开服务器上的 Realm 数据库,从而下载最新的数据库拷贝。而在 Realm 对象服务器备份完成之后、但还未同步回服务器的数据将保留在 Realm 的备份拷贝当中,但是当 Realm 重新下载过程当中所操作的数据将不会被保留。

是否需要进行客户端重置,将由发送给 RLMSyncManager 错误处理器中的错误所标示出来。这个错误将由 RLMSyncErrorClientResetError 所表示。

错误对象当中还包含有两个值:客户端重置操作执行时所备份的 Realm 文件位置,以及一个没有任何参数的密闭闭包,用以初始化客户端重置进程。

如果调用该闭包来手动初始化客户端重置进程的话,所有的 Realm 实例在闭包调用之前都必须先无效化并销毁。请注意,RLMRealm 可能不会完全无效化,即使所有该对象的引用都会 nil,除非包含该对象的自动释放池被释放掉。但是,这样做将会导致 Realm 在客户端重置进程完成之后被重新打开,从而使同步恢复执行。

如果闭包没有被调用的话,那么客户端重置进程将会在下一次应用启动之后自动执行,此外在第一次访问 RLMSyncManager 单例的时候也会调用。这时候需要应用来保存备份副本的位置,以便稍后可以找到备份副本。

需要重置的 Realm 仍然可以进行读写操作,但是所有的变更都不会同步到服务器上,除非客户端重置操作已经完成并且 Realm 文件也重新下载完毕。这一点非常重要,您的应用需要监听客户端重置所产生的错误,为了以防万一,当客户端重置触发之后,最少还需要保存所创建、修改的用户数据,这样才能够在 Realm 的副本重新下载完毕之后将这些保存掉的数据重新写回到数据库当中。

备份副本的文件路径可以通过调用 NSError 对象的 -[NSError rlmSync_clientResetBackedUpRealmPath] 来获取得到。此外也可以直接从 userInfo 字典中通过 kRLMSyncPathOfRealmBackupCopyKey 键来提取得到。

重置初始化闭包可以通过调用 NSError-[NSError rlmSync_clientResetBlock] 来获取得到。此外也可以直接从 userInfo 字典中通过 kRLMSyncInitiateClientResetBlockKey 键来提取得到。 key.

下例展示了该如何使用客户端重置 API 来实现客户端重置操作:

[[RLMSyncManager sharedManager] setErrorHandler:^(NSError *error, RLMSyncSession *session) {
  if (error.code == RLMSyncErrorClientResetError) {
    [realmManager closeRealmSafely];
    [realmManager saveBackupRealmPath:[error rlmSync_clientResetBackedUpRealmPath]];
    [error rlmSync_clientResetBlock]();
    return;
  }
  // Handle other errors...
}];

关于 Realm 对象服务器是如何处理客户端重置的更多信息,请参阅我们的服务器文档

同步迁移

可同步 Realm 数据库支持自动迁移,但是当数据库架构发生变更时,还需要将架构版本号进行递增。

目前,仅支持增量更改,例如新增类或者新增属性。自定义的迁移闭包将会被忽略。

迁移 会自动应用于可同步 Realm 数据库,但是也存在一些限制和注意事项:

  • 增量修改,例如增加类或者向某个既有类添加新的属性,将会自动执行迁移。
  • 从架构中移除属性将不会真正从数据库删除该字段,而是让 Realm 数据库忽略该属性。新的对象将会继续以这些属性创建,但是被删除属性的值将被设置为 null。不可空的字段将被适当地设置为 0 或者空值:数字字段将被设置为 0,字符串属性将被设置为空字符串等等。
  • 自定义迁移闭包无法在可同步 Realm 数据库迁移中使用,如果使用的话会抛出异常。
  • 破坏性更改——也就是对 Realm 架构进行了更改,需要借助代码才能实现的迁移更改——将不被直接支持。这包括属性的类型更改(例如从可空字符串类型更改为不可空字符串类型),更改主键或者将字段从可选值设置为不可空(反之亦然)。

对可同步 Realm 数据库进行自定义迁移,可以通过在客户端中编写一个通知处理器,也可以在服务器上使用 Node.js SDK 中使用 JavaScript 函数(如果您所使用的对象服务器支持的话)。然而,如果迁移发生了破坏性变化,那么 Realm 将停止与 ROS 进行同步,以防止产生”接收到错误的修改集”错误。

如果您对可同步 Realm 数据库迁移中执行了破坏性更改,那么您应该使用新的架构来创建一个新的可同步 Realm 数据库,然后创建一个迁移函数,它通过检测旧 Realm 数据库的更改情况,然后将值复制到新的 Realm 数据库当中。与非破坏性迁移类似,这段代码可以放在新版本的客户端当中(具备新架构版本的),也可以放在服务器端的 Node.js 代码当中。

将本地 Realm 数据库转换为可同步 Realm 数据库

目前,还不支持将本地(不可同步)的 Realm 数据库自动转换为可同步 Realm 数据库。我们计划在未来添加对此项功能的支持。

如果您希望将本地 Realm 数据库转换为可同步 Realm 数据库的话,您需要建立一个新的可同步 Realm 数据库,然后手动将本地 Realm 数据库当中的对象完全复制到可同步 Realm 数据库当中。

冲突处理

关于冲突处理的更多内容,请参阅Realm 对象服务器文档

问答时间到!

Realm 的库文件有多大?

一般而言,Realm 只会给应用的下载大小增加 5 到 8 MB 左右。我们的发布版本可能会更大一些,这是因为其中还包含了对 iOS、watchOS 以及 tvOS 模拟器的支持库、某些调试符号以及 bitcode,此外还有某些当编译应用时会被 Xcode 自动排除的中间代码。

Realm 开源了吗?

没错!Realm 内部 C++ 存储引擎以及其不同语言封装的 SDK 现已基于 Apache 2.0 协议开源!

不过,Realm 还包含了一个闭源的同步组件,但是如果您将 Realm 作为嵌入式数据库使用的话,这个组件是不必要的。

在我运行应用的时候发现了一个名为 Mixpanel 的网络调用,这是什么?

当您的应用处于调试模式运行,或者在模拟器当中运行的时候,Realm 将会收集匿名的统计信息。这些分析结果都是匿名的,并通过收集 Realm、iOS 、macOS 的版本信息、您使用的语言,以及您目前使用的 Xcode 版本信息从而帮助我们更好地改进产品。这个调用不会在实际应用上运行,也不会在用户设备上运行 - 只有在模拟器中或者处于调试状态下时,才会运行。您可以看到我们是如何进行收集的,也可以查看我们所收集的内容。其原理可以在我们的源码中查看。

疑难解答

崩溃报告

我们建议您在应用中使用崩溃报告 (crash reporter)。很多 Realm 操作都可能会在运行时发生崩溃(就如同其他硬盘 I/O 操作一样),因此从应用中收集这些崩溃报告可以帮助您(或者我们)发现出错的具体位置,从而更好地进行错误处理以及修复相关的问题。

绝大多数商用的崩溃报告都有收集日志的选项。我们强烈建议您开启此功能。Realm 在抛出异常或者处于无法恢复的状况时将会记录元数据 (metadata) 信息(但不会记录用户数据),这些信息在出错的时候对调试有很大帮助。

报告 Realm 错误

如果您发现了 Realm 中的任何错误,请 在 Github 上提交一个 issue,也可以给 help@realm.io 发邮件信息。请尽可能多地给我们发送相关的错误信息,这样可以帮助我们更好的理解并解决您提出的问题。

下面信息对我们来说将十分有用:

  1. 您的目的
  2. 您所期望的结果
  3. 实际结果
  4. 产生此结果的步骤
  5. 突出此问题的代码示例 (完整的 Xcode 项目的话我们可以自行编译,更好理解)
  6. Realm / Xcode / OS X 的版本
  7. 依赖库管理器的版本(CocoaPods / Carthage)
  8. 出现 Bug 的平台, OS 版本及架构(例如 64-bit iOS 8.1)
  9. 崩溃日志以及堆栈轨迹 (stack traces),参见上方的 崩溃报告 了解更多信息。

通过依赖管理器 (Dependency Manager) 重新安装

如果您曾经通过 CocoaPods 或者 Carthage 安装过 Realm,并且遇到了编译错误的话,那么很可能是您使用了该依赖管理器所不支持的版本,也可能是 Realm 没有成功整合到项目当中,还可能是构建工具中遗留有缓存。

如果这样的话,请尝试删除依赖管理器所创建的这些文件夹,并重新安装。

此外,您还可以尝试删除 Derived Data,然后清除 Xcode 的构建文件夹;这可以修复因构建工具版本更新或者项目配置发生变更(例如增加新的 Target,跨 Target 之间共享依赖等等)而导致的问题。

要清除构建文件夹的话,按住 “Option” 键然后打开 “Product” 菜单,随后选择 “Clean Build Folder…“。您也可以在 Xcode 帮助菜单的搜索栏中键入 “Clean”,然后在搜索结果当中选中出现的 “Clean Build Folder…” 菜单项。

CocoaPods

Realm 可以使用 CocoaPods 0.39.0 或者更高版本进行安装。

如果您在使用 CocoaPods 进行集成的过程中遇到了问题,那么重置集成状态或许可以帮助您解决这个问题。为了实现此操作,只需要在终端中,进入到项目根目录中并运行以下命令:

pod cache clean Realm
pod cache clean RealmSwift
pod deintegrate || rm -rf Pods
pod install --verbose
rm -rf ~/Library/Developer/Xcode/DerivedData

您还可以使用 [cocoapods-deintegrate] (https://github.com/CocoaPods/cocoapods-deintegrate),而不是选择直接删除 Pods 文件夹。对于 CocoaPods 1.0 而言,这属于预安装插件。如果您使用的 CocoaPods 版本较老的话,那么可以使用 gem install cocoapods-deintegrate 来安装这个插件。您可以使用 pod deintegrate 来运行这个插件。它将从您的 Xcode 项目中移除 CocoaPods 的所有痕迹。

Carthage

Realm 可以通过 Carthage 0.9.2 以及更高版本进行安装。

要从项目中移除所有由 Carthage 管理的依赖关系,只需要在终端中,进入到项目根目录中并运行以下命令:

rm -rf Carthage
rm -rf ~/Library/Developer/Xcode/DerivedData
carthage update

Realm 核心二进制库下载失败

当 Realm 构建的时候,其过程还包含了下载静态库形式的核心库,然后将其整合到 realm-cocoa 项目当中。

已经有报告指出,在某些情况下,核心二进制库会下载失败,并报以下错误:

Downloading core failed. Please try again once you have an Internet connection.

导致此错误的原因可能有以下几点:

  1. 您的 IP 地址处于 美国禁止区域 列表列出的地区当中。为了遵循美国的相关法律,Realm 无法给在此地区中使用。欲了解更多信息,请参阅我们的 许可证
  2. 您位于中国大陆,由于 GFW 的原因,此时无法正常访问 CloudFlare 或者 Amazon AWS S3 服务。请参见这个 Realm-Cocoa 问题 了解更多信息。
  3. Amazon AWS S3 可能会遇到服务问题,请检查 AWS Service Health Dashboard,稍后再试。

在低内存的限制下运行 Realm

如果您想要在内存受限的环境下使用 Realm,例如 watchOS 应用或者应用扩展,那么我们建议您明确将类指定由 Realm 进行管理,这样可以避免 objc_copyClassList() 的额外调用。

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.objectClasses = @[Dog.class, Person.class];
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];