惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tailwind CSS Blog
大猫的无限游戏
大猫的无限游戏
L
LINUX DO - 热门话题
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
博客园_首页
MongoDB | Blog
MongoDB | Blog
V
V2EX
GbyAI
GbyAI
量子位
Microsoft Azure Blog
Microsoft Azure Blog
有赞技术团队
有赞技术团队
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
B
Blog
Microsoft Security Blog
Microsoft Security Blog
S
SegmentFault 最新的问题
O
OpenAI News
N
News and Events Feed by Topic
博客园 - Franky
爱范儿
爱范儿
Forbes - Security
Forbes - Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V2EX - 技术
V2EX - 技术
Application and Cybersecurity Blog
Application and Cybersecurity Blog
N
News and Events Feed by Topic
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
Cloudbric
Cloudbric
Security Archives - TechRepublic
Security Archives - TechRepublic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Recent Commits to openclaw:main
Recent Commits to openclaw:main
人人都是产品经理
人人都是产品经理
P
Privacy International News Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog RSS Feed
阮一峰的网络日志
阮一峰的网络日志
D
DataBreaches.Net
Last Week in AI
Last Week in AI
罗磊的独立博客
Spread Privacy
Spread Privacy
Recent Announcements
Recent Announcements
The Cloudflare Blog
Google DeepMind News
Google DeepMind News
AWS News Blog
AWS News Blog
The Register - Security
The Register - Security
Y
Y Combinator Blog
J
Java Code Geeks
I
Intezer

木土金王可

复盘 2024,年度好物分享 落地 第二十台相机:宝丽来(Polaroid)Impulse AF 第十九台相机:哈苏(Hasselblad)X1D II 第十八台相机:宝丽来(Polaroid)Macro 5 SLR 第十七台相机:康泰时(Contax)S2 稻荷山 Dave Chappelle: The Closer 语言是思想的边界 对街头卖惨表演的看法 《民主森林》 电梯英雄 2023,年度好物分享 《大画幅 4x5》复活 新加坡华侨银行(OCBC)开户指南 第十六台相机:宾得(Pentax)PC35AF-M SEDATE 第十五台相机:沙慕尼(Chamonix)4x5 F2 Xbox 连接 Studio Display Twitter、Apple、Meta 和 Reddit 广告平台使用体验 第十四台相机:康泰时(Contax)S2 你到底什么时候买相机 拍摄胶片的理由 HomePod 不能作为 Mac 音响 男性生来就是残缺的女性 第十三台相机:柯尼卡(Konica)Big Mini 301 三年,十二台相机 恐怖片很好看啊 比残忍更残忍 用 SwiftUI 重构「行动日」 推荐《女生呵护指南》 你是左派还是右派? 驯服内心的猛犸象:为什么你应该停止关心别人对你的想法 想写点什么,但又不知道该写什么 摄影构图 精致的结婚照 中文没死,是有的人死了 上海爆发 COVID-19 疫情以来我收藏的文章(持续更新) 自我审查 25 美元,成为美国计算机学会(ACM)会员 将同事送我的仙人掌转换成 3D 模型 当我们谈论离屏渲染时,我们在谈论什么 如何让 iOS 给你的应用开后门 二零一九年夏秋之交工作生活的异动 一声一世 iOS 限制输入字数与特殊字符的巧妙方法 对于番茄学习法的误解 Swift 中的空字符串 我如何自学 iOS 开发 为什么我不喜欢社交网络 如何与人见面 为什么我不喜欢即时通讯 以配置信息(UserDefaults)的形式存储应用数据 在文件沙盒中读取与写入应用数据 在 iOS 开发中使用键值对监听(KVO)与通知(Notification) 介绍 GCD 中的线程,队列,延迟执行,线程阻断,信号量,任务组与任务对象 什么是自动引用计数(ARC)和引用循环 在 iOS 开发中使用代理与协议 独立开发的第一个应用:行动日 为什么我会把 Google Pixel 换成 iPhone SE? 手动进行 Nexus 和 Pixel 系列手机的 OTA 更新 开启 Chromebook 的夜间模式
iOS 原生 URLSession 网络请求
kinnouka bokudo · 2018-09-02 · via 木土金王可

应用无论是从服务器获取信息,还是更新社交媒体信息,抑或是下载文件,这都归功于应用核心的 HTTP 网络请求。为了帮助开发者满足网络请求的众多需求,苹果提供了 URLSession,这是一个完整的网络 API,用于发送和接收 HTTP/HTTPS 请求。

你可以通过 URLSessionConfiguration 来创建 URLSession,它有三种形式:

  • .default:创建全局持久化本地缓存,证书和 cookie 存储对象。
  • .ephemeral:与默认配置类似,只是所有与任务相关的数据都存储在内存中,可将此视为「私密」任务。
  • .background:允许在后台执行上传或下载任务。即使应用本身被系统暂停或终止,任务仍会继续。

URLSessionConfiguration 还允许你配置其他属性,例如超时值,缓存策略和 HTTP 头部信息等。

URLSessionTask 是一个表示网络请求任务的抽象类。网络请求会创建一个或多个任务来获取数据,和上传或下载。这里有四种类型的网络请求任务:

  • URLSessionDataTask:此任务用于 HTTP GET 请求,以将数据从服务器检索到内存。
  • URLSessionUploadTask:此任务通常用于 HTTP POST 或 PUT 方法将文件从本地上传到服务器。
  • URLSessionDownloadTask:此任务用于将文件从服务器下载到临时文件位置。
  • URLSessionStreamTask:此任务用于建立 TCP/IP 长连接。

您也可以暂停,恢复和取消任务。URLSessionDownloadTask 可以保存暂停时的状态,以至于恢复任务时,不用从头开始。

声明:笔者自身对 URLSession 也是初学,而本文介绍的 URLSession 知识也都是自己的一些理解,同时尽量不涉及过于原理性的内容,一切以普通工程师实用为目标原则。其中可以想象在很多地方会有理解的错误,还请多包涵。如您发现问题,也往不吝赐教指正,感激不尽。

组装 URL

在进行任何一项网络请求时,都需要配置 URL,苹果提供 URLComponents 以供开发者配置 URL。比起使用纯字符串形式的 URL,用这种方式可以根据 URLComponents 值的内容轻松获取 URL 值,反之亦然。

func makeURL() -> URL {
    var components = URLComponents()
    components.scheme = "https"
    components.host = "xxx.xxx.xxx"
    components.path = "/path"

    components.queryItems = [URLQueryItem]()
    components.queryItems?.append(URLQueryItem(name: "parameterName1", value: "parameterValue1"))
    components.queryItems?.append(URLQueryItem(name: "parameterName2", value: "parameterValue2"))

    return components.url!
}

发起 GET 请求

发起 GET 请求时,必须先创建 URLSessionTask 对象和获取 URL 地址。为防止重复发起请求,可在 dataTask 的调用 .resume() 方法开始每次任务前,调用 .cancel() 方法。

func getRequest() {
    var dataTask: URLSessionTask?
    dataTask?.cancel()

    let requestURL = makeURL()
    let request: URLRequest = URLRequest(URL: requestURL)

    dataTask = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
        ......
    })

    dataTask?.resume()
}

另一种发起 GET 请求的方式,同样是调用 dataTask(with:completionHandler:) 方法,只不过第一个参数可以直接传入 URL 地址。

发起 POST 请求

与发起 GET 请求的不同在于,需要指定 URLRequest 的请求类型,并且设置 POST 的内容。

func postRequest() {
    ......

    request.httpMethod = "POST"
    let postData = ["parameterName1": "parameterValue1", "parameterName2": "parameterValue2"]
    let postString = postData.compactMap({ (key, value) -> String in
        return "\(key)=\(value)"
    }).joined(separator: "&")
    request.httpBody = postString.data(using: .utf8))

    dataTask = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
        ......
    })

    ......
}

开始下载

发起简单的下载任务

如果不需要获取下载进度,可以就像之前发起 POST 请求时那样调用下载任务的方法:

func postRequest() {
    ......

    downloadTask = URLSession.shared.downloadTask(with: request) { (url, response, error) in
        print("Finished downloading to \(location).")
        ......
    }

    ......
}

开始下载任务

如果需要监听和及时更新下载进度,那就需要用到 URLSessionDownloadDelegate。而引入 URLSessionDownloadDelegate 必须要实现的方法是 urlSession(_:downloadTask:didFinishDownloadingTo:),该方法会在下载任务执行完成后被调用。

extension DownloadViewController: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        print("Finished downloading to \(location).")
        ......
    }
}

但首先,需要初始化一个特别的 URLSession 来负责调用代理:

lazy var downloadsSession: URLSession = {
    let configuration = URLSessionConfiguration.default
    return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()

这里使用默认配置初始化网络任务,并指定代理,这样便可以接收 URLSession 的代理事件,用于监听任务进度。将代理队列设置为 nil 会导致创建一个串行队列,以执行委派方法和处理程序的所有调用。

然后再使用 downloadsSession 发起一个下载任务:

var downloadTask: URLSessionDownloadTask = downloadsSession.downloadTask(with: downloadUrl)
downloadTask.resume()

当下载任务执行完后,之前提及过的 urlSession(_:downloadTask:didFinishDownloadingTo:) 方法会被调用,在这个方法中,所下载的文件将被存储在一个临时区域,如果需要长久使用这个文件的话,需要将它放入应用沙盒中:

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let destinationURL = documentsPath.appendingPathComponent(downloadTask.originalRequest?.url.lastPathComponent)

    let fileManager = FileManager.default
    try? fileManager.removeItem(at: destinationURL)
    do {
        try fileManager.copyItem(at: location, to: destinationURL)
    } catch let error {
        print("Could not copy file to disk: \(error.localizedDescription)")
    }

    ......
}

在这段代码中,首先按照所下载文件在服务器中的路径,在本地创建相同的路径,并清空。然后再将文件从临时的储存位置移动到先前创建好的地址。

暂停,继续和取消下载任务

取消下载任务的方式十分简单,直接在 downloadTask 后调用 .cancle() 方法即可。

暂停下载任务和取消下载任务很相似,只是需要在取消下载任务时,保存好已下载的数据:

downloadTask?.cancel(byProducingResumeData: { data in
    let downloadedData = data
    ......
})

继续下载任务时,如果存在已下载的数据则继续下载,如果没有,则重新开始下载:

if let resumeData = downloadedData {
    downloadTask = downloadsSession.downloadTask(withResumeData: resumeData)
} else {
    downloadTask = downloadsSession.downloadTask(with: downloadUrl)
}

downloadTask.resume()

别忘记,无论哪种情况发起何种请求,都需要调用 resume() 方法。

获取下载任进度

显示下载进度可以有效的提升用户体验:

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
    let totalSize = ByteCountFormatter.string(fromByteCount: totalBytesExpectedToWrite, countStyle: .file)
    let downloadProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
    ......
}

在后台执行下载任务

启用后台下载之后,即便是应用在后台或崩溃,除非用户手动终止应用,否则下载仍会继续,这非常适用于大型文件的下载。这是由于当程序没有执行的时候,系统在应用外运行一个单独的守护程序来管理后台传输任务,并在下载任务时将适当的代理消息发送到应用程序。如果应用在这期间被终止,下载任务也将在后台继续执行。任务完成后,守护程序将在后台重新启动应用。重新启动的应用程序将重新创建后台会话,以接收相关的完成代理消息,并执行任何所需的操作,例如将下载的文件保存到磁盘。

想要实现此功能,需要在初始化下载任务时,就指定下载任务的模式:

lazy var downloadsSession: URLSession = {
    let configuration = URLSessionConfiguration.background(withIdentifier: "backgroundSessionConfiguration")
    return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()

如之前所说,当后台下载任务完成后,应用将会重启,这时候需要一个代理方法来执行这个事件,可在 AppDelegate.swift 文件执行 application(_:handleEventsForBackgroundURLSession:) 这个方法:

class AppDelegate: UIResponder, UIApplicationDelegate {
    var backgroundSessionCompletionHandler: (() -> Void)?

    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        backgroundSessionCompletionHandler = completionHandler
    }
}

最后再在 URLSessionDelegate 当任务完成的代理方法中执行具体的操作:

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    DispatchQueue.main.async {
        if let appDelegate = UIApplication.shared.delegate as? AppDelegate, let completionHandler = appDelegate.backgroundSessionCompletionHandler {
            appDelegate.backgroundSessionCompletionHandler = nil
            completionHandler()
        }
    }
}

开始上传

发起上传任务和发起 POST 请求很类似,只是将需要上传的文件或数据放在上传任务初始化时。

func postRequest() {
    ......

    request.httpMethod = "POST"

    let documents =  NSHomeDirectory() + "/Documents/fileName.suffix"
    let data = try! Data(contentsOf: URL(fileURLWithPath: documents))

    uploadTask = URLSession.shared.uploadTask(with: request, from: data) { (data, response, error) in
        ......
    }

    // 或者

    uploadTask = URLSession.shared.uploadTask(with: request, fromFile: URL(string: documents)) { (data, response, error) in
        ......
    }

    ......
}

2018 年 9 月 2 日