
Realm的崩溃,猝不及防,不仅仅是Realm,任何数据库导致的奔溃总是个难题,总有那么零星几个让人没有头绪的bug。
本文提供了一个思路来解决Realm数据库崩溃问题
时间:2020年4月28日
代码部分见重点内容,Java等其他平台也可参考。 目前数据库崩溃率大概是:几十万分之一吧
谨记以下几点:
- Realm的数据写入是同步阻塞的,但是读取不会阻塞
- Realm托管的对象是不可以跨线程的,即不同线程是不可以修改彼此的对象的
- Realm托管的对象的任何修改必须是在realm.write{} 中完成的
- Realm 采用了 零拷贝 架构。
- 尽量少使用写入事件少量事件,可以尝试批量写入更多数据
- 将写入操作载入到专门的线程中执行。
- 推迟初始化任何用到 Realm API 属性的类型,直到应用完成 Realm 配置。否则会崩溃。
官方明确的限制:
- 类名称的长度最大只能存储 57 个 UTF8 字符。
- 属性名称的长度最大只能支持 63 个 UTF8 字符。
- Data 和 String 属性不能保存超过 16 MB 大小的数据
- 每个单独的 Realm 文件大小无法超过应用在 iOS 系统中所被允许使用的内存量——这个量对于每个设备而言都是不同的,并且还取决于当时内存空间的碎片化情况(关于此问题有一个相关的 Radar:rdar://17119975)。如果您需要存储海量数据的话,那么可以选择使用多个 Realm 文件并进行映射。
- 对字符串进行排序以及不区分大小写查询只支持“基础拉丁字符集”、“拉丁字符补充集”、“拉丁文扩展字符集 A” 以及”拉丁文扩展字符集 B“(UTF-8 的范围在 0~591 之间)。
Realm中多线程中的问题
一、跨线程修改数据
条件一: 线程A创建了对象xiaoming,并托管到realm中
条件二: 同时线程B创建了对象xiaomei,并托管到realm中
问:此时,我能从线程A中的直接修改线程B中创建的xiaomei吗?
不可以,对象一旦托管到realm中,修改其他线程中的Realm对象会导致崩溃
二、跨线程传输
官方实例:
let person = Person(name: "Jane")
try! realm.write {
realm.add(person)
}
let personRef = ThreadSafeReference(to: person)
DispatchQueue(label: "background").async {
autoreleasepool {
let realm = try! Realm()
guard let person = realm.resolve(personRef) else {
return // person 被删除
}
try! realm.write {
person.name = "Jane Doe"
}
}
}
Realm 提供了一个机制,通过以下三个步骤来保证受到线程限制的实例能够安全传递:
- 通过受到线程限制的对象来构造一个 ThreadSafeReference;
- 将此 ThreadSafeReference 传递给目标线程或者队列;
- 通过在目标 Realm 上调用 Realm.resolve(_:) 来解析此引用。
三、摆脱Realm数据托管,自由修改对象
这时我想修改xiaoming,假如把年龄从29 修改到了 28,我不希望立刻存到数据库中,因为我不确定 年龄为28是否正确,我想要临时修改,不让数据库托管,这时候怎么办?
答案是: 深拷贝 + 主键更新
import Foundation
import RealmSwift
/// Int、String、Float、Double、Bool、Date、Data、
/// List<Object>、List<Int>、List<String>、List<Float>、
/// List<Double>、List<Bool>、List<Date>、List<Data>等全部支持
protocol DetachableObject: AnyObject {
func detached() -> Self
}
extension Object: DetachableObject {
func detached() -> Self {
let detached = type(of: self).init()
for property in objectSchema.properties {
guard let value = value(forKey: property.name) else {
continue
}
if let detachable = value as? DetachableObject {
detached.setValue(detachable.detached(), forKey: property.name)
} else { // Then it is a primitive
detached.setValue(value, forKey: property.name)
}
}
return detached
}
}
extension List: DetachableObject {
func detached() -> List<Element> {
let result = List<Element>()
forEach {
if let detachableObject = $0 as? DetachableObject,
let element = detachableObject.detached() as? Element {
result.append(element)
} else { // Then it is a primitive
result.append($0)
}
}
return result
}
}
Int、String、Float、Double、Bool、Date、Data、List、List、List、List、List、List、List、List等全部支持
四、如何实现不同线程 使用不同的Realm
fileprivate init() {
_realm = try Realm(configuration: ······
}
fileprivate var _realm: Realm?
public var realm: Realm? {
get {
if Thread.isMainThread {
return _realm ?? (try? Realm())
} else {
return try? Realm()
}
}
}
五、上面的这个看似解决了问题,实际上会存在很大隐患。
项目中大部分relam奔溃的元凶就是这个了。
如主线程通过realm得到了xiaoming,子线程中获取的realm实例以及xiaoming和主线程是不同的,但这时在写入事件中对xiangming进行操作,还是会崩溃.
解决办法:
- 所有托管的对象利用上文提到的ThreadSafeReference,统一写入。
缺点:ThreadSafeReference 对象最多只能够解析一次。如果 ThreadSafeReference 解析失败的话,将会导致 Realm 的原始版本被锁死,直到引用被释放为止。因此,ThreadSafeReference 的生命周期应该很短。
- 对象的读写都确保在同一线程(包含realm实例,以及对象)
我建议获取realm实例和对象读写,放在同一线程中,那么如何保证同一线程呢?
- 优先级低的数据操作考虑:GCD的异步串行队列 会开辟一条新的线程,可以利用这一点
- 优先级高的数据操作考虑:主线程
重点内容:
深拷贝,主键更新
废话不多说,见代码:
import Foundation
import RealmSwift
class RealmManager{
static let shared = RealmManager()
private init() {
_realmMain = try? Realm()
}
private var _realmMain: Realm?
public var realm: Realm? {
get {
if Thread.isMainThread {
return _realmMain ?? (try? Realm())
} else {
return try? Realm()
}
}
}
/// 查询,返回的对象托管到realm中
func objects<Element: Object>(_ type: Element.Type) -> [Element] {
var result = [Element]()
realm!.objects(type).forEach { (element) in
result.append(element)
}
return result
}
/// 查询,返回的对象托管到realm中
func object<Element: Object, KeyType>(ofType type: Element.Type, forPrimaryKey key: KeyType) -> Element? {
return realm!.object(ofType: type, forPrimaryKey: key)
}
// MARK: - Safe operation 安全操作
/// 安全查询,返回的对象不托管到realm中
func safeQuery<Element: Object>(_ type: Element.Type) -> [Element] {
var result = [Element]()
realm!.objects(type).forEach { (element) in
result.append(element.detached())
}
return result
}
/// 安全查询,返回的对象不托管到realm中
func safeQuery<Element: Object, KeyType>(ofType type: Element.Type, forPrimaryKey key: KeyType) -> Element? {
return realm!.object(ofType: type, forPrimaryKey: key)?.detached()
}
/// 安全写入数据,保证不会出错
func safeWrite<T>(object:T) where T:Object {
let newRealm = realm
/// 深拷贝
let obj = object.detached()
if T.primaryKey() == nil{
// 删除老数据,然后更新
newRealm?.delete(object)
try? newRealm?.write {
newRealm?.add(obj)
}
}else{
// 通过主键更新
try? newRealm?.write {
newRealm?.add(obj, update: .all)
}
}
}
}
上述代码中,我分别实现了
- 普通查询
- 安全查询
- 普通写入
- 安全写入
如果程序员能保证线程安全,使用普通查询,普通写入,不能保证时,至少实现一种安全操作,不必全部查询写入都使用安全操作。
还可以进行优化,如,只要是主线程获取的数据,做个标记,不去操作,不用进行深拷贝,写入时,直接操作。
六、其他注意事项
一、绕过App Store 出现的提交 bug
在应用目标的 “Build Phases” 中创建一条新的 “Run Script Phase”,然后将下面这段代码粘贴到脚本文本框内:
bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework/strip-frameworks.sh"
二、多种数据库初始化的情况
1、内存中
let realm = try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: "MyInMemoryRealm"))
创建一个完全在内存中运行的 Realm 数据库 (in-memory Realm),它将不会存储在磁盘当中.
2、一般情况下,包含版本升级
do {
_realm = try Realm(configuration: Realm.Configuration(schemaVersion: schemaVersion, migrationBlock: { migration, oldSchemaVersion in
// 数据库迁移
if (oldSchemaVersion < 2) {
// 添加新字段
migration.enumerateObjects(ofType: RealmTPPlanItemModel.className()) { oldObject, newObject in
newObject?["timeStyle"] = "EEEE"
}
}
}))
}catch{
TPLog.log(error.localizedDescription)
}
3、打包进项目里的数据库的使用
public var appConfigureRealm: Realm? {
let config = Realm.Configuration(
fileURL: Bundle.main.url(forResource: "defaultAPP", withExtension: "realm"), readOnly: true, schemaVersion:2)
// 通过配置打开 Realm 数据库
let realm = try! Realm(configuration: config)
return realm
}
七、深入理解 Realm 的多线程处理机制
此文就是你想知道的:数据库的设计:深入理解 Realm 的多线程处理机制
加V备注:掘金;入群一起学习🐻
