Основные данные iOS — Серьезная ошибка приложения — попытка вставить ноль менее чем за 1%

#ios #objective-c #swift #core-data

#iOS #objective-c #swift #основные данные

Вопрос:

Основные данные iOS — Серьезная ошибка приложения — попытка вставить ноль

Здравствуйте,

Мое приложение работает на самом деле стабильно, но в редких случаях оно выходит из строя с этим сообщением об ошибке…

2019-04-02 20:48:52.437172 0200 MyAppName[4422: 1595677] [ошибка] ошибка: серьезная ошибка приложения. Исключение было обнаружено во время обработки изменений основных данных. Обычно это ошибка в наблюдателе NSManagedObjectContextObjectsDidChangeNotification. -[__NSCFSet AddObject:]: попытка вставить ноль с помощью userInfo (null) CoreData: ошибка: серьезная ошибка приложения. Исключение было обнаружено во время обработки изменений основных данных. Обычно это ошибка в наблюдателе NSManagedObjectContextObjectsDidChangeNotification. -[__NSCFSet AddObject:]: попытка вставить ноль с помощью userInfo (null) 2019-04-02 20:48:52.438246 0200 MyAppName[4422:1595677] *** Завершение работы приложения из-за неперехваченного исключения ‘NSInvalidArgumentException’, причина: ‘-[__NSCFSet AddObject:]: попытка вставить ноль’

…когда он пытается сохранить текущий контекст (эта часть в моем коде все еще находится в objc):

 - (void)saveChanges
{
    dispatch_async(dispatch_get_main_queue(), ^{
        NSError *err = nil;
        BOOL succesful = [self->context save:amp;err];
        if (!succesful)
        {
            NSLog(@"ERROR MESSAGE DURING SAVING CONTEXT: %@", [err localizedDescription]);
        }
    });
}
  

«редко» означает:
Большинство клиентов никогда не сталкиваются с проблемой, у немногих клиентов это происходит несколько раз в день.
За последние два дня мне удалось создать это 2 раза, хотя я пробовал несколько способов вызвать эту ошибку (см. Ниже).

Это настройка:

  • Соответствующие данные находятся в одном объекте (таблице)
  • NSFetchedResultsController отображает данные в UITableView
  • Пользователь может нажать кнопку, чтобы добавить новую запись.
  • Новая запись содержит только некоторые базовые данные и инициирует два вызова API для двух веб-серверов
  • Каждый ответ веб-сервера обновляет запись
  • После того, как оба выполнены (или были отменены из-за тайм-аута), я вызываю функцию SaveChanges сверху только один раз.
  • Все функции используют один и тот же контекст, созданный NSPersistentContainer, как показано ниже (эта часть уже в swift)
 @objc lazy var persistentContainer: NSPersistentContainer = {

        let container = NSPersistentContainer(name: "myAppName")
        let description = NSPersistentStoreDescription(url: SomeHelper.urlForFileInDocFolder("storev7.data"))
        container.persistentStoreDescriptions = [description]

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error (error), (error.userInfo)")
            }
        })

        return container
 }()
  

Если бы я мог каким-то образом воспроизвести ошибку, я мог бы найти подходящее решение, но, поскольку этого почти никогда не происходит, я застрял.

У вас есть идея, как я мог бы воспроизвести ошибку выше? Или у вас есть представление о том, что может вызвать ошибку в моем случае?

Что я уже пытался воспроизвести ошибку:

  • Создайте сотни записей
  • Создайте сотни записей за несколько секунд
  • Создание сотен записей при включении / выключении интернет-соединения / вкл / выкл /…
  • Создайте сотни записей во время микширования из фонового и основного потоков (для этого я удалил отправку из SaveChanges)
  • Создайте сотни записей с различными задержками в API (добавлена функция случайного ожидания на веб-сервере)
  • Длительное выполнение, приложение работает в течение 24 часов на реальном устройстве и создает запись каждые 2 минуты
  • Смешивает все из них

Комментарии:

1. Какой тип параллелизма является этим контекстом? Является ли вызывающий -save в основной очереди или в другой очереди? Изменяет ли эта очередь контекст каким-либо образом?

2. @RobNapier Я ожидаю, что это будет NSMainQueueConcurrencyType , но я не определяю это. Я просто использую NSPersistentContainer , как показано выше, для создания контекста с помощью persistentContainer.viewContext . Я также отправляю все -save запросы в основной поток. И я не изменяю контекст после его первоначального создания.

3. viewContext хорошо; это соответствует. Вносите ли вы изменения в какие-либо записи в любой очереди, отличной от основной очереди (не просто сохранить; любые изменения. NSManagedObjects не являются потокобезопасными, даже для чтения.)

4. @RobNapier только для текущего объекта, с помощью которого я создаю новый объект [NSEntityDescription insertNewObjectForEntityForName:@"MyEntityName" inManagedObjectContext:context]; , после чего он изменяется из основного и фонового потоков (из вызовов веб-api), и в конце я вызываю один раз context save в основном потоке.

Ответ №1:

NSManagedObjects ограничены одной очередью. Они не являются потокобезопасными для чтения или записи. Чтение NSManagedObject может вызвать ошибку, которая представляет собой операцию записи. Это означает, что NSManagedObjects, извлеченные из контекста основной очереди (например viewContext ), не могут быть переданы в другие очереди.

Подробности всего этого обсуждаются в Руководстве по программированию Core Data:

Экземпляры NSManagedObject не предназначены для передачи между очередями. Это может привести к повреждению данных и завершению работы приложения. Когда необходимо передать ссылку на управляемый объект из одной очереди в другую, это должно быть сделано через экземпляры NSManagedObjectID.

Общий подход с NSPersistentContainer заключается в использовании чего-то вроде viewContext исключительно в главной очереди и использовании performBackgroundTask для обработки фоновых операций, или вы можете использовать newBackgroundContext для создания фонового контекста и использовать perform или performAndWait в нем для манипулирования объектами, которые извлекаются из этого контекста.

Перемещение объекта между контекстами выполняется путем извлечения того же самого objectID в другом контексте (имея в виду, что это вернет новый экземпляр из хранилища).

Вы можете отслеживать ошибки, добавляя -com.apple.CoreData.ConcurrencyDebug 1 в свою схему. Когда вы это сделаете, ошибки немедленно попадут в восхитительно названный __Multithreading_Violation_AllThatIsLeftToUsIsHonor__ .

Комментарии:

1. Большое вам за это спасибо. Для меня это имеет смысл! Только один дополнительный вопрос: если я последую этому подходу и создам два выделенных частных контекста для управления объектами с помощью ответа на вызов API веб-сервера, могу ли я затем вызвать -save 3 раза? Один раз из основного потока после первоначального создания и два раза в performBackgroundTask частном контексте после предварительной загрузки объекта с existingObjectWithID:error ?

2. Другими словами, нет проблем, если два или более -save происходят одновременно, если у каждого есть свой контекст?

3. Если нет конфликта, то нет и проблемы. Но если есть конфликт, он выдаст ошибку. Пока каждый контекст управляет своими объектами в своей очереди, он предназначен для управления параллелизмом. Думайте об этом как о ФИКСАЦИИ базы данных.

4. Конфликт означает запись / чтение одного и того же атрибута (столбца) в одной строке из разных очередей, верно? Чтение / запись разных атрибутов в одной строке из разных очередей — это нормально, верно?

5. Нет, любое изменение по умолчанию вызовет конфликт. Смотрите mergePolicy , как установить переопределение, но если вы выполняете большую работу над одними и теми же объектами, подобными этому, вам, вероятно, следует направить всю работу по модификации в единый фоновый контекст для сериализации изменений. Я не верю, что существует какая-либо политика слияния, которая выглядит как «ошибка при возникновении конфликтов между свойствами». Вы можете автоматически объединять конфликты для каждого свойства, но при несоответствии версии всего объекта возникают ошибки. (Тем не менее, вы всегда можете просто проверить словарь конфликтов и решить, что делать для объединения данных самостоятельно.)