Swift Realm: потокобезопасный параллельный индекс чтения и записи выходит за пределы

Я пытаюсь безопасно выполнять одновременное чтение и запись в базе данных Realm. Это то, чего я пытаюсь достичь.

Я извлекаю изображения из Flickr, и как только imageData загружается, объект Photo записывается в базу данных Realm. Я также включил notification для прослушивания insertions. Как только объект Photo будет записан в Realm, обновите свойство transport этого же элемента. Однако моя реализация иногда падает, т.е. падает раз в 3-5 раз реализации.

Код как таковой:

override func viewDidLoad() {
    super.viewDidLoad()

    subscribeToRealmNotifications()
}

fileprivate func subscribeToRealmNotifications() {
    do {
        let realm = try Realm()
        let results = realm.objects(Photo.self)

        token = results.observe({ (changes) in
            switch changes {
            case .initial:
                self.setupInitialData()
                self.collectionView.reloadData()

            case .update(_, _, let insertions, _):
                if !insertions.isEmpty {
                    self.handleInsertionsWhenNotified(insertions: insertions)
                }

            case .error(let error):
                self.handleError(error as NSError)
            }
        })

    } catch let error {
        NSLog("Error subscribing to Realm Notifications: %@", error.localizedDescription)
    }
}

fileprivate func handleInsertionsWhenNotified(insertions: [Int]) {
    let lock = NSLock()
    let queue = DispatchQueue(label: "queue", qos: .userInitiated) //Serial queue

    queue.async(flags: .barrier) {
        do {
            let realm = try Realm()
            let objects = realm.objects(Photo.self)

            lock.lock()
            for insertion in insertions {
                print(insertion, objects.count, objects[insertion].id ?? "")
                let photo = objects[insertion] //Crash here
                self.update(photo: photo)
            }

            lock.unlock()

        } catch let error {
            NSLog("Error updating photos in Realm Notifications", error.localizedDescription)
        }
    }
}

func update(photo: Photo) {
    do {
        let realm = try Realm()
        let updatedPhoto = createCopy(photo: photo)

        let transport = Transport()
        transport.name = searchText
        updatedPhoto.transport = transport

        try realm.write {
            realm.add(updatedPhoto, update: true)
        }
    } catch let error {
        NSLog("Error updating photo name on realm: %@", error.localizedDescription)
    }
}

func createCopy(photo: Photo) -> Photo {
    let copiedPhoto = Photo()
    copiedPhoto.id = photo.id
    copiedPhoto.farm = photo.farm
    copiedPhoto.server = photo.server
    copiedPhoto.secret = photo.secret
    copiedPhoto.imageData = photo.imageData
    copiedPhoto.name = photo.name
    return copiedPhoto
}

//On push of a button, call fetchPhotos to download images.
fileprivate func fetchPhotos() {
    FlickrClient.shared.getPhotoListWithText(searchText, completion: { [weak self] (photos, error) in
        self?.handleError(error)

        guard let photos = photos else {return}

        let queue = DispatchQueue(label: "queue1", qos: .userInitiated , attributes: .concurrent)

        queue.async { 
            for (index, _) in photos.enumerated() {
                FlickrClient.shared.downloadImageData(photos[index], { (data, error) in
                    self?.handleError(error)

                    if let data = data {
                        let photo = photos[index]
                        photo.imageData = data
                        self?.savePhotoToRealm(photo: photo)

                        DispatchQueue.main.async {
                            self?.photosArray.append(photo)

                            if let count = self?.photosArray.count {
                                let indexPath = IndexPath(item: count - 1, section: 0)
                                self?.collectionView.insertItems(at: [indexPath])
                            }
                        }
                    }
                })
            }
        }
    })
}

fileprivate func savePhotoToRealm(photo: Photo) {
    do {
        let realm = try Realm()
        let realmPhoto = createCopy(photo: photo)

        try realm.write {
            realm.add(realmPhoto)
            print("Successfully saved photo:", photo.id ?? "")
        }
    } catch let error {
        print("Error writing to photo realm: ", error.localizedDescription)
    }
}

Обратите внимание, что приведенный выше код дает сбой раз в 3-5 раз, поэтому я подозреваю, что чтение и запись выполняются небезопасно. Журнал печати и журналы ошибок выглядят так, как показано при сбое

Successfully saved photo: 45999333945 
4 6 31972639607 
6 7 45999333945 
Successfully saved photo: 45999333605 
Successfully saved photo: 45999333675 
7 8 45999333605 
8 9 45999333675 
Successfully saved photo: 45999333285 
Successfully saved photo: 33038412228 
2019-01-29 14:46:09.901088+0800 GCDTutorial[24139:841805] *** Terminating app due to uncaught exception 'RLMException', reason: 'Index 9 is out of bounds (must be less than 9).'

Кто-нибудь поможет сказать, где я ошибся?

ПРИМЕЧАНИЕ. Я пытался запустить queue.sync в handleInsertionsWhenNotified. Это полностью устраняет сбой, но останавливает пользовательский интерфейс, когда он работает в основном потоке. Это не идеально в моем случае.


person Koh    schedule 29.01.2019    source источник


Ответы (2)


После более тщательного изучения журналов я заметил, что количество объектов не совпадает всякий раз, когда приложение аварийно завершает работу. Другими словами, общее количество объектов, напечатанных, когда Realm уведомляет о вставке, равно 9 (хотя физический просмотр базы данных области через браузер показал более 9), но индекс вставки равен 9.

Это означает, что при выполнении запроса количество объектов, вероятно, еще не обновлено (не совсем понятно почему). После прочтения дополнительных статей о документации по области и здесь, я реализовал realm.refresh() перед запросом объектов. Это решает проблему.

//Updated code for handleInsertionsWhenNotified
fileprivate func handleInsertionsWhenNotified(insertions: [Int]) {
    let lock = NSLock()
    let queue = DispatchQueue(label: "queue", qos: .userInitiated) //Serial queue

    queue.async(flags: .barrier) {
        do {
            let realm = try Realm()
            realm.refresh() // Call refresh here
            let objects = realm.objects(Photo.self)

            lock.lock()
            for insertion in insertions {
                print(insertion, objects.count, objects[insertion].id ?? "")
                let photo = objects[insertion] //Crash here
                self.update(photo: photo)
            }

            lock.unlock()

        } catch let error {
            NSLog("Error updating photos in Realm Notifications", error.localizedDescription)
        }
    }
}

Надеюсь, это поможет кому-нибудь там.

person Koh    schedule 30.01.2019

Строка вставки CollectionView вызывает первый вызов numberOfIteminSection. Я надеюсь, что этот код работает.

let indexPath = IndexPath(item: count - 1, section: 0)
self?.collectionView.numberOfItems(inSection: 0)
self?.collectionView.insertItems(at: [indexPath])
person Pradip Patel    schedule 29.01.2019
comment
Сбой не происходит на collectionView.insertItems. Это внутри handleInsertionsWhenNotified. - person Koh; 29.01.2019
comment
Ответы, содержащие только код, не приветствуются. Нажмите «Изменить» и добавьте несколько слов, описывающих, как ваш код отвечает на вопрос. - person Prakash Thete; 29.01.2019