4

🐻如何降低Realm数据库的崩溃

 2 years ago
source link: https://juejin.cn/post/6844904143501557773
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

🐻如何降低Realm数据库的崩溃



171c095277c0ece7~tplv-t2oaga2asx-watermark.awebp

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进行操作,还是会崩溃.

解决办法:
  1. 所有托管的对象利用上文提到的ThreadSafeReference,统一写入。

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

  1. 对象的读写都确保在同一线程(包含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备注:掘金;入群一起学习🐻


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK