2017-08-30 91 views
0

我對我的項目和我的數據庫使用MagicalRecord我有CDSong實體,可以由多個CDVoter實體投票。 Database structureMagicalRecord數據庫與NSFetchedResultsController不一致

選民在後臺添加和刪除使用從串行調度隊列調用的NSManagedObjectContext.performAndWait(block:)。我有一個NSFetchedResultsController,它提取CD歌曲並顯示他們的選民(在這種簡單的情況下,它只打印選民的名字)。

一切都會好起來的,但偶爾會在NSFetchedResultsControllerDelegate的controllerDidChangeContent方法中收到崩潰: - 根據我的分析,似乎在CDSong.voters關係中出現了一些無效的空CDVoter(name = nil,votedSong = nil)對象。這些空選票不會從CDVoter.mr_findAll()返回。

這是模擬崩潰的代碼(通常在按鈕點擊應用程序崩潰後,因爲CDVoter的名稱爲零)。我的上下文是否有問題並保存?將整個測試代碼放在這裏用數據庫和frc初始化,如果有人想嘗試一下,但問題部分在controllerDidChangeContentbuttonPressed方法。感謝您的幫助:)

import UIKit 
import CoreData 
import MagicalRecord 

class MRCrashViewController : UIViewController, NSFetchedResultsControllerDelegate { 

    var frc: NSFetchedResultsController<NSFetchRequestResult>! 
    let dispatchQueue = DispatchQueue(label: "com.testQueue") 

    override func viewDidLoad() { 
     super.viewDidLoad() 

     self.initializeDatabase() 
     self.initializeFrc() 
    } 

    func initializeDatabase() { 

     MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.error) 
     MagicalRecord.setupCoreDataStack() 
     MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.warn) 

     if CDSong.mr_findFirst() == nil { 
      for i in 1...5 { 
       let song = CDSong.mr_createEntity()! 
       song.id = Int16(i) 
      } 
     } 
     NSManagedObjectContext.mr_default().mr_saveToPersistentStoreAndWait() 
    } 

    func initializeFrc() { 
     let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "CDSong") 
     fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)] 
     NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: nil) 
     self.frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: NSManagedObjectContext.mr_default(), sectionNameKeyPath: nil, cacheName: nil) 
     self.frc!.delegate = self 
     try! self.frc!.performFetch() 
    } 

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { 
     for song in controller.fetchedObjects! { 
      print((song as! CDSong).voters!.reduce("", { $0 + ($1 as! CDVoter).name! })) 
     } 
     print("----"); 
    } 

    @IBAction func buttonPressed(_ sender: Any) { 
     for _ in 1...10 { 
      self.dispatchQueue.async { 
       let moc = NSManagedObjectContext.mr_() 
       moc.performAndWait { 
        for song in CDSong.mr_findAll(in: moc)! { 
         let song = song as! CDSong 
         let voters = song.voters! 
         for voter in voters { 
          (voter as! CDVoter).mr_deleteEntity(in: moc) 
         } 

         for _ in 1...4 { 
          if arc4random()%2 == 0 { 
           let voter = CDVoter.mr_createEntity(in: moc)! 
           voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65))) 
           voter.votedSong = song 
          } 
         } 
        } 
        moc.mr_saveToPersistentStoreAndWait() 
       } 
      } 
     } 
    } 
} 

注: 我試圖用MagicalRecord.save(blockAndWait :)沒有成功。

+0

「選民使用串行調度隊列在後臺添加和刪除」。你應該**不要使用你自己的隊列**作爲coredata。你應該只使用NSManagedObjectContext的PerformBlock方法**。 – 2017-08-30 12:36:42

+0

@Sneak我使用performBlockAndWait方法來處理核心數據,但是我從一個bg隊列中調用它們。我認爲這是可以的,至少[docs](https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext)不會說我只能從主線程調用performBlockAndWait,他們只是說我應該調用方法來自我創建上下文的同一個線程。我必須使用bg隊列,因爲performBlockAndWait會阻止圖形,如果從主調用執行只會混合刪除和插入(相信我,只是試了一下)。謝謝,我將編輯句子 –

+0

刪除我的意見,這將解決您的問題,而不是打字。因爲你對「句子編輯」和「相信我」非常傲慢,而不是質疑我的觀點並試圖學習某些東西。祝你好運。 – 2017-08-30 13:53:46

回答

0

好的,所以我找到了崩潰的原因:儘管mr_saveToPersistentStoreAndWait一直等到更改保存到rootSavingContext中,但它不會等到它們合併到defaultContext中(如果它們是由專用隊列上下文創建的)。如果在主線程上下文合併主線程上的舊更改之前,rootSavingContext已被另一個保存更改,則合併被破壞(NSManagedObjectContextDidSave通知中的更改與MagicalRecord的rootContextDidSave:內部方法中的rootSavingContext的當前上下文狀態不對應)。

說明我所提出的解決方案:

  1. DatabaseSavingManager包含應用程序的專用隊列節能方面,其將被用於所有省(也許,如果你想使用多個保存上下文的缺點,但它的足夠滿足我的需求 - 保存發生在後臺並保持一致性)。正如@Sneak所評論的,沒有理由使用創建多個上下文並等待它們完成的後臺串行隊列(這正是我原來所做的),因爲NSManagedObjectContext有它自己的串行隊列,所以現在我使用了一個創建的上下文主線程,因此必須始終從主線程調用(使用perform(block:)以避免主線程阻塞)。

  2. 保存到持久性存儲後,保存上下文將等待來自defaultContext的NSManagedObjectContextObjectsDidChange通知,以便它知道defaultContext合併了更改。這就是爲什麼不允許使用DatabaseSavingManager的保存上下文的其他保存,因爲它們可能會混淆等待過程。

這裏是DatabaseSavingManager代碼:

import Foundation 
import CoreData 

class DatabaseSavingManager: NSObject { 
    static let shared = DatabaseSavingManager() 

    fileprivate let savingDispatchGroup = DispatchGroup() 
    fileprivate var savingDispatchGroupEntered = false 

    fileprivate lazy var savingContext: NSManagedObjectContext = { 
     if !Thread.current.isMainThread { 
      var context: NSManagedObjectContext! 
      DispatchQueue.main.sync { 
       context = NSManagedObjectContext.mr_() 
      } 
      return context 
     } 
     else { 
      return NSManagedObjectContext.mr_() 
     } 
    }() 

    override init() { 
     super.init() 
     NotificationCenter.default.addObserver(self, selector: #selector(defaultContextDidUpdate(notification:)), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: NSManagedObjectContext.mr_default()) 
    } 

    deinit { 
     NotificationCenter.default.removeObserver(self) 
    } 

    func save(block: @escaping (NSManagedObjectContext) ->()) { 
     guard Thread.current.isMainThread else { 
      DispatchQueue.main.async { 
       self.save(block: block) 
      } 
      return 
     } 

     let moc = self.savingContext 
     self.savingContext.perform { 
      block(self.savingContext) 
      self.saveToPersistentStoreAndWait() 
     } 
    } 

    func saveAndWait(block: @escaping (NSManagedObjectContext) ->()) { 
     if Thread.current.isMainThread { 
      self.savingContext.performAndWait { 
       block(self.savingContext) 
       self.saveToPersistentStoreAndWait() 
      } 
     } 
     else { 
      let group = DispatchGroup() 
      group.enter() 
      DispatchQueue.main.async { 
       self.savingContext.perform { 
        block(self.savingContext) 
        self.saveToPersistentStoreAndWait() 
        group.leave() 
       } 
      } 
      group.wait() 
     } 
    } 

    fileprivate func saveToPersistentStoreAndWait() { 
     if self.savingContext.hasChanges { 
      self.savingDispatchGroupEntered = true 
      self.savingDispatchGroup.enter() 
      self.savingContext.mr_saveToPersistentStoreAndWait() 
      self.savingDispatchGroup.wait() 
     } 
    } 

    @objc fileprivate func defaultContextDidUpdate(notification: NSNotification) { 
     if self.savingDispatchGroupEntered { 
      self.savingDispatchGroup.leave() 
      self.savingDispatchGroupEntered = false 
     } 
    } 
} 

而且例如如何使用它(無NSFetchedResultsController崩潰了,可以從任何線程調用,也很頻繁):

DatabaseSavingManager.shared.save { (moc) in 
     for song in CDSong.mr_findAll(in: moc)! { 
      let song = song as! CDSong 
      let voters = song.voters! 
      for voter in voters { 
       (voter as! CDVoter).mr_deleteEntity(in: moc) 
      } 

      for _ in 1...4 { 
       if arc4random()%2 == 0 { 
        let voter = CDVoter.mr_createEntity(in: moc)! 
        voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65))) 
        voter.votedSong = song 
       } 
      } 
     } 
    } 

當然,這當然不是最優雅的解決方案,只是我想到的第一個,所以其他方法是可喜的

+0

我很累通過你的答案,但你忘了提示,打開多線程調試oleb.net/blog/2014/06/core-data-concurrency-debugging,你會知道你什麼時候有線程問題。但是,快速瀏覽您的答案,在執行更多保存之前,您不需要等待通知。而且,你似乎仍在使用派遣組和隊列進行保存等。這仍然是一個不好的做法。這不是真的會給你造成一團糟,我認爲你真正的問題是你正在使用MR進行修改,而不是深入研究核心數據的真實工作方式。 – 2017-08-31 21:42:17

+1

爲了展示這一點,這實際上是你應該使用從iOS 10 + https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1845237-automaticallymergeschangesfrompa沒有用於雜亂手動跟蹤通知編碼和合並。 :) – 2017-08-31 21:42:50

相關問題