【问题标题】:How to download a file from a URL in OS X app如何从 OS X 应用程序中的 URL 下载文件
【发布时间】:2026-01-20 01:35:01
【问题描述】:

如何从 OS X 应用程序中的 URL 下载文件?我是一名 Unix C 和 Perl 程序员,刚接触 Objective C。我一直在阅读 URL Loading System Programming Guide,它建议 NSURLSession 用于新的 OS X 应用程序。所以我更喜欢 NSURLSession 或 NSURLSeesionDownloadTask 的解决方案,但我对 NSURLDownload 或其他解决方案持开放态度。

我的第一次尝试是基于找到的示例 here

        NSURLSessionDownloadTask *downloadTask =
        [[NSURLSession sharedSession]
         downloadTaskWithURL:[NSURL URLWithString:pdfFile]
         completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {

             // move tmp file to permanent location
             NSLog(@"Done...");
             NSLog(@"Location:   %@", location);
             NSLog(@"Response:   %@", response);
             NSLog(@"Error       %@", error);

             NSFileManager *fileManager = [NSFileManager defaultManager];
             BOOL fileCopied = [fileManager moveItemAtPath:[location path] toPath:[appDir stringByAppendingString:@"/demo.pdf"] error:&error];
             NSLog(fileCopied ? @"File Copied OK" : @"ERROR Copying file.");
            }];
    [downloadTask resume];
    NSLog(@"Now we are here");

示例代码(和我的版本)没有单独的委托。完成处理程序是“在线的”。 (不确定这是否是正确的术语)。我假设此代码将在下载任务完成后执行。对吗?

我的第二次尝试是基于 URL 编程指南的“下载文件”部分:

    // PDF file Download, try #2
    NSError *error = nil;
    NSString *appDir = NSHomeDirectory();
    NSString *pdfFile = @"https://developer.apple.com/library/mac/documentation/IDEs/Conceptual/AppDistributionGuide/AppDistributionGuide.pdf";

    // Configure Cache behavior for default session
    NSURLSessionConfiguration
    *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSString *cachePath = [NSTemporaryDirectory()
                           stringByAppendingPathComponent:@"/pdfDownload.cache"];
    NSLog(@"Cache path: %@\n", cachePath);
    NSURLCache *myCache = [[NSURLCache alloc] initWithMemoryCapacity: 16384
                                                        diskCapacity: 268435456 diskPath: cachePath];
    defaultConfigObject.URLCache = myCache;
    defaultConfigObject.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
    defaultConfigObject.timeoutIntervalForRequest = 100;
    defaultConfigObject.timeoutIntervalForResource = 100;

    // Create a delegate-Free Session
    NSURLSession *delegateFreeSession =
        [NSURLSession sessionWithConfiguration: defaultConfigObject
                                      delegate: nil
                                 delegateQueue: [NSOperationQueue mainQueue]];

    [[delegateFreeSession downloadTaskWithURL:[NSURL URLWithString:pdfFile] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {

        // move tmp file to permanent location
        NSLog(@"Done...");
        NSLog(@"Location:   %@", location);
        NSLog(@"Response:   %@", response);
        NSLog(@"Error       %@", error);

        NSFileManager *fileManager = [NSFileManager defaultManager];
        BOOL fileCopied = [fileManager moveItemAtPath:[location path] toPath:[appDir stringByAppendingString:@"/demo.pdf"] error:&error];
        NSLog(fileCopied ? @"File Copied OK" : @"ERROR Copying file.");
    }] resume];

    sleep(10);
    NSLog(@"After sleep...");

在这两种情况下,代码执行永远不会到达作为completionHandler 一部分的NSLog 语句。我在模拟器中设置了断点,但从未到达那里。如何创建简单的“免费委托”文件下载方法?

由于我正在编写“命令行”OS X 应用程序,因此我没有 UIViewController 或其他现有的委托结构。

关于如何进行的任何建议?我不需要下载进度条等,只需要“通过/失败”状态。

另外,我的示例是针对单个 PDF 文件的。我的应用程序有一系列我想下载的 URL。我不需要并行下载它们。我可以在循环中重复使用单个 NSURLSession 吗?

任何帮助将不胜感激!

【问题讨论】:

  • 如果问题是您的请求超时,您可能没有给它足够的时间使其失败并打印错误消息。这取决于您的超时设置(可能是默认值)。
  • @ZevEisenberg:我添加了第二个代码片段,其中包含一些详细配置,包括高超时值。仍然永远不会到达完成处理程序。我认为这应该在模拟器中工作,对吗?
  • 什么模拟器?你说这是一个 OS X 应用程序。
  • @ZevEisenberg:Xcode 模拟器。我的目标是 OS X,但目前我仍在模拟器中运行。
  • Xcode 模拟器 = OSX

标签: objective-c xcode macos


【解决方案1】:

得到解决方案后忘记关闭它。正如 Zev 所指出的,我选择的 PDF 文件很大,需要更多时间来下载。 10 秒的“sleep(10)”没有提供足够的时间来下载 39 MB 文件。

应该使用通知来指示completionHandler 何时完成。但是现在,我正在使用一个简单的轮询循环。我改变了睡眠(10);到

int  timeoutCounter = 60;
while ((timeoutCounter > 0) && !downloadDone) {
    sleep(1);
    NSLog(@"Timout: %d", timeoutCounter--);
}

if (!downloadDone) {
    NSLog(@"ERROR: Timeout occurred before file completed downloading");
}

这需要声明downloadDone以允许completionHandler块设置它:

    __block BOOL downloadDone = FALSE;

NSFileManger 移动文件后,必须设置这个信号量:

     downloadDone = TRUE;

再次感谢 Zev 的帮助!

我仍在进行错误检查/处理,但这里是完整的更正方法:

+ (NSError *)           downloadFileAtURL:(NSString *)fileURL
                                toDir:(NSString *)toDir
                              timeout:(NSNumber *)timeout {

// completion handler semaphore to indicate download is done
__block BOOL downloadDone = FALSE;

// same PDF file name for later
NSString *downloadFileName = [fileURL lastPathComponent];
NSString *finalFilePath = [toDir stringByAppendingPathComponent:downloadFileName];
NSLog(@"PDF file name: '%@'", downloadFileName);
NSLog(@"Final file path: '%@'", finalFilePath);

// Configure Cache behavior for default session
NSURLSessionConfiguration
*defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSString *cachePath = [NSTemporaryDirectory()
                       stringByAppendingPathComponent:@"/downloadFileCache"];
NSLog(@"Cache path: %@\n", cachePath);
NSURLCache *myCache = [[NSURLCache alloc] initWithMemoryCapacity: 16384
                                                    diskCapacity: 268435456 diskPath: cachePath];
defaultConfigObject.URLCache = myCache;
defaultConfigObject.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
defaultConfigObject.timeoutIntervalForRequest = 100;
defaultConfigObject.timeoutIntervalForResource = 100;

// Create a delegate-Free Session
//      delegateQueue: [NSOperationQueue mainQueue]];
NSURLSession *delegateFreeSession =
[NSURLSession sessionWithConfiguration: defaultConfigObject
                              delegate: nil
                         delegateQueue: nil];

[[delegateFreeSession downloadTaskWithURL:[NSURL URLWithString:fileURL] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
    
    // move tmp file to permanent location
    NSLog(@"In the completion handler...");
    NSLog(@"Location:   %@", location);
    NSLog(@"Response:   %@", response);
    NSLog(@"Error       %@", error);
    NSLog(@"Location path: %@", [location path]);
    
    // Verify temp file exists in cache
    NSFileManager *fileManager = [[NSFileManager alloc] init];
    if ( [fileManager fileExistsAtPath:[location path]]) {
        NSLog(@"File exists.  Now move it!");
        
        error = nil;
        BOOL fileCopied = [fileManager moveItemAtPath:[location path]
                                               toPath:finalFilePath
                                                error:&error];
        NSLog(@"Error: %@", error);
        NSLog(fileCopied ? @"File Copied OK" : @"ERROR Copying file.");
    }
    downloadDone = TRUE;
    
}] resume];

// use timeout argument
int  timeoutCounter = [timeout intValue];
while ((timeoutCounter > 0) && !downloadDone) {
    sleep(1);
    NSLog(@"Timout: %d", timeoutCounter--);
}

if (!downloadDone) {
    NSLog(@"ERROR: Timeout occurred before file completed downloading");
}

NSError *error = nil;
return error;
}

【讨论】:

  • 不要使用sleep()。运行run loopdispatch_main()。如果您想要超时,请使用NSTimer(需要运行运行循环)或dispatch_after()。计时器可以无效,如果您的程序要继续运行做其他事情,则需要这样做。如果您只是在下载完成后退出,也没关系。
  • 感谢运行循环和 dispatch_main() 的建议,Ken。我是 Objective C 的新手,所以这个问题的目标是了解 NSURLSession 和完成处理程序。我已将线程编程指南添加到我需要阅读的文档列表中。感谢您的评论!
最近更新 更多