Обновить содержимое NSWindow при асинхронной загрузке файла - PullRequest
0 голосов
/ 29 июня 2018

В моем приложении MacOS для документов у меня есть несколько больших файлов, которые загружаются (особенно недавние файлы, открытые при запуске приложения). Я создал ProgressController (подкласс NSWindowController), чтобы сообщить пользователю в окне, что идет загрузка файла. Он выделяется методом makeWindowControllers подкласса NSDocument, который я использую для управления документами. Они создаются, когда пользователь открывает файл (и особенно при запуске, когда документы отображались, когда пользователь вышел из приложения), и они загружают свое содержимое асинхронно в фоновую очередь, что в принципе не должно влиять на производительность основного потока:

-(instancetype) initForURL:(NSURL *)urlOrNil withContentsOfURL:(NSURL *)contentsURL ofType:(NSString *)typeName error:(NSError *
{
  if (self = [super init]){
    ... assign some variables before loading content...
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0), ^{

            [[[NSURLSession sharedSession] dataTaskWithURL:urlOrNil completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                self.content =  [[NSMutableString alloc] initWithData:data encoding:encoder];
                dispatch_async(dispatch_get_main_queue(), ^(){
                    [self contentIsLoaded];
                });
            }] resume];
        });
  }
  return self;
}

В contentIsLoaded окно прогресса закрывается.

- (void) contentIsLoaded
{
    ... do something ...
    [self.progressController close];
}

Это поведение в порядке, окно отображается и закрывается при необходимости. Проблема возникает, когда я хочу обновить содержимое этого окна в основной очереди. Я попытался установить NSTimer, но он никогда не запускается, даже если он создается в основной очереди. Итак, в MyApplicationDelegate я создал таймер GCD следующим образом:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
             if (self->timer !=nil) dispatch_source_cancel(timer);
             self.queue =  dispatch_queue_create( "my session queue", DISPATCH_QUEUE_CONCURRENT);
             timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
             dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
             dispatch_source_set_event_handler(timer, ^{
                [self updateProgress];
             });
             dispatch_resume(timer);
            …
    }

В MyApplicationDelegate метод updateProgress определяется как:

- (void) updateProgress
{
    dispatch_async(self.queue, ^{
        NSLog(@"timer method fired");
        dispatch_async(dispatch_get_main_queue(), ^(){
            NSLog(@"access to UI fired");
            ... update window (e.g. NSProgressIndicator)
            }
        });
        if (self.shouldCancelTimer) dispatch_source_cancel(self->timer);
    });
}

При запуске приложения «Timer method fired» регистрируется каждую секунду. Сообщение "timer method fired" (в основной очереди) регистрируется только один или два раза, затем запись кажется приостановленной, пока файл не будет загружен. Затем это пропущенное сообщение появляется несколько раз подряд, как и раньше.

Что я не так сделал? Я предположил, что фоновая очередь, используемая для загрузки файлов, не должна влиять на основную очередь и обновления пользовательского интерфейса. Многие приложения ведут себя так, и мне нужно такое поведение, поскольку файлы в моем приложении (строки, CSV, JSON) могут быть сотни мегабайт!

1 Ответ

0 голосов
/ 08 июля 2018

Для начала, вы смотрели API-интерфейсы отчетов о прогрессе NSURLSessionTask? Существует множество средств для измерения и обратного вызова при загрузке данных. Возможно, вам удастся избежать опроса и просто обновить свой пользовательский интерфейс, так как он вызывается обратно - все в главном потоке.

На самом деле, вам может даже и не понадобиться. Насколько я помню, сетевые операции, выполняемые NSURLSession, все равно выполняются в фоновом режиме. Могу поспорить, что вы можете удалить все это, и просто использовать некоторые дополнительные функции обратного вызова делегата из API NSURLSession, чтобы сделать это. Намного проще:)

Удачи!

...