首页 > 解决方案 > 在异步加载文件时更新 NSWindow 内容

问题描述

在我基于文档的 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"队列中)只记录一次或两次,然后记录似乎暂停,直到文件被加载。然后这条丢失的消息连续出现几次,因为它之前被暂停了。

我做错了什么?我认为用于文件加载的后台队列不应该影响主队列和 UI 更新。许多应用程序的行为都是这样,我需要这样的行为,因为我的应用程序中的文件(字符串、csv、json)可能有数百兆字节!

标签: macosuser-interfacegrand-central-dispatchappkitnsprogressindicator

解决方案


对于初学者,您是否研究过 NSURLSessionTask 的进度报告 API?有很多工具可以测量数据并作为数据加载被回调。您可能会避免进行轮询,而只是在回调这些 UI 时更新您的 UI - 所有这些都在主线程上。

事实上,你甚至可能根本不需要一个。我记得,NSURLSession 执行的网络操作都是在后台完成的。我敢打赌,您可以删除所有这些,只需使用 NSURLSession API 中的一些额外委托回调即可完成此操作。方式也更简单:)

祝你好运!


推荐阅读