8

私のアプリでは、一連の動的データを定期的にファイルに書き込んでいます。データ オブジェクトは約 1 秒ごとに更新されます。時折、encodeWithCoder: メソッドの行の 1 つで、「変異中にコレクションが変異しました」という例外が発生します。各オブジェクトは次のようにエンコードされます。

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

self.speeds は NSMutableArray です。問題は、エンコード中にデータが更新されていることだと思います。エンコーディングブロック保存ブロックで @synchronize を使用してみました。また、プロパティを非アトミックではなくアトミックにしようとしましたが、どちらも機能しませんでした。保存バックグラウンドで行われています。更新中にこのデータをバックグラウンドで保存する方法のアイデアはありますか? コピーを作成して保存すればうまくいくような気がしますが、同じ問題は発生しませんか? ありがとう!


編集1:

アプリのアイデアは、マップ ビューを開くことです。マップ ビューは、データ オブジェクトの配列を含むシングルトン クラスを定期的に更新します。各データ オブジェクトはユーザーのマップ情報です。各データ オブジェクトには、ユーザーの位置、速度、高度、距離などがあります。ロケーション マネージャーがユーザーの位置を 3 回更新するたびに、現在のデータ オブジェクト (この旅行を追跡するために作成されたばかりの「ライブ」データ オブジェクト) を更新します。新しい情報を持つ「ライブ」データ オブジェクトは常に 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"];
    }
}

これは update メソッドです (メソッドにはさらに多くのコードがあり、update メソッドで呼び出される他のメソッドもありますが、「ライブ」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)];

必要に応じてさらにコードを投稿できますが、それらは重要な部分のようです。

4

3 に答える 3

5

dispatch_asyncあなたはあなたの内の 1 つを実行します@synchronize。そこにあるものは、同期に組み込まれた暗黙的なロックの対象ではありません。ロックを取得し、ブロックをディスパッチしてからロックを解放するだけです。したがって、ブロックはロックの外で簡単に発生する可能性があります (実際、通常はそうなると予想されます)。

同期パスに固執するには@synchronize、ブロックの外側ではなく、ブロック内にする必要があります。ただし、すべての更新を 1 つのシリアル ディスパッチ キューで実行し、関連する新しい値をメイン キューにプッシュできるようにするなど、それほど強力ではないアプローチを考え出すこともできます。

于 2012-02-28T00:13:54.203 に答える
1

シリアライゼーションに時間がかかり、次のシリアライゼーションに影響することが心配な場合は、オブジェクトをコピーしてから、 を使用dispatch_asyncしてシリアライズします。そうすれば、シリアル化は非同期キューで行われます。

ただし、このアプローチを完全に再考する必要があるかもしれません。Core Data はオプションではありませんか? それを使用すると、実際に変更された値のみを更新でき、ロックの問題を処理できると確信しています。

編集申し訳ありませんが、最初の投稿を読み違えました。頻繁に保​​存しない場合は、ロックの使用を検討してください。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 に答える