UIManagedDocument Singleton Code openWithCompletionHandler вызывается дважды и аварийно завершает работу

Я использую реализацию Джастина Дрисколла в Core Данные с одним общим UIManagedDocument. В моем приложении для iphone все было хорошо, пока я не перенес его на раскадровку iPad и контроллер с разделенным экраном для приложения ipad. Проблема в том, что openwithCompletionHandler вызывается дважды, один раз из моего основного представления в viewDidLoad и снова в моем подробном представлении viewWillLoad. Вызовы выполняются в быстрой последовательности, и, поскольку документ все еще находится в UIDocumentStateClosed, когда выполняется второй вызов моего метода executeWithDocument (ниже) синглтона, приложение аварийно завершает работу. Я посмотрел ответ e_x_p на сообщение iOS5.1: синхронизация задач ( дождитесь завершения), но @sychronized в этом случае не сработает, так как в том же потоке вызывается PerformWithDocument ниже. Как мне защититься от нескольких вызовов openwithCompletionHandler? Единственный способ защититься от этого — приостановить выполнение одного из вышеперечисленных вызовов, пока я не буду уверен, что UIDocumentStateNormal имеет значение true, а затем отпустить. Это, однако, заморозило бы основной поток пользовательского интерфейса, что не очень хорошо. Что, однако, было бы лучшим способом сделать это, не замораживая пользовательский интерфейс?

Из кода UIManagedDocumentSingleton:

- (void)performWithDocument:(OnDocumentReady)onDocumentReady
{
    void (^OnDocumentDidLoad)(BOOL) = ^(BOOL success)
    {
        onDocumentReady(self.document);
    };

    if (![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]])
    {
        //This should never happen*******************
        [self.document saveToURL:self.document.fileURL
                forSaveOperation:UIDocumentSaveForCreating
               completionHandler:OnDocumentDidLoad];

    } else if (self.document.documentState == UIDocumentStateClosed) {
        [self.document openWithCompletionHandler:OnDocumentDidLoad];
    } else if (self.document.documentState == UIDocumentStateNormal) {
        OnDocumentDidLoad(YES);
    }
}

person William Bagdan    schedule 20.11.2012    source источник


Ответы (3)


Я сделал это, как предложил Джастин выше ниже. Отлично работает в одном из моих приложений в течение двух лет с ~ 20 000 пользователей.

@interface SharedUIManagedDocument ()  
@property (nonatomic)BOOL preparingDocument; 
@end

- (void)performWithDocument:(OnDocumentReady)onDocumentReady
{
    void (^OnDocumentDidLoad)(BOOL) = ^(BOOL success) {
        onDocumentReady(self.document);
        self.preparingDocument = NO; // release in completion handler
    };

    if(!self.preparingDocument) {
        self.preparingDocument = YES; // "lock", so no one else enter here
        if(![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]]) {
            [self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:OnDocumentDidLoad];
        } else if (self.document.documentState == UIDocumentStateClosed) {
            [self.document openWithCompletionHandler:OnDocumentDidLoad];
        } else if (self.document.documentState == UIDocumentStateNormal) {
            OnDocumentDidLoad(YES);
        }
    } else {
        // try until document is ready (opened or created by some other call)
        [self performSelector:@selector(performWithDocument:) withObject:onDocumentReady afterDelay:0.5];
    }
}

Swift (не тестировалось)

typealias OnDocumentReady = (UIManagedDocument) ->()

class SharedManagedDocument {

private let document: UIManagedDocument
private var preparingDocument: Bool

static let sharedDocument = SharedManagedDocument()

init() {
    let fileManager = NSFileManager.defaultManager()
    let urls = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
    let documentsDirectory: NSURL = urls.first as! NSURL
    let databaseURL = documentsDirectory.URLByAppendingPathComponent(".database")
    document = UIManagedDocument(fileURL: databaseURL)
    let options = [NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true]
    document.persistentStoreOptions = options
    preparingDocument = false
}

func performWithDocument(onDocumentReady: OnDocumentReady) {

    let onDocumentDidLoad:(Bool) ->() = {
        success in
        onDocumentReady(self.document)
        self.preparingDocument = false
    }
    if !preparingDocument {
        preparingDocument = true
        if !NSFileManager.defaultManager().fileExistsAtPath(document.fileURL.path!) {
            println("Saving document for first time")
            document.saveToURL(document.fileURL, forSaveOperation: .ForCreating, completionHandler: onDocumentDidLoad)
        } else if document.documentState == .Closed {
            println("Document closed, opening...")
            document.openWithCompletionHandler(onDocumentDidLoad)
        } else if document.documentState == .Normal {
            println("Opening document...")
            onDocumentDidLoad(true)
        } else if document.documentState == .SavingError {
            println("Document saving error")
        } else if document.documentState == .EditingDisabled {
            println("Document editing disabled")
        }
    } else {
        // wait until document is ready (opened or created by some other call)
        println("Delaying...")
        delay(0.5, closure: {
            self.performWithDocument(onDocumentReady)
        })
    }
}

private func delay(delay:Double, closure:()->()) {
    dispatch_after(
        dispatch_time(
            DISPATCH_TIME_NOW,
            Int64(delay * Double(NSEC_PER_SEC))
        ),
        dispatch_get_main_queue(), closure)
}
}
person Vladimir Shutyuk    schedule 18.06.2013
comment
// заблокировать, чтобы сюда больше никто не входил - это очень большая ошибка - person adnako; 12.05.2014
comment
@adnako, если вы говорите о лингвистике, я согласен, что замок здесь не очень хорошее слово, если это просто флаг. Но если вы считаете, что у этого подхода есть слабые стороны, пожалуйста, объясните, мне действительно интересно! - person Vladimir Shutyuk; 12.05.2014
comment
Нет, я говорю о многопоточности. Этот код должен работать только в одной очереди. Я тоже использую этот шаблон и сейчас пытаюсь решить проблему с вызовом [self.document openWithCompletionHandler:OnDocumentDidLoad]; дважды. - person adnako; 12.05.2014
comment
@adnako Это синглтон, реализованный с помощью GCD dispatch_once (описанный, например, в Effective Objective-C 2.0). Так что нам гарантирует iOS (можете возразить, но тут не в этом дело) здесь экземпляр один и только с кодом dispatch_once. Что оставило нам одну-единственную переменную self.preparingDocument. - person Vladimir Shutyuk; 13.05.2014
comment
Ты не понимаешь. dispatch_once работает только при создании статического ivar. Все остальное хорошо страдает от проблем с многопоточностью. Я пришлю вам простой пример позже вечером. - person adnako; 13.05.2014
comment
Я не могу быстро сделать пример, который постоянно вылетает. Запустите этот проект несколько раз, он должен рухнуть после нескольких запусков. github.com/pomozoff/CoreDataExample - person adnako; 14.05.2014
comment
Спасибо, что уделили время, но у меня есть этот код в производстве уже полгода и тысячи сессий в день, и он никогда не зависал. Этот BOOL предназначен для создания/открытия документов, поэтому тот, кто доберется туда первым, закроет дверь и сделает свою работу. - person Vladimir Shutyuk; 14.05.2014

Это интересно и определенно является недостатком моего кода (извините!). Моей первой мыслью было добавить последовательную очередь в качестве свойства к вашему классу обработчика документов и выполнить ее проверку.

self.queue = dispatch_queue_create("com.myapp.DocumentQueue", NULL);

а затем в PerformWithDocument:

dispatch_async(self.queue, ^{
    if (![[NSFileManager defaultManager] fileExistsAtPath... // and so on
});

Но и это не сработает...

Вы можете установить флаг BOOL при вызове saveToURL и очистить его в обратном вызове. Затем вы можете проверить наличие этого флага и использовать PerformSelectorAfterDelay для повторного вызова executeWithDocument чуть позже, если файл создается.

person Justin Driscoll    schedule 16.04.2013

Блок кода, общий для numberOfRowsInSection: и cellForRowAtIndexPath:, должен вызываться только один раз. numberOfRowsInSection всегда будет вызываться до того, как tableView попытается отобразить ячейки, поэтому вам следует создать объект NSArray, в котором вы сможете хранить результаты запроса на выборку, а затем использовать этот массив при рендеринге ваших ячеек:

@implementation FooTableViewController {
    NSArray *_privateArray;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    [[UIManagedDocumentSingletonHandler sharedDocumentHandler] performWithDocument:^(FCUIManagedDocumentObject *document) {
        NSManagedObjectContext * context =  document.managedObjectContext;

        NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"FCObject"];
        NSPredicate * searchStringPredicate = nil;
        if (searchFilterString)
        {
            searchStringPredicate = [NSPredicate predicateWithFormat:@"word BEGINSWITH[c] %@",searchFilterString];
        }
        request.predicate = searchStringPredicate;
        request.shouldRefreshRefetchedObjects = YES;
        NSError * error;
        _privateArray = [context executeFetchRequest:request error:&error];
        }];
    return _privateArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"FCCell";
    FCardCell *cell = (FCCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    // Configure the cell...
    FCManagedObject * fcc = [_privateArray objectAtIndex:indexPath.row];
    cell.isWordVisible.on = fcc.isUsed;
    cell.fWord.text = fcc.word;
    return cell;
}

Я не уверен, что вам нужно сделать что-то особенное с NSArray, чтобы установить его внутри блока (а-ля __block).

Основная причина этого заключается в том, что вам необходимо убедиться, что в 100% случаев набор данных, используемый для определения количества строк, имеет тот же размер, что и при создании ячеек. Если они не совпадают, вы потерпите крах. Кроме того, поскольку у вас нет блока, вам не нужно выполнять диспетчеризацию для выполнения UITableViewCell обновлений.

Наконец, если UIDocumentStateClosed вызывает проблемы, вы должны либо отфильтровать их из результатов NSFetch (дополнительный предикат, см. NSCompoundPredicate, если требуется), либо иметь код для лучшей обработки в cellForRowAtIndexPath:.

person Michael Kernahan    schedule 21.11.2012
comment
Я объединил запрос на выборку в один первоначальный вызов, который теперь используют cellForRowAtIndexPath и numberOfRowsInSection, поэтому проблема больше не возникает, но проблема все еще существует. Я обновил вопрос, чтобы упростить его. - person William Bagdan; 22.11.2012
comment
не могли бы вы @synchronized (documentObject) синхронизировать сам объект? За исключением этого, вы можете делегировать полномочия от мастера/подробности, чтобы только один из них должен был сделать вызов (или, если они имеют гарантированный порядок выполнения, вы могли бы передать сообщение между ними (вероятно, через @protocol) для выполнения на второй, когда первый сделан. - person Michael Kernahan; 22.11.2012