【问题标题】:Problems saving NSManagedObjects on a background Context在后台上下文中保存 NSManagedObjects 的问题
【发布时间】:2019-05-28 23:56:52
【问题描述】:

我已经为此苦苦挣扎了好几天。我会很感激任何帮助。

我有一个位置NSManagedObject 和一个图像NSManagedObject,它们具有一对多的关系,即一个位置有许多图像。

我有 2 个屏幕,在第一个屏幕中,用户在视图上下文中添加位置,并且它们被添加和检索而没有问题。

现在,在第二个屏幕中,我想根据在第一个屏幕中选择的位置检索图像,然后在 Collection View 中显示图像。图像首先从 flickr 检索,然后保存在数据库中。

我想在背景上下文中保存和检索图像,这给我带来了很多问题。

  1. 当我尝试保存从 flickr 检索到的每张图像时,我收到一条警告,指出存在悬空对象并且无法建立关系:

这是我的保存代码:

  func saveImagesToDb () {

        //Store the image in the DB along with its location on the background thread
        if (doesImageExist()){
            dataController.backgroundContext.perform {

                for downloadedImage in self.downloadedImages {
                    print ("saving to context")
                    let imageOnMainContext = Image (context: self.dataController.viewContext)
                    let imageManagedObjectId = imageOnMainContext.objectID
                    let imageOnBackgroundContext = self.dataController.backgroundContext.object(with: imageManagedObjectId) as! Image

                    let locationObjectId = self.imagesLocation.objectID
                    let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location

                    let imageData = NSData (data: downloadedImage.jpegData(compressionQuality: 0.5)!)
                    imageOnBackgroundContext.image = imageData as Data
                    imageOnBackgroundContext.location = locationOnBackgroundContext


                    try? self.dataController.backgroundContext.save ()
                }
            }
        }
    }

正如您在上面的代码中所见,我正在根据从视图上下文中检索到的 ID 在后台上下文中构建 NSManagedObject。每次调用saveImagesToDb 我都会收到警告,那么问题是什么?

  1. 尽管有上面的警告,当我通过 FetchedResultsController(在后台上下文中工作)检索数据时。集合视图有时可以很好地查看图像,有时我会收到此错误:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (4) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'

这里有一些与设置 FetchedResultsController 和根据上下文或 FetchedResultsController 中的更改更新 Collection View 相关的代码 sn-ps。

  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

        guard let imagesCount = fetchedResultsController.fetchedObjects?.count else {return 0}

        return imagesCount
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        print ("cell data")
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as! ImageCell
        //cell.placeImage.image = UIImage (named: "placeholder")

        let imageObject = fetchedResultsController.object(at: indexPath)
        let imageData = imageObject.image
        let uiImage = UIImage (data: imageData!)

        cell.placeImage.image = uiImage
        return cell
    }



func setUpFetchedResultsController () {
        print ("setting up controller")
        //Build a request for the Image ManagedObject
        let fetchRequest : NSFetchRequest <Image> = Image.fetchRequest()
        //Fetch the images only related to the images location

        let locationObjectId = self.imagesLocation.objectID
        let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location
        let predicate = NSPredicate (format: "location == %@", locationOnBackgroundContext)

        fetchRequest.predicate = predicate
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "location", ascending: true)]

        fetchedResultsController = NSFetchedResultsController (fetchRequest: fetchRequest, managedObjectContext: dataController.backgroundContext, sectionNameKeyPath: nil, cacheName: "\(latLongString) images")

        fetchedResultsController.delegate = self

        do {
            try fetchedResultsController.performFetch ()
        } catch {
            fatalError("couldn't retrive images for the selected location")
        }
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {

        print ("object info changed in fecthed controller")

        switch type {
        case .insert:
            print ("insert")
            DispatchQueue.main.async {
                print ("calling section items")
                self.collectionView!.numberOfItems(inSection: 0)
                self.collectionView.insertItems(at: [newIndexPath!])
            }
            break

        case .delete:
            print ("delete")

            DispatchQueue.main.async {
                self.collectionView!.numberOfItems(inSection: 0)
                self.collectionView.deleteItems(at: [indexPath!])
            }
            break
        case .update:
            print ("update")

            DispatchQueue.main.async {
                self.collectionView!.numberOfItems(inSection: 0)
                self.collectionView.reloadItems(at: [indexPath!])
            }
            break
        case .move:
            print ("move")

            DispatchQueue.main.async {
                self.collectionView!.numberOfItems(inSection: 0)
                self.collectionView.moveItem(at: indexPath!, to: newIndexPath!)

            }

        }
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
        print ("section info changed in fecthed controller")
        let indexSet = IndexSet(integer: sectionIndex)
        switch type {
        case .insert:
            self.collectionView!.numberOfItems(inSection: 0)
            collectionView.insertSections(indexSet)
            break
        case .delete:
            self.collectionView!.numberOfItems(inSection: 0)
            collectionView.deleteSections(indexSet)
        case .update, .move:
            fatalError("Invalid change type in controller(_:didChange:atSectionIndex:for:). Only .insert or .delete should be possible.")
        }

    }

    func addSaveNotificationObserver() {
        removeSaveNotificationObserver()
        print ("context onbserver notified")
        saveObserverToken = NotificationCenter.default.addObserver(forName: .NSManagedObjectContextObjectsDidChange, object: dataController?.backgroundContext, queue: nil, using: handleSaveNotification(notification:))
    }

    func removeSaveNotificationObserver() {
        if let token = saveObserverToken {
            NotificationCenter.default.removeObserver(token)
        }
    }

    func handleSaveNotification(notification:Notification) {
        DispatchQueue.main.async {
            self.collectionView!.numberOfItems(inSection: 0)
            self.collectionView.reloadData()
        }
    }

我做错了什么?我会很感激任何帮助。

【问题讨论】:

    标签: ios swift multithreading core-data data-persistence


    【解决方案1】:

    我不能告诉你 1) 的问题是什么,但我认为 2) 不是(只是)数据库的问题。

    当您向集合视图添加或删除项目/部分时,通常会发生错误,但是当之后调用 numberOfItemsInSection 时,数字不会相加。示例:您有 5 个项目并添加 2,但随后调用 numberOfItemsInSection 并返回 6,这会造成不一致。

    在您的情况下,我的猜测是您使用 collectionView.insertItems() 添加项目,但此行之后返回 0:

    guard let imagesCount = fetchedResultsController.fetchedObjects?.count else {return 0}
    

    你的代码中让我感到困惑的是这些部分:

     DispatchQueue.main.async {
                print ("calling section items")
                self.collectionView!.numberOfItems(inSection: 0)
                self.collectionView.insertItems(at: [newIndexPath!])
            }
    

    您正在请求那里的项目数量,但您实际上并没有对函数的结果做任何事情。有什么原因吗?

    即使我不知道 CoreData 的问题是什么,我还是建议您不要在 tableview 委托方法中访问数据库,而是要拥有一个仅在数据库内容更改时获取的项目数组.这可能更高效,更容易维护。

    【讨论】:

    • 感谢@Robin Bork。至于看起来令人困惑的线路,我根据 Michael Su 在这里的回答使用它:stackoverflow.com/questions/19199985/…。讨论指出问题出在 Collection View 中的一个错误,因为它不知道它拥有的项目数,因此添加该行将让它知道计数,但似乎它并没有完全解决问题。我打印了“imagesCount”,对于前 3 次插入操作,它总是 1,突然它跳到 4,之后插入产生了崩溃。知道为什么会这样吗?
    • 啊,很高兴知道,我不知道这个错误。关于您的问题:这可能是由于在后台向数据库添加数据时的竞争条件引起的,但如果不实际尝试就很难说,所以这只是一个猜测。我的建议是 1) 将所有数据库代码移出 VC 以获得更简洁的架构,以及 2) 将每次更改(插入/删除)的项目缓存到一个属性,以便您的内容有一个中心“真相”任何给定时刻的collectionview。现在每个回调方法都对数据库进行自己的调用,我们不知道结果是否总是相同
    • 另一个观察结果:我看到您在集合视图中也有添加和删除部分的代码。是这样称呼的吗?据我所知,在您的 numberOfItemsInSection 中,您没有区分不同的部分,这也可能导致计数错误。
    • 感谢您的帮助。是的,您对不需要的部分更新是正确的。我终于可以解决这两个问题,我发布了一个答案,你可以检查一下。再次感谢
    【解决方案2】:

    您在批量更新期间遇到UICollectionView 不一致的常见问题。 如果您以错误的顺序执行删除/添加新项目UICollectionView 可能会崩溃。 这个问题有两种典型的解决方案:

    1. 使用 -reloadData() 代替批量更新。
    2. 使用第三方库安全地实施批量更新。像这样https://github.com/badoo/ios-collection-batch-updates

    【讨论】:

      【解决方案3】:

      问题是 NSFetchedResultsController 应该只使用 主线程 NSManagedObjectContext。

      解决方案:创建两个 NSManagedObjectContext 对象,一个用于 NSFetchedResultsController 的主线程,一个用于执行数据写入的后台线程。

      let writeContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) let readContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) let fetchedController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: readContext, sectionNameKeyPath: nil, cacheName: nil) writeContext.parent = readContext

      一旦数据保存在 writeContext 中,UICollectionView 将正确更新,具有以下链:

      writeContext(后台线程)-> readContext(主线程)-> NSFetchedResultsController(主线程)-> UICollectionView(主线程)

      【讨论】:

        【解决方案4】:

        我要感谢 Robin Bork、Eugene El 和 meim 的回答。

        我终于可以解决这两个问题了。

        对于 CollectionView 问题,我觉得我更新了太多次了,正如你在代码中看到的那样,我曾经在两个 FetchedResultsController 委托方法中更新它,并且还通过一个观察者来观察它的任何变化上下文。所以我删除了所有这些,只使用了这个方法:

        func controllerWillChangeContent(_ controller: 
        
            NSFetchedResultsController<NSFetchRequestResult>) {
                    DispatchQueue.main.async {
                        self.collectionView.reloadData()
                    }
                }
        

        除此之外,CollectionView 在维护部分中的项目计数方面存在错误,有时正如 Eugene El 提到的那样。所以,我只是使用 reloadData 来更新它的项目并且效果很好,我删除了任何逐项调整其项目的方法的使用,例如在特定的 IndexPath 插入一个项目。

        针对悬空物体问题。从代码中可以看出,我有一个 Location 对象和一个 Image 对象。我的位置对象已经填充了一个位置,它来自view context,所以我只需要使用它的 ID 从它创建一个相应的对象(正如您在问题中的代码中看到的那样)。

        问题出在图像对象上,我在view context 上创建了一个对象(其中不包含插入的数据),获取它的ID,然后在background context 上构建一个相应的对象。在阅读了这个错误并考虑了我的代码之后,我认为原因可能是因为 view context 上的 Image 对象不包含任何数据。所以,我删除了在view context 上创建该对象的代码,并直接在background context 上创建了一个,并在下面的代码中使用它,它工作了!

        func saveImagesToDb () {
        
                //Store the image in the DB along with its location on the background thread
                dataController.backgroundContext.perform {
                    for downloadedImage in self.downloadedImages {
                        let imageOnBackgroundContext = Image (context: self.dataController.backgroundContext)
        
                        //imagesLocation is on the view context
                        let locationObjectId = self.imagesLocation.objectID
                        let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location
        
                        let imageData = NSData (data: downloadedImage.jpegData(compressionQuality: 0.5)!)
                        imageOnBackgroundContext.image = imageData as Data
                        imageOnBackgroundContext.location = locationOnBackgroundContext
        
        
                        guard (try? self.dataController.backgroundContext.save ()) != nil else {
                            self.showAlert("Saving Error", "Couldn't store images in Database")
                            return
                        }
                    }
                }
        
            }
        

        如果有人有与我所说的不同的想法,为什么第一种方法首先在 view context 上创建一个空的 Image 对象,然后在 background context 上创建一个相应的对象不起作用,请告诉我们.

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-09-19
          • 1970-01-01
          • 2013-11-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多