6

簡単に言えば、バックグラウンド キューを使用して、Web サービスから取得した JSON オブジェクトを Core Data Sqlite3 データベースに保存しています。保存は、GCD を介して作成したシリアル化されたバックグラウンド キューで行われ、そのバックグラウンド キュー用に作成された NSManagedObjectContext のセカンダリ インスタンスに保存されます。保存が完了したら、メイン スレッドにある NSManagedObjectContext のインスタンスを、新しく作成/更新されたオブジェクトで更新する必要があります。私が抱えている問題は、メイン スレッドの NSManagedObjectContext のインスタンスが、バックグラウンド コンテキストに保存されたオブジェクトを見つけることができないことです。以下は、コード サンプルで実行しているアクションのリストです。私が間違っていることについて何か考えはありますか?

  • GCD を介してバックグラウンド キューを作成し、すべての前処理ロジックを実行してから、バックグラウンド コンテキストをそのスレッドに保存します。

.

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    if (savedObjectIDs.count > 0) {
        [savedObjectIDs removeAllObjects];
    }
    if (savedObjectClass) {
        savedObjectClass = nil;
    }

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path, and return all created/updated objects in an array
    NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (objectIds) {
        [savedObjectIDs addObjectsFromArray:objectIds];
    }
    if (completion) {
        saveCompletionBlock = completion;
    }
    if (managedObjectClass) {
        savedObjectClass = managedObjectClass;
    }

    // save all changes object context
    [self saveManagedObjectContext];
});
  • 「saveManagedObjectContext」メソッドは、基本的にどのスレッドが実行されているかを調べ、適切なコンテキストを保存します。このメソッドが正しく機能することを確認したので、ここにコードを配置しません。

  • このコードはすべてシングルトンにあり、シングルトンの init メソッドに「NSManagedObjectContextDidSaveNotification」のリスナーを追加して、mergeChangesFromContextDidSaveNotification: メソッドを呼び出します。

.

// merge changes from the context did save notification to the main context
- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification
{
    NSThread *currentThread = [NSThread currentThread];

    if ([currentThread.name isEqual:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]) {

        // merge changes to the primary context, and wait for the action to complete on the main thread
        [_managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

        // on the main thread fetch all new data and call the completion block
        dispatch_async(dispatch_get_main_queue(), ^{

            // get objects from the database
            NSMutableArray *objects = [[NSMutableArray alloc] init];
            for (id objectID in savedObjectIDs) {
                NSError *error;
                id object = [_managedObjectContext existingObjectWithID:objectID error:&error];
                if (error) {
                    [self logError:error];
                } else if (object) {
                    [objects addObject:object];
                }
            }

            // remove all saved object IDs from the array
            [savedObjectIDs removeAllObjects];
            savedObjectClass = nil;

            // call the completion block
            //completion(objects);
            saveCompletionBlock(objects);

            // clear the saved completion block
            saveCompletionBlock = nil;
        });
    }
}

上記のメソッドでわかるように、メイン スレッドで「mergeChangesFromContextDidSaveNotification:」を呼び出しており、アクションが完了するまで待機するように設定しています。Apple のドキュメントによると、バックグラウンド スレッドは、そのアクションが完了するまで待ってから、その呼び出しの下の残りのコードを続行する必要があります。上で述べたように、このコードを実行するとすべてが機能するように見えますが、フェッチしたオブジェクトをコンソールに出力しようとすると、何も返されません。マージが実際には行われていないか、残りのコードが実行される前に終了していないようです。マージが完了したことを確認するためにリッスンする必要がある別の通知はありますか? または、マージ後、フェクトの前にメイン オブジェクト コンテキストを保存する必要がありますか?

また、コードのフォーマットが悪くて申し訳ありませんが、SOのコードタグはメソッド定義が気に入らないようです。

みんなありがとう!

アップデート:

以下で推奨されている変更を行いましたが、それでも同じ問題が発生します。以下は私が持っている更新されたコードです。

これは、プロセスを保存するバックグラウンド スレッドを呼び出すコードです。

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    if (savedObjectIDs.count > 0) {
        [savedObjectIDs removeAllObjects];
    }
    if (savedObjectClass) {
        savedObjectClass = nil;
    }

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path
    NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (objectIds) {
        [savedObjectIDs addObjectsFromArray:objectIds];
    }
    if (completion) {
        saveCompletionBlock = completion;
    }
    if (managedObjectClass) {
        savedObjectClass = managedObjectClass;
    }

    // listen for the merge changes from context did save notification
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // save all changes object context
    [self saveManagedObjectContext];
});

これは、NSManagedObjectContextDidSaveNotification 通知によって呼び出されるコードです。

    // merge changes from the context did save notification to the main context
- (void)mergeChangesFromBackground:(NSNotification *)notification
{
    // kill the listener
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    NSThread *currentThread = [NSThread currentThread];

    // merge changes to the primary context, and wait for the action to complete on the main thread
    [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

    // dispatch the completion block
    dispatch_async(dispatch_get_main_queue(), ^{

        // get objects from the database
        NSMutableArray *objects = [[NSMutableArray alloc] init];
        for (id objectID in savedObjectIDs) {
            NSError *error;
            id object = [[self managedObjectContext] existingObjectWithID:objectID error:&error];
            if (error) {
                [self logError:error];
            } else if (object) {
                [objects addObject:object];
            }
        }

        // remove all saved object IDs from the array
        [savedObjectIDs removeAllObjects];
        savedObjectClass = nil;

        // call the completion block
        //completion(objects);
        saveCompletionBlock(objects);

        // clear the saved completion block
        saveCompletionBlock = nil;
    });
}

アップデート:

だから私は解決策を見つけました。バックグラウンド スレッドでオブジェクト ID を保存し、メイン スレッドでそれらを使用して再取得しようとしていた方法がうまくいかなかったことがわかりました。そのため、NSManagedObjectContextDidSaveNotification 通知で送信される userInfo 辞書から挿入/更新されたオブジェクトを取得することになりました。以下は、現在動作している私の更新されたコードです。

前と同様に、このコードは事前処理と保存ロジックを開始します

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    [self logMessage:[NSString stringWithFormat:@"(%@) saveJSONObjects:objectMapping:class:completion:", [managedObjectClass description]]];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path
    [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (completion) {
        saveCompletionBlock = completion;
    }

    // listen for the merge changes from context did save notification
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // save all changes object context
    [self saveManagedObjectContext];
});

これは、NSManagedObjectContextDidSaveNotification を処理する変更されたメソッドです。

- (void)mergeChangesFromBackground:(NSNotification *)notification
{
    // kill the listener
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // merge changes to the primary context, and wait for the action to complete on the main thread
    [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

    // dispatch the completion block
    dispatch_async(dispatch_get_main_queue(), ^{

        // pull the objects that were saved from the notification so we can get them on the main thread MOC
        NSDictionary *userInfo = [notification userInfo];
        NSMutableArray *modifiedObjects = [[NSMutableArray alloc] init];
        NSSet *insertedObject = (NSSet *)[userInfo objectForKey:@"inserted"];
        NSSet *updatedObject = (NSSet *)[userInfo objectForKey:@"updated"];

        if (insertedObject && insertedObject.count > 0) {
            [modifiedObjects addObjectsFromArray:[insertedObject allObjects]];
        }
        if (updatedObject && updatedObject.count > 0) {
            [modifiedObjects addObjectsFromArray:[updatedObject allObjects]];
        }

        NSMutableArray *objects = [[NSMutableArray alloc] init];

        // iterate through the updated objects and find them in the main thread MOC
        for (NSManagedObject *object in modifiedObjects) {
            NSError *error;
            NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
            if (error) {
                [self logError:error];
            }
            if (obj) {
                [objects addObject:obj];
            }
        }

        modifiedObjects = nil;

        // call the completion block
        saveCompletionBlock(objects);

        // clear the saved completion block
        saveCompletionBlock = nil;
    });
}
4

3 に答える 3

26

私はこれをそこに捨てるつもりです。Core Data Programming Guide に記載されている同時実行のベスト プラクティスに従うのをやめます。Apple は、はるかに使いやすいネストされたコンテキストを追加して以来、それを更新していません。このビデオで詳しく説明しています: https://developer.apple.com/videos/wwdc/2012/?id=214

メイン スレッドを使用するようにプライマリ コンテキストをセットアップします (UI の処理に適しています)。

NSManagedObjectContext * context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setPersistentStoreCoordinator:yourPSC];

同時操作を行う可能性のあるオブジェクトを作成する場合は、使用するプライベート キュー コンテキストを作成します。

NSManagedObjectContext * backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[backgroundContext setParentContext:context];
//Use backgroundContext to insert/update...
//Then just save the context, it will automatically sync to your primary context
[backgroundContext save:nil];

QueueConcurrencyType は、コンテキストがフェッチ (保存およびフェッチ要求) 操作を実行するキューを参照します。NSMainQueueConcurrencyType コンテキストは、メイン キューですべての作業を行うため、UI の操作に適しています。NSPrivateQueueConcurrencyType は、独自のプライベート キューでそれを行います。したがって、backgroundContext で save を呼び出すと、parentContext を呼び出すプライベート データがperformBlock適切に自動的に使用されてマージされます。performBlockデッドロックの原因となるメイン スレッド上にある場合に備えて、プライベート キュー コンテキストで呼び出したくありません。

本当に凝ったものにしたい場合は、プライマリ コンテキストをプライベート キュー同時実行タイプ (バックグラウンド保存に適しています) として作成し、UI だけのメイン キュー コンテキストを作成してから、バックグラウンド操作用のメイン キュー コンテキストの子コンテキストを作成できます (輸入のように)。

于 2013-01-25T17:15:55.040 に答える
7

あなたに合った答えを見つけたようです。しかし、私はいくつかの同様の問題を抱えており、私の経験を共有し、この状況を見ているあなたや他の人にとって役立つかどうかを確認したいと思いました.

マルチスレッド化された Core Data のものは、常に少し読みにくいので、コードを読み違えていた場合はご容赦ください。しかし、もっと簡単な答えがあるようです。

最初の試行で発生した中心的な問題は、メイン スレッドで使用するために、マネージド オブジェクト ID (おそらくスレッド間で受け渡しできるオブジェクト識別子) をグローバル変数に保存したことです。これはバックグラウンド スレッドで行いました。問題は、バックグラウンド スレッドの管理オブジェクト コンテキストに保存する前にこれを行ったことです。オブジェクト ID は、保存前に別のスレッド/コンテキスト ペアに渡すのは安全ではありません。保存すると変更される場合があります。objectID のドキュメントの警告を参照してください: NSManagedObject 参照

バックグラウンド スレッドに保存を通知し、そのスレッド内で、コンテキストが保存されているため安全に使用できるようになったオブジェクト ID を通知オブジェクトから取得することで、これを修正しました。これらはメイン スレッドに渡され、mergeChangesFromContextDidSaveNotification への呼び出しによって、実際の変更もメイン スレッドにマージされました。ここで、1 つまたは 2 つのステップを節約できます。

バックグラウンドスレッドで NSManagedObjectContextDidSaveNotification を聞くために登録しています。代わりに、メインスレッドで同じ通知を聞くように登録できます。その通知には、メイン スレッドで安全に使用できる同じオブジェクト ID が含まれます。メイン スレッドの MOC は、mergeChangesFromContextDidSaveNotification と渡された通知オブジェクトを使用して安全に更新できます。メソッドは次のように機能するように設計されているためです: mergeChanges docs。完了ブロックが呼び出されるスレッドに moc を一致させる限り、どちらのスレッドからの完了ブロックの呼び出しも安全になりました。

そのため、すべてのメイン スレッドの更新をメイン スレッドで行うことができ、スレッドをきれいに分離し、更新されたものをパックして再パックしたり、同じ変更を永続ストアに二重に保存したりする必要がなくなります。

明確にするために-発生するMergeは管理対象オブジェクトのコンテキストとそのメモリ内状態にあります-メインスレッドのモックはバックグラウンドスレッドのモックと一致するように更新されますが、これらをすでに保存しているため、新しい保存は必要ありませんバックグラウンド スレッドでストアに変更します。バックグラウンド スレッドで使用したときと同じように、通知オブジェクト内の更新されたオブジェクトにスレッド セーフでアクセスできます。

あなたのソリューションがあなたのために働いていて、リファクタリングする必要がないことを願っていますが、これを見るかもしれない他の人のために私の考えを追加したかった. あなたのコードを誤解した場合はお知らせください。修正します。

于 2013-01-25T16:43:40.350 に答える
5

あなたの場合、バックグラウンドモックへの書き込みは、mergeChangesFromContextDidSaveNotificationの通知が、フォアグラウンドモックではなく、バックグラウンドモックで受信されるためです。

そのため、バックグラウンドモックオブジェクトに到達するバックグラウンドスレッドの通知を登録する必要があります。

その呼び出しを受信すると、mergeChangesFromContextDidSaveNotificationにメッセージをメインスレッドmocに送信できます。

アンドリュー

更新:これが機能するはずのサンプルです

    //register for this on the background thread
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:backgroundMOC];

- (void)mergeChanges:(NSNotification *)notification {
    NSManagedObjectContext *mainThreadMOC = [singleton managedObjectContext];

    //this tells the main thread moc to run on the main thread, and merge in the changes there
    [mainThreadMOC performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
}
于 2012-10-02T14:54:07.143 に答える