返回

NSURLSession 下载:URL 使用详解及避坑指南

IOS

NSURLSession 下载任务 URL 使用详解

在使用 NSURLSession 进行网络数据下载时,downloadTaskWithURL 方法中的 URL 参数经常使人困惑。问题核心在于,为何需要传入完整查询 URL (queryURL),而不是基础 URL (baseURL)? 这涉及 URL 的构造和 NSURLSession 的工作机制。下面进行分析说明。

URL 的正确构造

理解问题的关键在于 URL 的构造过程。HTTP 请求通常需要完整的 URL 来指定资源位置。一个 URL 包含几个部分,包括:协议(例如 "http" 或者 "https"),主机名或IP地址,以及资源路径和查询参数。

在这个例子中, baseURL 指定了基础 URL 地址。为了发起特定资源的请求,就需要加上具体的查询参数(如 “&q=onion+soup”)。通过 NSURL(string: relativeToURL:) 的方法可以将查询参数附加到 baseURL ,构建出完整的 queryURL

解决方案一: 使用完整URL发起下载

NSURLSession 需要一个指向完整资源的 URL 进行下载。因此, downloadTaskWithURL 必须传入 queryURL 。这个 queryURL 是包含了查询参数的完整URL。这是其根本作用。

代码示例如下:

func searchRecipeData() {
    let baseURL = NSURL(string: "http://api.recipes.com/v1/api/recipes?_app_id=YOUR_API_ID&_app_key=YOUR_API_KEY")!
    let queryURL = NSURL(string: "&q=onion+soup", relativeToURL: baseURL)!
    
    let sharedSession = NSURLSession.sharedSession()
    
    let downloadTask = sharedSession.downloadTaskWithURL(queryURL) { (location: NSURL?, response: NSURLResponse?, error: NSError?) -> Void in
        
        if let error = error {
           print("Error downloading data: \(error)")
           return
        }

       guard let location = location else {
          print("No temporary location found after download")
          return
       }

       //移动临时下载文件到指定目录 (通常为本地文件系统)
       let fileManager = NSFileManager.defaultManager()
        let documentPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first!
        let finalFilePath = NSURL(fileURLWithPath: documentPath + "/recipe_download.json")

        do{
           try fileManager.moveItemAtURL(location, toURL: finalFilePath)
           print("File successfully downloaded and stored: \(finalFilePath)")

           let data = try NSData(contentsOfURL: finalFilePath)
           print(data)

       } catch {
           print("Unable to move/get data at file \(location)")
       }
        
    }

    downloadTask.resume()
}

操作步骤:

  1. 将代码粘贴到 Swift 项目的 ViewController 中
  2. 用你自己的 API_IDAPI_KEY 替换占位符,或者选择其他不需要 apiIDapiKeybaseURL
  3. 编译并运行,程序会将下载内容存储在Documents目录。
  4. 如果在终端下运行,会在控制台打印 data 对象的内容

此代码首先通过给定的baseURL生成queryURL, 然后启动网络下载任务,在回调函数中处理下载结果。location 参数是临时文件的本地路径,可以从中读取下载内容。 我们通常使用 fileManager.moveItemAtURL() 移动文件,方便进行下一步处理。注意需要错误处理。

** 安全建议**

  • API Keys等敏感信息不要直接写在代码中,而是要采用更为安全的方式例如存储在密钥链或环境变量中。
  • 务必在生产环境中对数据进行适当的验证。
  • 需要添加异常处理代码以避免程序在不处理错误的情况下发生崩溃。
  • 根据不同的业务需求选择不同的 NSURLSessionConfiguration。例如,可以使用NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier方法来创建一个支持在后台运行的网络会话,即便应用切换到后台或者被关闭,下载任务依然可以继续。

错误用法剖析

之前错误的代码尝试用 baseURL 来读取内容是不正确的,因为它并没有实际下载数据, 而是在内存里使用之前定义的 baseURL 这个字串创建一个 NSURL 对象去加载内存中缓存或者无效数据, 这个地址是没有下载的。 这样的操作在实际情况中将可能得到错误或者无法预期的数据。NSURLSessionDownloadTask 在后台下载结束后将返回一个临时的文件路径, 这个路径才是需要处理的。

代码示例 (错误):

 func searchRecipeData() {

     let baseURL = NSURL(string: "http://api.recipes.com/v1/api/recipes?_app_id=\(apiID)&_app_key=\(apiKey)")!
    let queryURL = NSURL(string: "&q=onion+soup", relativeToURL: baseURL)!

    let sharedSession = NSURLSession.sharedSession()

    let downloadData: NSURLSessionDownloadTask = sharedSession.downloadTaskWithURL(queryURL, completionHandler: { (location: NSURL?, response: NSURLResponse?, error: NSError?) -> Void in

        if (error == nil) {

            let data = NSData(contentsOfURL: baseURL) // 错误: 从 baseURL 中尝试加载数据。

             print(data)

        } else{

          print(error)
       }
     })

     downloadData.resume()
 }

代码解释:
let data = NSData(contentsOfURL: baseURL) 这一行,应该改为读取下载到 location 指向的文件。因为,当一个下载任务完成后,其结果不会被存入 baseURL对应的url中。它仅仅返回一个本地下载文件路径(location),只有处理location指向的文件内容才是正确的做法。 使用原始 baseURL 获取数据将获取不到实际的下载数据内容。

总而言之,NSURLSessionDownloadTask 的核心在于下载指定 URL 指向的资源到本地文件。 使用正确的URL 以及在下载完成回调中使用正确的处理逻辑才能成功获取到所需要的数据内容。 理解 URL 的构成和 NSURLSession 的工作原理是使用 downloadTask 的关键。