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

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

ZDDHUB

PixelsMeasure 开发第二年总结 PixelsMeasure 开发一年总结 Swift/SwiftUI 踩坑记 为什么说 GPT 利好程序员 ChatGPT 编程实现 Web 数字水印 Web 数字水印探究 Micro Frontends for Mobile Protocol Buffers GraphQL 从 0 到 1 开发一款 IOS 应用 - Swift MV* 软件设计架构 学习一个新技巧需要多久? 不停机数据库迁移 Rspec 如何 mock update 方法更新自己? Rails 使用 mysql2 出现的段错误 使用 Docker-compose 部署 Rails 应用到生产环境 Cocoa troubleshooting 独孤九剑 Dit (0x05) - 终端篇 Gem-based Jekyll theme 开发小记 Miscellaneous 前端手记 TodoMVC 之 Redux 篇 前端手记 TodoMVC 之 Server 篇 前端手记 TodoMVC 之 React 篇 前端手记 TodoMVC 之 CSS 篇 独孤九剑 Dit (0x04) - 测试篇 独孤九剑 Dit (0x03) - 缓存篇 英语小抄 LLDB debug Golang Make mistakes 大牛俱乐部上线啦 独孤九剑 Dit (0x02) - 数据结构篇 独孤九剑 Dit (0x01) - 总决 独孤九剑 Dit (0x00) - 我为什么要做 Dit 零值强制类型转换的使用 终端颜色输出重定向 Go语法简略 - 正则表达式 Makefile Go语法简略 - Duck框架探索 Go语法简略 - 依赖注入 Go语法简略 - web应用框架 Go语法简略 - 反射 Go语法简略 - 面向对象 Go语法简略 - 方法和接口 Go语法简略 - goroutine Go语法简略 - 基础篇 大牛 | 轻松科研 未来这几年 为 Android Studio 创建图标 Shell Git Vim 金庸答百问 论拖延症 Flex, A fast scanner generator 有理想的人 从虚拟到现实 常用视频转接口 Recognizer configuration on CentOS 整个世界清静了 《Python源码剖析》读书笔记
URL 加载系统(URL Loading System)
2022-05-04 · via ZDDHUB

URL Loading System,通过 URLs,使用标准的网络协议,与服务器交换数据。本文帮助理解 URL 加载系统,并通过示例代码练习如何使用它,示例代码已开源。

简介

iOS 应用开发少不了要和服务器打交道,而 URL 加载系统就是用来和 http- 和 URL-based 资源通信的。虽然叫做加载系统,但是也包括更新数据到服务器。这种加载通常是异步的,异步的机制能使得你的应用在数据或者错误回来之前,仍然能响应,不出现卡顿。

和 URL 资源的通信依赖 URLSession 来实现。通过 URLSession 创建一个或多个 URLSessionTask 来实现数据的访问、上传、下载,和对流媒体以及 Web socket 的处理。

在创建 URLSession 时,可以设置 URLSessionConfiguration 来配置 URLSession 的行为和策略。通过 URLSessionDelegate 来处理 Session 级别的事件响应,例如监听 Session 生命周期的改变并做相应的处理。

注:上图是 svg 图片,可放大后观看

URLSession

URLSession 是整个 URL 加载系统的核心,创建 Session 后,通过方法创建 URLSessionTask,然后执行 Session Task 来实现和后端服务的通信。一个应用程序可以有多个 Session,每个 Session 可以创建多个 Session Task (Session Task 只能使用 Session 创建)。应用可以根据需要,尽可能少的创建 Session,来减少对资源的消耗。一般来说,Session 可分为以下三类:

  • 常规的 Session。使用 URLSession 的构造函数创建,使用 configuration 配置 Session 的权限和策略,使用 delegate 处理 Session 级别的事件响应。
  • 临时的 Session。使用默认的配置,不对配置做更改。或者直接使用 URLSession shared 实例,能满足大部分的需要。
  • 后台 Session。需要配置,当应用挂起时,能在后台下载或者上传。

URLSessionTask

URLSession 提供了多套 API 来创建不同的 URLSessionTask, 来和服务器交换数据。URLSession 创建的 URLSessionTask 属于挂起状态,需要调用 resume() 方法来执行。从目前的 API (iOS 15) 来看, 有 5 类 Session Task:

  • URLSessionDataTask,最常用的一种,dataTask 返回 Data 类型的数据,存在内存中。不能在后台运行。
  • URLSessionUploadTask,能方便的设置 request body,上传到服务器。能在后台运行。
  • URLSessionDownloadTask,下载内容到临时文件,支持后台运行。
  • URLSessionStreamTask,支持流媒体
  • URLSessionWebSocketTask,支持 WebSocket

URLSession 为每种 Session Task 提供了多种创建/使用方式(为什么要这样?Python 告诉我们解决问题最直接的方法应该有一种,最好只有一种),如下表所示:

Task type Combine API Async API Completion Handler API Normal API
URLSessionDataTask
URLSessionUploadTask -
URLSessionDownloadTask -
URLSessionStreamTask - - -
URLSessionWebSocketTask - - -

其中,

  • Combine API: 使用 Combine 风格调用 API
  • Async API: 使用 async/await 关键字来调用 API
  • Completion Handler API: 使用回调的方式,在 completionHandler 中处理结果
  • Normal API:只创建对应的 Task,需要使用 Task Delegate 获取结果

每种 API 都提供了 URL 和 URLRequest 两个版本,当需要配置 Request 时选用 URLRequest 版本。

使用时可根据需要选择合适的 API,个人倾向于 Combine API > Async API > Completion Handler API > Normal API。

示例程序

由于 URLSessionDataTask 支持所有的四类使用方式,本文以 URLSessionDataTask 为例,来展示 URL 加载系统的使用。

准备工作

用来示例的程序是一个展示个人信息的卡片,个人信息和头像地址使用 JSON 格式,存储在服务器上。如下图所示:

profile view

View 的实现如下所示:

var body: some View {
    VStack {
      profileCard
    }
    .background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all))
    .onAppear {
      viewModel.loadData(loadingMethodType)
    }
    .onChange(of: loadingMethodType) { newValue in
      viewModel.loadData(newValue)
    }
  }

  private var profileCard: some View {
    HStack {
      Image(uiImage: viewModel.avatar)
        .resizable()
        .frame(width: 120, height: 120)
        .cornerRadius(80)
        .background(.white)
        .clipShape(Circle())
        .padding(8)

      VStack(alignment: .leading) {
        Text(viewModel.name)
          .font(.title)

        HStack {
          Image(systemName: "mail")
          Text("\(viewModel.email)")
        }

        HStack {
          Image(systemName: "link")
          Link("\(viewModel.blog)", destination: viewModel.blogUrl!)
        }
      }
    }
    .frame(maxWidth: .infinity, alignment: .leading)
    .background(backgroundColor)
    .cornerRadius(8)
    .shadow(
      color: shadowColor,
      radius: 4,
      x: 0.0,
      y: 1.0
    )
    .padding()
  }

所用的数据来自 JSON 格式的 API. 查看 JSON payload,如下所示:

$ `curl https://zddhub.com/assets/profile.json`
{
  "name": "zddhub",
  "avatar": "https://zddhub.com/assets/zddhub_big.png",
  "email": "zddhub@gmail.com",
  "blog": "www.zddhub.com",
  "bio": "Just for fun!"
}

对应的 Model 为:

struct Profile: Decodable {
  let name: String
  let avatar: String
  let email: String
  let blog: String
}

ProfileViewModel 持有一个 Profile 的 model,加载完 API 后,将服务器端的值赋值给 model,View 将自动完成刷新。

class ProfileViewModel: ObservableObject {
  private var url: URL
  @Published private var model: Profile?
}

这里使用一个简单的 MVVM 的架构。示例代码已开源,去 zddhub/url-loading-system 查看完整示例代码。

让我们来看看四种 API 的使用方式:

Combine API

Combine API 目前只支持 URLSessionDataTask,使用 SwiftUI 的话可以优先选用。

  URLSession.shared
      .dataTaskPublisher(for: url)
      .tryMap { (data: Data, response: URLResponse) in
        return data
      }
      .decode(type: Profile.self, decoder: JSONDecoder())
      .receive(on: DispatchQueue.main)
      .sink {_ in } receiveValue: { model in
        self.model.send(model)
      }
      .store(in: &cancellable)

Async API

async/await 要求的 iOS 版本比较高,使用前需确认 iOS 版本不小于 15。

When it was originally announced, Swift concurrency required at least iOS 15, macOS 12, watchOS 8, tvOS 15, or on other platforms at least Swift 5.5.

  let (data, response) = try await URLSession.shared.data(from: url)

  guard let httpResponse = response as? HTTPURLResponse,
        (200...299).contains(httpResponse.statusCode) else {
    print ("Service error \(String(describing: response))")
    return
  }

  if let mimeType = httpResponse.mimeType, mimeType == "application/json",
      let model = try? JSONDecoder().decode(Profile.self, from: data) {
    self.model.send(model)
  }

Completion Handler API

这里要注意创建的 task 是挂起的状态,需要手动 resume 后才能执行。

  let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let error = error {
      print("Client error \(error)")
      return
    }

    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
      print ("Service error \(String(describing: response))")
      return
    }

    if let mimeType = httpResponse.mimeType, mimeType == "application/json",
        let data = data,
        let model = try? JSONDecoder().decode(Profile.self, from: data) {
        self.model.send(model)
    }
  }
  task.resume()

Normal API

这里展示了创建常规的 Session 的方法,虽然 configuration 使用了默认值,什么都没改,但是可以改。由于这里的 dataTask 只创建了 task,所以需要设置 delegate,才能拿到 loading 后的值。

  let configuration = URLSessionConfiguration.default
  let session = URLSession(configuration: configuration)
  let task = session.dataTask(with: url)
  task.delegate = coordinator

  task.resume()

  class Coordinator: NSObject, URLSessionDataDelegate {
    var loadingMethod: LoadingMethod
    private var data: Data? = nil

    init(_ loadingMethod: LoadingMethod) {
      self.loadingMethod = loadingMethod
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
      guard error == nil, let data = data, let model = try? JSONDecoder().decode(Profile.self, from: data) else {
        self.data = nil
        return
      }

      self.loadingMethod.model.send(model)
      self.data = nil
    }

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
      if self.data == nil {
        self.data = data
      } else {
        self.data?.append(data)
      }
    }
  }

还需要注意的是 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) 这个方法会被调用多次,如果 data 过大,会分批传送,需要自己拼接数据后再处理。

Loading Avatar

上例中都使用 url 作为参数来创建 task,这里展示一种使用 URLRequest 创建 task 的例子,来抓去头像。

正如你在下述代码中看到的那样,可以配置缓存策略和超时时间,下述代码忽略了缓存,每次从服务器请求 API。当然,你也可以对 request 进行更改。

let request = URLRequest(url: avatarUrl, cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 5.0)

URLSession.shared
  .dataTaskPublisher(for: request)
  .tryMap { (data: Data, response: URLResponse) in
    return data
  }
  .receive(on: DispatchQueue.main)
  .sink { _ in
  } receiveValue: { data in
    self.avatarData = data
  }
  .store(in: &cancellable)

总结

URL 加载系统是 App 使用最频繁的模块,值得反复练习。主要使用 URLSession 创建 Session Task 执行不同的操作。所有 Session Task 中,URLSessionDataTask 最常用,所以本文选取 URLSessionDataTask 为例,需要熟练掌握。

本文示例完整源码地址 zddhub/url-loading-system

练习

网上得来终觉浅,绝知此事要躬行。读完本文后,可以用类似的方法,练习上传 URLSessionUploadTask 和下载 URLSessionUploadTask 的创建和使用。练习对 URLSessionConfigurationURLRequest 的配置。

如果你喜欢这篇文章,欢迎赞赏作者以示鼓励