8

在我的应用程序中,我定期将一组动态数据写入文件。数据对象大约每秒更新一次。有时,我的 encodeWithCoder: 方法中的一行出现“集合在被变异时发生变异”异常。每个对象的编码如下:

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

其中 self.speeds 是一个 NSMutableArray。我认为问题在于数据在编码时正在更新。我尝试在编码保存块中使用@synchronize,并且我还尝试使属性原子而不是非原子,但都没有奏效。保存后台进行。关于如何在更新时将这些数据保存在后台的任何想法?我觉得制作副本然后保存副本会起作用,但不会出现同样的问题吗?谢谢!


编辑1:

应用程序中的想法是我打开一个地图视图,它定期更新一个包含数据对象数组的单例类,每个数据对象都是用户的地图信息。在每个数据对象中,用户的位置、速度、高度、距离等。位置管理器每 3 次更新用户的位置,我就更新当前数据对象(刚刚创建用于跟踪这次旅行的“实时”数据对象——任何时候都只能有一个“实时”数据对象)带有新信息。

我想每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(更新,以便现在根据 Tommy 的回答在异步块内发生同步):

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)];

如果需要,我可以发布更多代码,但这些似乎是重要的部分。

4

3 回答 3

5

您在其中一个sdispatch_async内执行 a。@synchronize里面的东西不受同步内置的隐式锁的影响;所有发生的事情都是你获得锁,分派块然后释放锁。所以阻塞很容易发生在锁之外(事实上,你会期望它通常会发生)。

要坚持同步路径,您希望@synchronize在块内而不是在块外。但是,您可能会尝试提出一种不那么有力的方法,例如在单个串行调度队列上执行所有更新,并允许它们将相关的新值推送到主队列中。

于 2012-02-28T00:13:54.203 回答
1

如果担心序列化时间过长会影响下一次序列化,请复制对象,然后使用dispatch_async它进行序列化。这样,序列化将在异步队列中进行。

但是,也许您想完全重新考虑这种方法。核心数据不是一种选择吗?有了它,您只能更新实际更改的值,而且我很确定它可以处理您的锁定问题。

编辑对不起,我误读了您的初始帖子。如果您不经常保存,您可能需要考虑使用锁。请参阅https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html

但只有在您不经常序列化时才这样做,因为这会显着降低您的性能。

所以,锁定对象,复制它,解锁对象,异步序列化复制。

于 2012-02-26T01:36:41.760 回答
0

是的,序列化数组的副本而不是可变数组将确保数组在保存时不会更改,但您只是在转移问题:您仍然遇到数组可能在一个线程上更改的情况,而它被复制到另一个上。您可以在副本和数组突变周围放置 @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 几乎相同(这里有一篇关于它的好 SO 帖子),但您不必担心管理锁对象。同样,@synchronize 应该对你有用,而且只要你不需要同步几十个不同的地方,它就真的很方便。

于 2012-02-26T02:23:01.810 回答