Коллекция была видоизменена при перечислении - архивирование данных и запись в файл с помощью NSCoder - PullRequest
8 голосов
/ 24 февраля 2012

В моем приложении я периодически записываю набор динамических данных в файл. Объект данных обновляется примерно каждую секунду. Иногда в одной из строк моего метода encodeWithCoder: метод я получаю исключение «Коллекция была видоизменена во время изменения». Каждый объект кодируется так:

[aCoder encodeObject:self.speeds forKey:@"speeds"];

Где self.speeds - это NSMutableArray. Я предполагаю, что проблема в том, что данные обновляются во время их кодирования. Я попытался использовать @synchronize в кодирующих блоках и , а также попытался сделать свойство атомарным, а не неатомарным, но ни один из них не работал. Сохранение происходит в фоновом режиме. Любые идеи о том, как сохранить эти данные в фоновом режиме, пока они обновляются? Мне хочется сделать копию, а затем сохранить ее, но не возникнет ли такая же проблема? Спасибо!


Редактировать 1:

Идея приложения заключается в том, что я открываю представление карты, которое периодически обновляет одноэлементный класс, содержащий массив объектов данных, причем каждый объект данных является информацией о карте пользователя. В каждом объекте данных, местоположениях пользователя, скоростях, высотах, расстоянии и т. Д. Каждые три раза менеджер местоположений обновляет местоположение пользователя, я обновляю текущий объект данных («живой» объект данных, который был только что создан для отслеживания этой поездки). в любой момент может быть только один «живой» объект данных) с новой информацией.

Я хотел бы записать весь синглтон в файл каждые x минут, но иногда запись и обновление происходят одновременно, и я получаю эту ошибку (или, по крайней мере, я так думаю) вызывает этот сбой). Проблема в моем коде или шаблоне дизайна?


Это метод кодирования в моем пользовательском классе:

- (void)encodeWithCoder:(NSCoder*)aCoder {
    @synchronized([SingletonDataController sharedSingleton]) {
        [aCoder encodeObject:[[lineLats copy] autorelease] forKey:@"lineLats"];
        [aCoder encodeObject:[[lineLongs copy] autorelease] forKey:@"lineLongs"];
        [aCoder encodeObject:[[horizontalAccuracies copy] autorelease] forKey:@"horAcc"];
        [aCoder encodeObject:[[verticalAccuracies copy] autorelease] forKey:@"vertAcc"];
        [aCoder encodeObject:[[speeds copy] autorelease] forKey:@"speeds"];
        [aCoder encodeObject:[[overlayColors copy] autorelease] forKey:@"colors"];
        [aCoder encodeObject:[[annotationLats copy] autorelease] forKey:@"annLats"];
        [aCoder encodeObject:[[annotationLongs copy] autorelease] forKey:@"annLongs"];
        [aCoder encodeObject:[[locationManagerStartDate copy] autorelease] forKey:@"startDate"];
        [aCoder encodeObject:[[locationManagerStartDateString copy] autorelease] forKey:@"locStartDateString"];
        [aCoder encodeObject:[[mapTitleString copy] autorelease] forKey:@"title"];
        [aCoder encodeObject:[[shortDateStringBackupCopy copy] autorelease] forKey:@"backupString"];

        [aCoder encodeFloat:pathDistance forKey:@"pathDistance"];
        [aCoder encodeFloat:linearDistance forKey:@"linearDistance"];
        [aCoder encodeFloat:altitudeChange forKey:@"altitudeChange"];
        [aCoder encodeFloat:averageSpeedWithFilter forKey:@"avWithFilter"];
        [aCoder encodeFloat:averageSpeedWithoutFilter forKey:@"avWithoutFilter"];

        [aCoder encodeInt:totalTripTimeInSeconds forKey:@"totalTimeInSecs"];
    }
}

Это метод обновления (в методе и других методах, вызываемых в методе обновления, есть больше кода, но я опускаю все, что не ссылается на «живой» объект dataObject; тот, который обновляется) :

- (void)locationManager:(CLLocationManager*)manager didUpdateToLocation:(CLLocation*)newLocation fromLocation:(CLLocation*)oldLocation {
    @synchronized([SingletonDataController sharedSingleton]) {
        //create temporary location for last logged location
        CLLocation* lastLocation;
        if([dataObject.lineLats lastObject] && [dataObject.lineLongs lastObject]) {
            lastLocation = [[CLLocation alloc] initWithLatitude:[[dataObject.lineLats lastObject] floatValue] longitude:[[dataObject.lineLongs lastObject] floatValue]];
        } else {
            lastLocation = [oldLocation retain];
        }

        //.....

        //periodically add horizontal/vertical accuracy
        if(iterations > 0 && iterations % 4 == 0) {
            [dataObject.horizontalAccuracies addObject:[NSNumber numberWithFloat:[newLocation horizontalAccuracy]]];
            [dataObject.verticalAccuracies addObject:[NSNumber numberWithFloat:[newLocation verticalAccuracy]]];
        }

        //.....

        //accumulate some speed data
        if(iterations % 2 == 0) {
            NSNumber* speedNum = [[NSNumber alloc] initWithFloat:[newLocation speed]];
            [dataObject.speeds addObject:speedNum];
            [speedNum release];
        }

        //.....

        //add latitude and longitude
        NSNumber* lat = [[NSNumber alloc] initWithFloat:[newLocation coordinate].latitude];
        NSNumber* lon = [[NSNumber alloc] initWithFloat:[newLocation coordinate].longitude];
        if(fabs([lat floatValue]) > .0001 && fabs([lon floatValue]) > .0001) {
            [dataObject.lineLats addObject:lat];
            [dataObject.lineLongs addObject:lon];
        }

        if(iterations % 60 == 0) {
            [[SingletonDataController sharedSingleton] synchronize];
        }
    }
}

И, наконец, метод synchronize в классе SingletonDataController (обновлен так, что теперь синхронизация происходит в асинхронном блоке согласно ответу Томми):

dispatch_async(self.backgroundQueue, ^{
    @synchronized([SingletonDataController sharedSingleton]) {
        NSLog(@"sync");
        NSData* singletonData = [NSKeyedArchiver archivedDataWithRootObject:
            [SingletonDataController sharedSingleton]];

        if(!singletonData) {
            return;
        }

        NSString* filePath = [SingletonDataController getDataFilePath];
        [singletonData writeToFile:filePath atomically:YES];
    }
});

где backgroundQueue создается следующим образом:

[sharedSingleton setBackgroundQueue:dispatch_queue_create("com.xxxx.xx", NULL)];

Я могу выложить больше кода, если это необходимо, но они, кажется, являются важными частями.

Ответы [ 3 ]

5 голосов
/ 28 февраля 2012

Вы выполняете dispatch_async в течение одного из ваших @synchronize с.Вещи там не подлежат неявной блокировке, встроенной в синхронизацию;все, что происходит, это когда вы получаете блокировку, отправляете блок и затем снимаете блокировку.Таким образом, блок может легко произойти за пределами блокировки (и, действительно, вы ожидаете, что обычно это произойдет).

Чтобы придерживаться пути синхронизации, вы хотите @synchronize внутри блока, а не вне его.,Однако вы можете попытаться придумать менее решительный подход, например, выполнить все свои обновления в одной очереди последовательной отправки и позволить им помещать соответствующие новые значения в основную очередь.

1 голос
/ 26 февраля 2012

Если вы беспокоитесь, что сериализация занимает достаточно много времени, чтобы повлиять на следующую сериализацию, скопируйте объект, а затем используйте dispatch_async для его сериализации. Таким образом, сериализация будет происходить в асинхронной очереди.

Однако, возможно, вы хотите полностью переосмыслить этот подход. Разве Core Data не вариант? С его помощью вы можете обновлять только те значения, которые действительно изменились, и я почти уверен, что он сможет решить ваши проблемы с блокировкой.

Редактировать Извините, я неправильно прочитал ваш первоначальный пост. Если вы не сохраняете слишком часто, вы можете рассмотреть возможность использования блокировок. Смотри https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html

Но делайте это, только если вы не слишком часто сериализуете, так как это значительно снизит вашу производительность.

Итак, заблокируйте объект, скопируйте его, разблокируйте объект, выполните сериализацию асинхронного копирования.

0 голосов
/ 26 февраля 2012

Да, сериализация копии массива вместо изменяемого массива гарантирует, что массив не изменится во время сохранения, но вы просто решаете проблему: у вас все еще есть случай, когда массив может измениться один поток, пока он копируется в другой. Вы можете поместить блоки @synchronize вокруг мутаций как копии, так и массива (точно так же, как вы сказали, что делали с сохранением / обновлением .. что должно работать - если бы вы использовали тот же объект для @synchronize параметр? @synchronize (self) - это удобный способ сделать это).

Другой способ синхронизировать операцию копирования - использовать dispatch_sync () для копирования в основном потоке:

__block NSArray* listCopy;

dispatch_sync(dispatch_get_main_queue(), ^{ listCopy = [self.speeds copy]; });

[aCoder encodeObject:listCopy forKey:@"speeds"];
[listCopy release];

Это немного более грубо - он не может сделать копию, пока основной поток не очистится, тогда как @synchronized копия может запускаться, как только основной поток выйдет из своего блока @synchronize - но он имеет преимущество в том, что вам нужно только поместить этот код в поток сохранения, и не беспокоиться о том, где вы можете изменять массив в основном потоке.

Редактировать: только что увидел другую заметку об использовании NSLock. Использование @synchronize - это почти то же самое, что и использование NSLock ( здесь хороший пост об этом), но вам не нужно беспокоиться об управлении объектом блокировки. Опять же, @synchronize должен был сработать для вас, и это действительно удобно, если у вас нет десятков разных мест для синхронизации.

...