1

最初にいくつかのデータを UIManagedDocument にロードしてから実行するアプリケーションがありますsaveToURL:forSaveOperation:completionHandler:。completionHandler ブロック内で、このデータベースのさまざまな要素の更新を行い、それが完了すると、別の保存を行います。

それに加えて、アプリには、データの再読み込み、データの再更新、およびデータベースの 1 つのエンティティの削除をそれぞれ行う 3 つのボタンがあります。どのボタン方式でも、最後の命令は節約にもなります。

これらすべてをシミュレーターで実行すると、すべてがスムーズに進みます。しかし、デバイスではそうではありません。常にクラッシュします。通常、「削除」ボタンを押すか、データベースを再ロードまたは再更新するとクラッシュすることがわかりました。そして、それは常にsaveToURL操作にあります。私の意見では、データベースを保存するスレッド
が複数ある場合に問題が発生します。デバイスのコード実行が遅くなると、複数の節約が同時に発生し、アプリがそれらを正しく処理できなくなる可能性があります。また、削除ボタンでエンティティが削除されず、エンティティが存在しないと表示される場合があります (存在する場合)。

私はこれに完全に困惑しており、このすべての保存操作を実行する必要があります...実際、それらを削除すると、アプリはさらに一貫性のない動作をします。

この問題を解決するために何ができるかについての提案はありますか? どうもありがとうございました!

[編集] ここに問題のあるコードを投稿します。最初にデータをロードするために、ヘルパー クラスを使用します。具体的には、次の 2 つのメソッドを使用します。

+ (void)loadDataIntoDatabase:(UIManagedDocument *)database
{
    [database.managedObjectContext performBlock:^{
        // Read from de plist file and fill the database
        [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {
            [DataHelper completeDataOfDatabase:database];
        }];
}

+ (void)completeDataOfDatabase:(UIManagedDocument *)database
{
    [database.managedObjectContext performBlock:^{

        // Read from another plist file and update some parameters of the already existent data (uses NSFetchRequest and works well)

        // [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
        [database updateChangeCount:UIDocumentChangeDone];

    }];
}  

ビューには、次のような 3 つのアクション メソッドがあります。

- (IBAction)deleteButton {

    [self.database.managedObjectContext performBlock:^{
        NSManagedObject *results = ;// The item to delete
        [self.database.managedObjectContext deleteObject:results];

            //  [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
        [self.database updateChangeCount:UIDocumentChangeDone];
        }];
}

- (IBAction)reloadExtraDataButton {

    [DataHelper loadDataIntoDatabase:self.database];

    // [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
    [self.database updateChangeCount:UIDocumentChangeDone];

}

- (IBAction)refreshDataButton {

    [DataHelper completeDataOfDatabase:self.database];
    //[self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
    [self.database updateChangeCount:UIDocumentChangeDone];
}

[編集 2] コードの追加: まず、最初のビューは次のように viewDidLoad を実行します。

- (void)viewDidLoad{
    [super viewDidLoad];
    self.database = [DataHelper openDatabaseAndUseBlock:^{
        [self setupFetchedResultsController];
    }];
}

setupFetchedResultsController メソッドは次のようになります。

- (void)setupFetchedResultsController
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Some entity name"];
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)]];

    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                                        managedObjectContext:self.database.managedObjectContext
                                                                          sectionNameKeyPath:nil
                                                                                   cacheName:nil];
}

アプリの各ビュー (タブがあります) には、データベースに含まれるさまざまなエンティティを表示するために、異なる setupFetchedResultsController があります。

ヘルパー クラスでは、これが各ビューの viewDidLoad を介して実行される最初のクラス メソッドです。

+ (UIManagedDocument *)openDatabaseAndUseBlock:(completion_block_t)completionBlock
{
    NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    url = [url URLByAppendingPathComponent:@"Database"];
    UIManagedDocument *database = [[UIManagedDocument alloc] initWithFileURL:url];

    if (![[NSFileManager defaultManager] fileExistsAtPath:[database.fileURL path]]) {

        [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
            [self loadDataIntoDatabase:database];
            completionBlock();
        }];

    } else if (database.documentState == UIDocumentStateClosed) {
        // Existe, pero cerrado -> Abrir
        [database openWithCompletionHandler:^(BOOL success) {
            [self loadDataIntoDatabase:database];
            completionBlock();
        }];

    } else if (database.documentState == UIDocumentStateNormal) {
        [self loadDataIntoDatabase:database];
        completionBlock();
    }

    return database;
}
4

1 に答える 1

4

あなたは実際には多くのコードを提供しませんでした。あなたが与えた唯一の本当の手がかりは、あなたが複数のスレッドを使用しているということでした。

UIManagedDocumentには2つのManagedObjectContextがあります(1つはメインキュー用に指定され、もう1つはプライベートキュー用に指定されています)が、それぞれが独自のスレッド内からのみアクセスする必要があります。

したがって、メインスレッド内からのみmanagedDocument.managedObjectContextを使用する必要があります。別のスレッドから使​​用する場合は、performBlockまたはperformBlockAndWaitのいずれかを使用する必要があります。同様に、親コンテキストのプライベートスレッドで実行していることを知ることはできないため、親に対して特別に何かを実行する場合は、performBlock*を使用する必要があります。

最後に、最初にデータベースを作成する場合を除いて、実際にはsaveToURLを呼び出さないでください。UIManagedDocumentは(独自の時間で)自動保存します。

以前に保存するように促したい場合は、updateChangeCount:UIDocumentChangeDoneを送信して、保存する必要のある変更があることを通知できます。

編集

初めてファイルを作成するときにのみ、saveToURLを呼び出す必要があります。UIManagedDocumentを使用すると、再度呼び出す必要はありません(実際には、意図しない問題が発生する可能性があります)。

基本的に、ドキュメントを作成するときは、完了ハンドラーが実行されるまでiVarを設定しないでください。それ以外の場合は、部分的な状態のドキュメントを使用している可能性があります。この場合、完了ハンドラーでこのようなヘルパーを使用します。

- (void)_document:(UIManagedDocument*)doc canBeUsed:(BOOL)canBeUsed
{
    dispatch_async(dispatch_get_main_queue(), ^{
        if (canBeUsed) {
            _document = doc;
            // Now, the document is ready.
            // Fire off a notification, or notify a delegate, and do whatever you
            // want... you really should not use the document until it's ready, but
            // as long as you leave it nil until it is ready any access will
            // just correctly do nothing.
        } else {
            _document = nil;
            // Do whatever you want if the document can not be used.
            // Unfortunately, there is no way to get the actual error unless
            // you subclass UIManagedDocument and override handleError
        }
    }];
}

そして、ドキュメントを初期化するには、次のようにします...

- (id)initializeDocumentWithFileURL:(NSURL *)url
{
    if (!url) {
        url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
        url = [url URLByAppendingPathComponent:@"Default_Project_Database"];
    }
    UIManagedDocument *doc = [[UIManagedDocument alloc] initWithFileURL:url];

    if (![[NSFileManager defaultManager] fileExistsAtPath:[doc.fileURL path]]) {
        // The file does not exist, so we need to create it at the proper URL
        [doc saveToURL:doc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
            [self _document:doc canBeUsed:success];
        }];
    } else if (doc.documentState == UIDocumentStateClosed) {
        [doc openWithCompletionHandler:^(BOOL success) {
            [self _document:doc canBeUsed:success];
        }];
    } else {
        // You only need this if you allow a UIManagedDocument to be passed
        // in to this object -- in which case the code above that initializes
        // the <doc> variable will be conditional on what was passed...
        BOOL success = doc.documentState == UIDocumentStateNormal;
        [self _document:doc canBeUsed:success];
    }
}

上記の「パターン」は、完全に使用できるようになるまでドキュメントを使用しないようにするために必要です。これで、saveToURLを呼び出すのはそのコードだけです。

定義上、document.managedObjectContextのタイプはNSMainQueueConcurrencyTypeであることに注意してください。したがって、コードがメインスレッドで実行されていることがわかっている場合(すべてのUIコールバックのように)、performBlockを使用する必要はありません。

ただし、実際にバックグラウンドでロードを実行している場合は、検討してください。

- (void)backgroundLoadDataIntoDocument:(UIManagedDocument*)document
{
    NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    moc.parentContext = document.managedObjectContext;
    [moc performBlock:^{
        // Do your loading in here, and shove everything into the local MOC.
        // If you are loading a lot of stuff from the 'net (or elsewhere),
        // consider doing it in strides, so you deliver objects to the document
        // a little at a time instead of all at the end.

        // When ready to save, call save on this MOC.  It will shove the data up
        // into the MOC of the document.
        NSrror *error = nil;
        if ([moc save:&error]) {
            // Probably don't have to synchronize calling updateChangeCount, but I do it anyway...
            [document.managedObjectContext performBlockAndWait:^{
                [document updateChangeCount:UIDocumentChangeDone];
            }];
        } else {
            // Handle error
        }
    }];
}

バックグラウンドMOCをmainMOCにペアレント化する代わりに、parentContextにペアレント化することができます。ロードしてから保存すると、変更がメインMOCの「上」に配置されます。メインMOCは、次にフェッチ操作を実行するときにこれらの変更を確認します(NSFetchRequestのプロパティに注意してください)。

注:一部の人々は、最初のsaveToURLの後で、すべてを正しく機能させるために閉じてから開く必要があると報告しています(Erica Sadunの本にもメモとして表示されています)。

編集

これは本当に長くなっています。もっとポイントがあれば、チャットをお勧めします。実際には、SOを介して実行することはできませんが、別のメディアを介して実行することはできます。簡潔にしようと思いますが、戻って投稿内容を読み直し、コードがまだいくつかのテナントに違反しているので注意してください。

まず、viewDidLoad()で、openDatabaseAndUseBlockを呼び出した結果にドキュメントを直接割り当てます。その時点では、ドキュメントは使用可能な状態ではありません。完了ハンドラーが起動するまでドキュメントにアクセスできないようにします。これは、openDatabaseAndUseBlock()が戻る前には発生しません。

次に、データベースを最初に作成するとき(openDatabaseAndUseBlock()内)にのみsaveToURLを呼び出します。他の場所では使用しないでください。

第3。すべてのイベントを受信するには、通知センターに登録してください(ログに記録するだけです)。何が起こっているかを確認できるため、これはデバッグに大いに役立ちます。

第4に、UIManagedDocumentをサブクラス化し、handleErrorをオーバーライドして、呼び出されているかどうかを確認します。これが、発生した場合に正確なNSErrorを確認する唯一の方法です。

3/4は主にデバッグを支援するためのものであり、本番コードには必要ありません。

約束があるので、今すぐやめなければなりません。ただし、これらの問題に対処してください。

于 2012-05-03T12:19:07.403 に答える