In my application, I periodically write a set of dynamic data to a file. The data object is updated every second. Sometimes I get the exception "The collection was mutated, being mutated" on one of the lines of my encodeWithCoder: method. Each object is encoded as follows:
[aCoder encodeObject:self.speeds forKey:@"speeds"];
Where self.speeds is an NSMutableArray. I assume that the problem is that the data is being updated while it is being encoded. I tried using @synchronize in the encoding and saving blocks, and I also tried to make the property atomic rather than non-atomic, but it didn't work. Saving occurs in the background. Any ideas on how to keep this data in the background during the update? I want to make a copy and then save a copy, but will the same problem occur? Thanks!
Change 1:
The idea of the application is that I open a map view that periodically updates a singleton class and contains an array of data objects, each data object being information about the user's map. In each data object - the user's location, speed, altitude, distance, etc. Each time the location manager updates the user's location, I update the current data object (the "live" data object that was just created to track this trip, at any time there can be only one "live" data object) with new information.
I would like to write the entire singleton to a file every x minutes, but sometimes recording and updating happen at the same time, and I get this error (or at least this is what I assume causes this crash). Is there a problem here with my code or my design pattern?
This is the encoding method in my custom class:
- (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"]; } }
This is the update method (in the method and other methods called by the update method, there is more code, but I omit everything that does not refer to the "live" dataObject , the one that is being updated)
- (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]; } } }
And finally, the synchronize method in the SingletonDataController class (updated so that now synchronization occurs in the asynchronous block according to Tommy's answer):
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]; } });
where backgroundQueue is created as follows:
[sharedSingleton setBackgroundQueue:dispatch_queue_create("com.xxxx.xx", NULL)];
I can send more code if necessary, but they seem to be important parts.