Question

I pretty much know why my iPad app is crashing, but I'm having trouble coming up with a scheme to get around the scenario. The app is a jigsaw puzzle app. I've got a working version that's more stable than the one in the app store, but I still have a pesky problem I can't quite lick.

The root of the problem is a clash between user activity and automated saves. The save essentially stores the state of the puzzle as property list. The property list contains, among other things, a compilation of all the palettes in the puzzle, and for each palette, the details of all the pieces on that palette. It works well, except that user activity could change these details. A palette is essentially a UIView containing puzzle pieces as subviews. A user can move pieces around on a palette or move them from palette to palette.

I have the save process working in two phases. The first phase is kicked off by a timer. At regular intervals, this phase checks to see if there is some user activity that warrants a save. It sets a property abortSave to NO and then triggers a nonrepeating timer to wait for another period of time before starting phase two.

In phase two, the save takes place as long as abortSave is NO.

Meanwhile, if the user performs any operation that affects the save, abortSave is set to YES. The idea is that the delay between phase 1 and phase 2 is longer than it takes to perform a user operation, so if abortSave is NO, then it should be safe to do a save.

This process has eliminated 95% or so of the crashes, but I'm still getting crashes.

Of course, for decent performance of the app, the user activity as well as the save operation take place in background threads.

The type of circumstance I am running into is usually a mutation during fast enumeration, or something like that. Essentially, some user action is making a change during the save process. If I copy the object being fast enumerated and then work on the copy, it doesn't help. Sometimes the error will happen on the copy statement. If the object is an array, I don't use fast enumeration but use a regular for loop to work through the array. That helps a bit.

I hope this question isn't too generic. I suppose I could post some code, but I'm not sure how helpful it really would be. And I don't want to needlessly clutter the question.

One thing that I have not done yet, would be to use a flag working the other way:

saveProcessActive set to YES right before the save happens and set to NO when it finishes. Then all the user actions would have to be stalled if saveProcessActive is YES. The problem with this scenario is that it would result in a delay of the user action, potentially visible to the user, but maybe any delay is insignificant. It would only need to be as long as the save takes until its next check of abortSave. The aborted save process would then turn saveProcessActive to NO when it acknowledged the abort request. Is there a better solution?

Was it helpful?

Solution 2

You will need to synchronize access to the data, both while saving and while altering it during normal play. As writing to file would likely take longer than making a copy, in order to minimize lock time you should make a copy while you have a lock, then release the lock and write the data to disk. There are a few ways to do this, but the easiest is an @synchronised block:

-(void) save
{
    NSDictionary *old = self.data;
    NSDictionary *new;
    @synchronized(old) {
        new = [old copy];
    }
    [self writeData:new];
}

And remember to synchronize changes too:

-(void) updateDataKey:(id)key toValue:(id)val
{
    NSDictionary *old = self.data;
    @synchronized(old) {
        old[key] = val;
    }    
}

data obviously doesn't need to be an NSMutableDictionary, it was just a convenient example.

OTHER TIPS

Making a copy of the current game state in memory should be a fast action. When you want to save, make that copy, and then hand it to your background queue to save it with dispatch_async(). Doing it this way gets rid of all the concurrency issues because each piece of data is only ever accessed on a single queue.


EDIT: Here is how I've typically addressed such issues (untested):

- (void)fireSave:(NSTimer *)timer {
  id thingToSave = [self.model copyOfThingToSave];
  dispatch_async(self.backgroundSavingSerialQueue, ^{
    [self writeToDisk:copyOfThingToSave];
  }
}

- (void)saveLater {
  [self.timer invalidate];
  self.timer = [NSTimer scheduledTimerWithTimeInterval:5
                                                target:self
                                              selector:@selector(fireSave:)
                                              userInfo:nil 
                                               repeats:NO];
}

Now, anywhere you modify data, you call [self saveLater]. Everything here is on the main thread except for writeToDisk: (which is passed a copy of the data). Since writeToDisk: always runs on its own serial queue, it also avoids race conditions, even if you ask it to save faster than it can.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top