1
votes

I have an application that first loads some data into an UIManagedDocument, then executes saveToURL:forSaveOperation:completionHandler:. Inside the completionHandler block, it does an update of various elements of this database, and when it's done, it does another saving.

Besides that, the app has 3 buttons that reload the data, re-update the data, and delete one entity of the database, respectively. In every button method, the last instruction is a saving as well.

When I run all this in the simulator, all goes smoothly. But in the device doesn't. It constantly crashes. I have observed that, normally, it crashes when pressing the "delete" button, or when reloading or re-updating the database. And it's always in the saveToURL operation.
In my opinion, the problem comes when there are multiple threads saving the database. As the device executes the code slower, maybe multiple savings come at same time and the app can't handle them correctly. Also, sometimes the delete button doesn't delete the entity, and says that doesn't exist (when it does).

I'm totally puzzled with this, and all this saving operations must be done...In fact, if I remove them, the app behaves even more incoherently.

Any suggestions of what could I do to resolve this problem? Thank you very much!

[Edit] Here I post the problematic code. For first loading the data, I use a helper class, with this two methods in particular:

+ (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];

    }];
}  

And in the view, I have 3 action methods, like these:

- (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];
}

[Edit 2] More code: First of all, the initial view executes viewDidLoad this way:

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

This is what the setupFetchedResultsController method looks like:

- (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];
}

Each view of the app (it has tabs) has a different setupFetchedResultsController in order to show the different entities the database contains.

Now, in the helper class, this is the first class method that gets executed, via the viewDidLoad of each view:

+ (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;
}
1

1 Answers

4
votes

You didn't really provide much code. The only real clue you gave was that you are using multiple threads.

UIManagedDocument has two ManagedObjectContexts (one specified for the main queue, and the other for a private queue), but they still must each only be accessed from within their own thread.

Thus, you must only use managedDocument.managedObjectContext from within the main thread. If you want to use it from another thread, you have to use either performBlock or performBlockAndWait. Similarly, you can never know you are running on the private thread for the parent context, so if you want to do something specifically to the parent, you must use performBlock*.

Finally, you really should not be calling saveToURL, except when you initially create the database. UIManagedDocument will auto-save (in its own time).

If you want to encourage it to save earlier, you can send it updateChangeCount: UIDocumentChangeDone to tell it that it has changes that need to be saved.

EDIT

You should only call saveToURL when you create the file for the very first time. With UIManagedDocument, there is no need to call it again (and it can actually cause some unintended issues).

Basically, when you create the document DO NOT set your iVar until the completion handler executes. Otherwise, you could be using a document in a partial state. In this case, use a helper, like this, in the completion handler.

- (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
        }
    }];
}

And to initialize your document, something like...

- (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];
    }
}

The "pattern" above is necessary to make sure you do not use the document until it is fully ready for use. Now, that piece of code should be the only time you call saveToURL.

Note that by definition, the document.managedObjectContext is of type NSMainQueueConcurrencyType. Thus, if you know your code is running on the main thread (like all your UI callbacks), you do not have to use performBlock.

However, if you are actually doing loads in the background, consider..

- (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
        }
    }];
}

Instead of parenting your background MOC to the mainMOC, you can parent it to the parentContext. Loading and then saving into it will put the changes "above" the main MOC. The main MOC will see those changes the next time it does a fetch operation (note the properties of NSFetchRequest).

NOTE: Some people have reported (and it also appears as a note in Erica Sadun's book), that after the very first saveToURL, you need to close, then open to get everything working right.

EDIT

This is getting really long. If you had more points, I'd suggest a chat. Actually, we can't do it through SO, but we could do it via another medium. I'll try to be brief, but please go back and reread what I posted, and pay careful attention because your code is still violating several tenants.

First, in viewDidLoad(), you are directly assigning your document to the result of calling openDatabaseAndUseBlock. The document is not in a usable state at that time. You do not want the document accessible until the completion handlers fire, which will not happen before openDatabaseAndUseBlock() returns.

Second, only call saveToURL the very first time you create your database (inside openDatabaseAndUseBlock()). Do not use it anywhere else.

Third. Register with the notification center to receive all events (just log them). This will greatly assist your debugging, because you can see what's happening.

Fourth, subclass UIManagedDocument, and override the handleError, and see if it is being called... it's the only way you will see the exact NSError if/when it happens.

3/4 are mainly to help you debug, not necessary for your production code.

I have an appointment, so have to stop now. However, address those issues, and here's on