在 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)
let realm = try! 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.lock
与资源锁相关.realm.management
进程间通信的锁相关文件.realm.note
A named pipe for notifications?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
}
要注意的是:
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
}
}
使用 Swift 定义的 Realm 的数据模型是通过类属性来实现,像普通 Swift 类一样使用,只要继承了 Object
类,或者另一个数据模型类
主要的限制就是,数据模型的实例,只能在创建它的线程中使用
Realm 将会在代码运行时,转换所有的 Model, 因此,它们必须是合法的,即使有些模型并没有使用到
在 Swift 中使用 Realm, Swift.reflect(_:)
将会被调用来了解关于 Model 的信息,这就要求,Model 的 init()
方法必须能成功调用
Bool
Int
Int8
Int16
Int32
Int64
Double
Float
String
Date
Data
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"
}
}
cons
pros
支持建立索引的数据类型
String
Bool
Date
使用索引,直接覆盖 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 来监控数据的更新状态
Results
从查询中获取到的结果List
表示模型的”对多”关系LinkingObjects
表示模型间的反向关系RealmCollection
Realm 集合类型需要实现的一个协议AnyRealmCollection
一个无类型的类,用来向具体的集合类型传递方法调用所有对模型数据的更改(添加,修改,删除),都必须在一个写事务中进行
通过模型数据类创建的实例,在添加到 Realm 之前,可以像普通的 Swift 类实例一样浪,但当添加到 Realm 时,必须通过一个写事务
beginWrite()
时自动刷新,所以不会产生竞争条件???更新数据有两种方式
基于主键对数据进行更新或添加,调用 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.
}
对于没有定义主键的模型,不可以基于主键来添加或更新数据
Results
实例
Array
相同,不同之处在于,Results
中的元素类型都必须是一致的Results
中的数据并不是数据的一份副本,所以对 Results
中的数据进行修改(需要在写事务中),都会直接影响到持久性存储中的数据Results
中的数据将与最新数据保持同步最简单的查询,返回所有数据
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 中的写事务提交后,通知都会被传递
// 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])
}