#はじめに
去年からずっと悩んでいたRealmSwiftのエラーが最近、解決しました。
#原因
タイトルにもあるように不正なスレッドからレルムにアクセスしたこと
が原因です。
Realmのデータベース(以下、DB)では、異なるスレッド間でのアクセスは保証されていますが、スレッドを跨いでアクセスするのは禁止されています。
let realm = Realm() // メインスレッドで生成
DispatchQueue.global().async {
// メインスレッドとは異なるスレッド
realm.write {} // Realm accessed from incorrect thread.
}
#沼った部分
自分はずっとRealmインスタンスだけが、スレッド間を跨いでアクセスしなければOKだと思っていて、その思い込みのせいで沼にハマってしまいました。
解決の糸口は、Realm入門という書籍にありました。
#Realmの制約
Realmインスタンスは初期化した際にスレッドID
を内部で保持し、DBのアクセス時に初期化の時のスレッドIDと現在のスレッドIDを比較して異なるスレッドIDならエラーを吐く仕様になっています。
でこれはなにもRealmインスタンスに限った話ではありませんでした。
Realmではマネージドオブジェクト・アンマネージドオブジェクト
があって、Realmインスタンスやモデル定義(テーブル定義)する際に継承するObject、クエリが実行された時に返ってくるResults等がマネージドオブジェクトに該当します。
このマネージドオブジェクトでは必ず、内部でRealmインスタンスを持っており、値にアクセスする時はスレッドチェック
が行われます。
そして、注意点としてObjectを継承したモデルオブジェクトは元々、アンマネージドオブジェクトでRealm.add()する事によってマネージドオブジェクトになります
。
// モデル定義
class HogeObject: Object {
// 何らかの値
}
let realm = try! Realm() // メインスレッドで生成
let hogeObj = HogeObject() // この時点ではアンマネージドオブジェクト
try! realm.write {
// メインスレッドのRealmに保存している
realm.add(hogeObj) // add()することによってマネージドオブジェクトになる
}
let results = realm.objects(HogeObject.self) // メインスレッドのrealmから生成
DispatchQueue.global().async {
// メインスレッドとは異なるスレッド
// 各インスタンスはメインスレッドで生成しているので
// ここでアクセスするとエラーが発生する
print(hogeObj) // Realm accessed from incorrect thread.
print(results) // Realm accessed from incorrect thread.
realm.write { } // Realm accessed from incorrect thread.
}
以上を踏まえて自分のコードを読んでみるとマネージドオブジェクトになったモデルオブジェクトを別スレッドで使っていた
ことが判明しました。
だからずっとエラーが消えなかったんですね。
因みにアンマネージドオブジェクトはスレッドを跨いでもOK
です。
#解決方法①
Realmでは、異なるスレッド間でオブジェクトを渡せるようにThreadSafeReferenceが用意されていて本来であれば、これで解決します。
let realm = try! Realm() // メインスレッドで生成
let hogeObj = HogeObject()
try! realm.write {
realm.add(hogeObj)
}
// マネージドオブジェクトのhogeObjを渡し、
// ThreadSafeReferenceインスタンスを生成する
let ref = ThreadSafeReference(to: hogeObj) // hogeObjへのスレッドセーフな参照を持つ
DispatchQueue.global().async {
// メインスレッドとは異なるスレッド
let realm = try! Realm() // サブスレッドで生成
// 定数refはhogeObjへのスレッドセーフな参照を持つ
// resolve()によって、サブスレッドのrealmインスタンスからhogeObjへの参照を解決(取り出す)ことが出来る
guard let resolved = realm.resolve(ref) else {
return
}
// マネージドオブジェクトにアクセスできるようになる
print(resolved) // HogeObject
}
ただ今回は特殊ケースで、保存後・削除後のモデルオブジェクトを一時的に保持してそれを別スレッドで使います
。
保存後のモデルオブジェクトであれば、ThreadSafeReferenceでどうにかなりましたが、削除後のモデルオブジェクトはDBから削除されて無効なものになり、モデル定義したプロパティにアクセスするとエラーになります
。
なのでThreadSafeReferenceは参照できません。
let realm = try! Realm()
let hogeObj = HogeObject()
try! realm.write {
realm.delete(hogeObj)
}
// 無効なオブジェクトで参照できないのでエラーになる
let ref = ThreadSafeReference(to: hogeObj) // Cannot construct reference to invalidated object
しかも、無効になった後もマネージドオブジェクトのままです。
#解決方法②
ではどうしたのかというと、会社の先輩を頼りました。
どうやって解決されたかというと、元々あるモデルオブジェクトとは別にObjectを継承していないモデルオブジェクトを定義して
この問題を回避していました。
// Objectを継承していないモデルオブジェクト
struct HogeObject {
var hoge: String
init() {
let obj = HogeRealmObject()
self.hoge = obj.hoge
}
// Realmオブジェクトに変換する
func toRealmObject() -> HogeRealmObject {
let obj = HogeRealmObject()
obj.hoge = self.hoge
return obj
}
// Objectを継承しているモデルオブジェクト
class HogeRealmObject: Object {
@objc dynamic var hoge: String = ""
}
}
let realm = try! Realm()
var hogeObj = HogeObject()
hogeObj.hoge = "hogehoge"
// Realmのオブジェクトに変換してDBに追加する
let realmObj = hogeObj.toRealmObject()
try! realm.write {
realm.add(realmObj)
}
DispatchQueue.global().async {
// マネージドオブジェクトではないので別スレッドでもアクセス可能
print(hogeObj.hoge) // "hogehoge"
}
以上のやり方だと、保存後・削除後とか関係なく値にアクセスできるし、かつスレッド間を跨ぐことも出来ます。
#終わりに
とりあえず、年明け前の悩みが消えて良かったです。