Notes

Realm

打开一个 Realm

后台刷新

在 iOS 8 后,若使用了后台刷新的特性,并且涉及了 Realm, 则需要注意了

App 内的文件在设备锁屏下,会使用 NSFileProtection 自动加密,对 Realm 数据库进行打开操作会抛出异常

因此,需要降低 Realm 数据库文件和它的辅助文件的保护等级,同时,也可以选择使用 Realm 自带的加密方式

降低保护等级,从相关文件的上一层文件夹入手

let realm = try! Realm()

// Get our Realm file's parent directory
let folderPath = realm.configuration.fileURL!.deletingLastPathComponent().path

// Disable file protection for this directory
try! FileManager.default.setAttributes([FileAttributeKey(rawValue: NSFileProtectionKey): NSFileProtectionNone],
                                       ofItemAtPath: folderPath)

打开一个默认 Realm

let realm = try! Realm()

设置 Realm 的配置

Realm(configuration: config)

// 设置默认的配置
Realm.Configuration.defaultConfiguration = config

错误处理

在实践中,发生异常的时刻总是会在第一次在某一个线程中,创建一个 Realm 实例。随后,在相同的线程中访问 Realm 实例总会成功,因为会复用缓存的实例

捕获异常,使用 Swift 的捕获机制就好

do {
  let realm = try Realm()
} catch let error as NSError {
  // handle error
}

Realm 的文件

压缩 Realm

Realm 的架构意味着,它的文件大小会比它最后包含的数据大小要大,这与 Realm 的高性能,并行和安全的优点有关

为了避免在扩充 Realm 文件时经常调用耗时的系统操作,Realm 文件在运行时默认不会压缩

当然,可以通过 Configuration 来控制压缩

let config = Realm.Configuration(shouldCompactOnLaunch: { totalBytes, usedBytes in
    // totalBytes refers to the size of the file on disk in bytes (data + free space)
    // usedBytes refers to the number of bytes used by data in the file

    // Compact if the file is over 100MB in size and less than 50% 'used'
    let oneHundredMB = 100 * 1024 * 1024
    return (totalBytes > oneHundredMB) && (Double(usedBytes) / Double(totalBytes)) < 0.5
})
do {
    // Realm is compacted on the first open if the configuration block conditions were met.
    let realm = try Realm(configuration: config)
} catch {
    // handle error compacting or opening Realm
}

要注意的是:

删除 Realm 文件

autoreleasepool {
    // all Realm usage here
}
let realmURL = Realm.Configuration.defaultConfiguration.fileURL!
let realmURLs = [
    realmURL,
    realmURL.appendingPathExtension("lock"),
    realmURL.appendingPathExtension("note"),
    realmURL.appendingPathExtension("management")
]
for URL in realmURLs {
    do {
        try FileManager.default.removeItem(at: URL)
    } catch {
        // handle error
    }
}

数据模型 Model

定义 Model

使用 Swift 定义的 Realm 的数据模型是通过类属性来实现,像普通 Swift 类一样使用,只要继承了 Object 类,或者另一个数据模型类

限制

主要的限制就是,数据模型的实例,只能在创建它的线程中使用

Model 的定义必须合法

Realm 将会在代码运行时,转换所有的 Model, 因此,它们必须是合法的,即使有些模型并没有使用到

所有必选项属性都需要有默认值

在 Swift 中使用 Realm, Swift.reflect(_:) 将会被调用来了解关于 Model 的信息,这就要求,Model 的 init() 方法必须能成功调用

支持的类型

CGFloat 不鼓励使用,因为这不是与平台无关的类型

String, Date, Data 类型的属性,可以是 optional 若要存储 optional 的数字类型,需要使用 RealmOptional

主键

定义一个主键可以提升查找和更新数据的性能,主键一旦设置了就不可以更改

定义主键直接覆盖 Object.primaryKey() 方法

class Person: Object {
    @objc dynamic var id = 0
    @objc dynamic var name = ""

    override static func primaryKey() -> String? {
        return "id"
    }
}

索引 Indexing properties

cons

pros

支持建立索引的数据类型

使用索引,直接覆盖 Object.indexedProperties() 方法

class Book: Object {
    @objc dynamic var price = 0
    @objc dynamic var title = ""

    override static func indexedProperties() -> [String] {
        return ["title"]
    }
}

忽略属性

若不想某些属性整合到 Realm, 就可以通过覆盖 Object.ignoreProperties() 方法

class Person: Object {
    @objc dynamic var tmpID = 0
    var name: String { // read-only properties are automatically ignored
        return "\(firstName) \(lastName)"
    }
    @objc dynamic var firstName = ""
    @objc dynamic var lastName = ""

    override static func ignoredProperties() -> [String] {
        return ["tmpID"]
    }
}

这些被忽略的属性

模型数据自动刷新

如果 UI 以来模型数据的话,可以订阅 Realm notifications 来监控数据的更新状态

集合类型

写操作

所有对模型数据的更改(添加,修改,删除),都必须在一个写事务中进行

通过模型数据类创建的实例,在添加到 Realm 之前,可以像普通的 Swift 类实例一样浪,但当添加到 Realm 时,必须通过一个写事务

更新数据

更新数据有两种方式

通过主键来更新数据

基于主键对数据进行更新或添加,调用 Realm().add(_:update:), 重点在于 update 这个参数

// Creating a book with the same primary key as a previously saved book
let cheeseBook = Book()
cheeseBook.title = "Cheese recipes"
cheeseBook.price = 9000
cheeseBook.id = 1

// Updating book with id = 1
try! realm.write {
    realm.add(cheeseBook, update: true)
}

通过这种方式更新数据,可以只传入需要更新的数据,而不是整个对象

// Assuming a "Book" with a primary key of `1` already exists.
try! realm.write {
    realm.create(Book.self, value: ["id": 1, "price": 9000.0], update: true)
    // the book's `title` property will remain unchanged.
}

对于没有定义主键的模型,不可以基于主键来添加或更新数据

查询

最简单的查询,返回所有数据

let dogs = realm.objects(Dog.self)

过滤

过滤调用方法 Results().filter(_:...)

过滤条件使用的是字符串,与 NSPredicate 大抵相同

// Query using a predicate string
var tanDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'")

// Query using an NSPredicate
let predicate = NSPredicate(format: "color = %@ AND name BEGINSWITH %@", "tan", "B")
tanDogs = realm.objects(Dog.self).filter(predicate)

排序

链式查询

分页读取

在其他的数据库中,通常使用 LIMIT 关键字限制一次读取的数据量,达到了分页的效果

而在 Realm 中,由于懒加载的特性,直接拿 Results 读取就可以了

// Loop through the first 5 Dog objects
// restricting the number of objects read from disk
let dogs = try! Realm().objects(Dog.self)
for i in 0..<5 {
    let dog = dogs[i]
    // ...
}

通知

可以注册一个监听器来接受通知,这些通知将会在 Realm 发生改变,或者 Realm 中的实体发生了变化是发出

当一个通知的 token 还在被强引用着时,通知就会发出,所以,当需要获取通知时,必须保持对 token 的强引用,否则,通知将会被取消订阅

通知将会在它们注册订阅者的线程上进行发送,并且这个线程必须要有一个 RunLoop - 如果在主线程外的线程订阅通知,则需要负责对该线程创建一个 RunLoop

通知的发送是异步的,发送时机在对应的写事务 commit 之后进行

由于同时是通过 RunLoop 来尽心传递,所以通知可能会被该 RunLoop 中的其他活动延迟。当通知不能被立即传递时,多个写事务中的变化可能会合并成一个通知

Realm 通知

通知可以被注册到一整个 Realm 中,每一次某个 Realm 中的写事务提交后,通知都会被传递

// Observe Realm Notifications
let token = realm.addNotificationBlock { notification, realm in
    viewController.updateUI()
}

// later
token.stop()

集合通知

集合通知中不会包含整个对应的 Realm, 但包含对数据变化的跟详细的叙述,如添加,删除,更改

集合通知也是异步传递,通知中的参数首先是最原始的结果,后面就是对添加,删除,更新数据的进一步说明,这些变化可以通过 RealmCollectionChange 参数进行获取,其中包括了 deletions, insertions, modifications

通知将会涵盖对实例属性的一切修改,以及一对一,一对多关系的变化,但不会包含反向关系的变化

class ViewController: UITableViewController {
    var notificationToken: NotificationToken? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        let realm = try! Realm()
        let results = realm.objects(Person.self).filter("age > 5")

        // Observe Results Notifications
        notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
            guard let tableView = self?.tableView else { return }
            switch changes {
            case .initial:
                // Results are now populated and can be accessed without blocking the UI
                tableView.reloadData()
            case .update(_, let deletions, let insertions, let modifications):
                // Query results have changed, so apply them to the UITableView
                tableView.beginUpdates()
                tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
                                     with: .automatic)
                tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
                                     with: .automatic)
                tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
                                     with: .automatic)
                tableView.endUpdates()
            case .error(let error):
                // An error occurred while opening the Realm file on the background worker thread
                fatalError("\(error)")
            }
        }
    }

    deinit {
        notificationToken?.stop()
    }
}

对象通知

Realm 支持对象级别的通知。这意味着,可以为某一个特定的 Realm 数据模型注册通知

当这个数据模型实例被删除之后,通知回调将不会在被调用

class StepCounter: Object {
    @objc dynamic var steps = 0
}

let stepCounter = StepCounter()
let realm = try! Realm()
try! realm.write {
    realm.add(stepCounter)
}
var token : NotificationToken?
token = stepCounter.addNotificationBlock { change in
    switch change {
    case .change(let properties):
        for property in properties {
            if property.name == "steps" && property.newValue as! Int > 1000 {
                print("Congratulations, you've exceeded 1000 steps.")
                token = nil
            }
        }
    case .error(let error):
        print("An error occurred: \(error)")
    case .deleted:
        print("The object was deleted.")
    }
}

界面驱动存储

Realm 中的通知总是异步传送的,所以并不会堵塞主线程。但在某些情况下,数据的变化需要在主线程中同步进行,并立即反应到 UI 上

其中一种情况就是为 table view 添加一个条目,同时需要为 table view 的数据源添加一条数据,此时,通知的异步在这里并不恰当,因为异步的通知反馈将会导致 App 崩溃,因为 table view 的条目与数据源中的数据不一致

此时,我们将使用 Realm.commitWrite(withoutNotifying:)

// Add fine-grained notification block
token = collection.addNotificationBlock { changes in
    switch changes {
    case .initial:
        tableView.reloadData()
    case .update(_, let deletions, let insertions, let modifications):
        // Query results have changed, so apply them to the UITableView
        tableView.beginUpdates()
        tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
                             with: .automatic)
        tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
                             with: .automatic)
        tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
                             with: .automatic)
        tableView.endUpdates()
    case .error(let error):
        // handle error
        ()
    }
}

func insertItem() throws {
     // Perform an interface-driven write on the main thread:
     collection.realm!.beginWrite()
     collection.insert(Item(), at: 0)
     // And mirror it instantly in the UI
     tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
     // Making sure the change notification doesn't apply the change a second time
     try collection.realm!.commitWrite(withoutNotifying: [token])
}

References