我们所开发的 App 或多或少会与网络相连。或是拉取服务器端数据来更新 UI,或是通过网络推送自己的消息,或是在手机端删除自己曾经的照片,或是打开音乐播放应用下载自己喜欢的歌曲。如何请求、接收、处理、发送数据,就是我们这节要讨论的内容。


计算机理论


1.谈谈 HTTP 中 GET 与 POST 的区别


关键词:#方向 #类型 #参数位置


  • 从方向上来看,GET 是从服务器端获取信息,POST 是向服务器端发送信息。
  • 从类型上来看,GET 处理静态和动态内容,POST 只处理动态内容。
  • 从参数位置来看,GET 的参数在其 URI 里,POST 的参数在它的包体里:从这个角度来看,POST 比 GET 更加安全隐秘。
  • GET 可以被缓存,可以被储存在浏览器历史中,其内容理论上有长度限制;POST 在这 3 点上恰恰相反。


2.谈谈 Session,Token,Cookie 的概念


关键词:#用户认证 #客户端 #服务器端


  • Session 是服务器端用来认证、追踪用户的数据结构。它通过判断客户端传来的信息确定用户,确定用户的唯一标识是客户端传来的 Session ID。
  • Token 是服务器端生成的一串字符串,是客户端进行请求的令牌、服务器端用以确定用户的唯一标识。Session ID 就经常被用作 Token 来使用。Token的出现避免了服务器频繁的查询用户名和密码,降低了数据库的查询压力。
  • Cookie 是客户端保存用户信息的机制。初次会话 HTTP 协议会在 Cookie 里记录一个 Session ID ,之后每次把 Session ID 发给服务器端。
  • Session 一般用于用户验证。它默认存在服务器的一个文件里,当然内存、数据库里也可以存储。
  • 若是客户端禁用了 Cookie,客户端会用 URL 重写技术,即会话时在 URL 的末尾加上 Session ID,并发送给服务器端。


3.在一个 HTTPS 连接的网站里,输入账号密码点击登录后,到服务器返回这个请求前,中间经历了什么


关键词:#锁 #客户端 #服务器端


1) 客户端打包请求。包括 url,端口啊,你的账号密码等等。账号密码登陆应该用的是 Post 方式,所以相关的用户信息会被加载到 body 里面。这个请求应该包含三个方面:网络地址,协议,资源路径。注意,这里是 HTTPS,就是 HTTP + SSL / TLS,在 HTTP 上又加了一层处理加密信息的模块(相当于是个锁)。这个过程相当于是客户端请求钥匙。


2) 服务器接受请求。一般客户端的请求会先发送到 DNS 服务器。 DNS 服务器负责将你的网络地址解析成 IP 地址,这个 IP 地址对应网上一台机器。这其中可能发生 Hosts Hijack 和 ISP failure 的问题。过了 DNS 这一关,信息就到了服务器端,此时客户端会和服务器的端口之间建立一个 socket 连接,socket 一般都是以 file descriptor 的方式解析请求。这个过程相当于是服务器端分析是否要向客户端发送钥匙模板。


3) 服务器端返回数字证书。服务器端会有一套数字证书(相当于是个钥匙模板),这个证书会先发送给客户端。这个过程相当于是服务器端向客户端发送钥匙模板。


4) 客户端生成加密信息。根据收到的数字证书(钥匙模板),客户端会生成钥匙,并把内容锁上,此时信息已经加密。这个过程相当于客户端生成钥匙并锁上请求。


5) 客户端发送加密信息。服务器端会收到由自己发送出去的数字证书加锁的信息。 这个时候生成的钥匙也一并被发送到服务器端。这个过程是相当于客户端发送请求。


6) 服务器端解锁加密信息。服务器端收到加密信息后,会根据得到的钥匙进行解密,并把要返回的数据进行对称加密。这个过程相当于服务器端解锁请求、生成、加锁回应信息。


7) 服务器端向客户端返回信息。客户端会收到相应的加密信息。这个过程相当于服务器端向客户端发送回应。


8) 客户端解锁返回信息。客户端会用刚刚生成的钥匙进行解密,将内容显示在浏览器上。

整个过程的流程图如下:


 iOS 面试经-网络、推送与数据处理_chatGPT


iOS 网络请求


4.请说明并比较以下类:URLSessionTask,URLSessionDataTask,URLSessionUploadTask,URLSessionDownloadTask


关键词:#URLSession


  • URLSessionTask 是个抽象类。通过实现它可以实例化任意网络传输任务,诸如请求、上传、下载任务。它的暂停(cancel)、继续(resume)、终止(suspend)方法有默认实现
  • URLSessionDataTask 负责 HTTP GET 请求。它是 URLSessionTask 的具体实现。一般用于从服务器端获取数据,并存放在内存中。
  • URLSessionUploadTask 负责 HTTP Post/Put 请求。它继承了 URLSessionDataTask。一般用于上传数据。
  • URLSessionDownloadTask 负责下载数据。它是 URLSessionTask 的具体实现。它一般将下载的数据保存在一个临时的文件中;在 cancel 后可将数据保存,并之后继续下载。


它们之间的关系如下图:


 iOS 面试经-网络、推送与数据处理_大数据_02


5. 什么是 Completion Handler?


关键词:#闭包


Completion Handler 一般用于处理 API 请求之后的返回数据。


当URLSessionTask 结束之后,无论成功或是报错,Completion Handler 一般都会接受 3 个参数:Data, URLResponse,Error,注意这 3 个参数都是 Optional。


在 Swift 中,Completion Handler 必须标明 @escaping。因为它总是在 API 请求之后才执行,也就是说方法已经返回才会涉及 Completion Handler,是个经典的逃逸闭包情况。


6. 代码实战:设计一个方法,给定 API 的网址,返回用户数据


关键词:#URLSessionDataTask


这道题目考察的是 URLSessionDataTask 的基本用法。下面是一种最简单粗暴的写法:


func queryUser(url: String, completion: @escaping (_ user: User?, _ error : Error?) -> Void)) {

  guard let url = URL(string: url) else {
    return
  }

  URLSession.shared.dataTask(with: url) { data, response, error in
    if let error = error {
      DispatchQueue.main.async {
        completion(nil, error)
      }
      return
    } else if let data = data, let response = response as? HTTPURLResponse {
       DispatchQueue.main.async {
         completion(convertDataToUser(data), nil)
       }
    } else {
       DispatchQueue.main.async {
         completion(nil, NSError(“invalid response”))
       }
    }
  }.resume()
}


上面的写法有很多问题,其中最主要的是:


  • url 出错处理不当。应该返回错误信息以方便日后调试,而不是应该 return
  • 用 URLSession 的单例不妥。这样每次请求创建一个 dataTask 是一种浪费,同时短时间内多次请求会不必要的造成服务器压力。正确的处理方法应该是每次请求都取消上一次请求(无论有无完成)。
  • 代码重复冗余。代码中多次用到了切换至主线程并调用闭包的过程。实际上我们可以将整个方法扩展为一个类,然后将返回值与成员变量结合起来使用。


除了以上 3 点,我们还可以进一步修正代码,增强其可读性,并完善其逻辑。修改后的代码如下:


enum QueryError: String {
  case InvaldURL = “Invalid URL”,
  case InvalidResponse = “Invalid response”
}

class QueryService {
  typealias QueryResult = (User?, String?) -> Void

  var user: User?
  var errorMessage: String?

  let defaultSession = URLSession(configuration: .default)
  var dataTask: URLSessionDataTask?

  func queryUsers(url: String, completion: @escaping QueryResult) {
    dataTask?.cancel()

    guard let url = URL(string: url) else {
      DispatchQueue.main.async {
        completion(user, QueryError.InvalidURL)
      }
      return   
    }

    dataTask = defaultSession.dataTask(with: url) { [weak self] data, response, error in
      defer {
        self?.dataTask = nil
      }
      if let error = error {
        self?.errorMessage = error.localizedDescription
      } else if let data = data,
          let response = response as? HTTPURLResponse,
          response.statusCode == 200 {
            self?.user = convertDataToUser(data)
      } else {
        self?.errorMessage = QueryError.InvalidResponse
      }

      DispatchQueue.main.async {
        completion(self?.user,self?.errorMessage)
      }
    }.resume()  
  }
}


上面的修改方法主要针对一些硬伤。如果配合 Swift 的面向协议的编程来实现该 API,整个代码会更加灵活。


信息推送


7. iOS 开发中本地消息通知的流程是怎样的?


关键词:#UserNotifications


UserNotifications 框架是苹果针对远程和本地消息通知的框架。其流程主要分 4 步:


1) 注册。通过调用 requestAuthorization 这个方法,通知中心会向用户发送通知许可请求。在弹出的 Alert 中点击同意,即可完成注册。

2) 创建。首先设置信息内容 UNMutableNotificationContent 和触发机制 UNNotificationTrigger ;然后用这两个值来创建 UNNotificationRequest;最后将 request 加入到当前通知中心 UNUserNotificationCenter.current() 中。

3) 推送。这一步就是系统或者远程服务器推送通知。伴随着一声清脆的响声(或自定义的声音),通知对应的 UI 显示到手机界面的过程。

4) 响应。当用户看到通知后,点击进去会有相应的响应选项。设置响应选项是 UNNotificationAction 和 UNNotificationCategory。




加分回答:

远程推送的流程与本地推送大同小异,不同的是第 2 步创建,参数内容和消息创建都在服务器端完成,而不是在本地完成。


8.iOS 开发中远程消息推送的原理是怎样的?


关键词: #APNs Server


回答这道题目的关键在于理清 iOS 系统,App,APNs 服务器,以及 App 对应的客户端之间的关系。具体来说就是:


  1. App 向 iOS 系统申请远程消息推送权限。这与本地消息推送的注册是一样的;
  2. iOS 系统向 APNs(Apple Push Notification Service) 服务器请求手机的 device token,并告诉 App,允许接受推送的通知;
  3. App 将手机的 device token 传给 App 对应的服务器端;
  4. 远程消息由 App 对应的服务器端产生,它会先经过 APNs;
  5. APNs 将远程通知推送给响应手机。


具体的流程图如下:


 iOS 面试经-网络、推送与数据处理_chatGPT_03


数据处理


9.iOS 开发中如何实现编码和解码?


关键词: #Encodable #Decodable


编码和解码在 Swift 4 中引入了 Encodable 和 Decodable 这两个协议,而 Codable 是 Encodable 和 Decodable 的合集。在 Swift 中,Enum,Struct,Class 都支持 Codable。一个最简单的使用如下:


enum Gender: String, Codable {
  case Male = “Male”
  case Female = “Female”
}

class User: Codable {
  let name: String
  let age: Int
  let gender: Gender

  init(name: String, age: Int, gender: Gender) {
    (self.name, self.age, self.gender) = (name, age, gender)
  }
}


这样定义完成之后,我们就可以轻易的在 User 及其对应 JSON 数据进行编码和解码,示范代码如下:


let userJsonString = """
{
        "name": "Cook",
        "age": 58,
        "gender": "Male"
}
"""

// 从JSON解码到实例
if let userJSONData = userJsonString.data(using: .utf8) {
  let userDecode = try? JSONDecoder().decode(User.self, from: userJSONData)
}

//从实例编码到JSON
let userEncode = User(name: "Cook", age: 58, gender: Gender.Male)
let userEncodedData = try? JSONEncoder().encode(userEncode)


追问:假如 JSON 的键值和对象的属性名不匹配该怎么办?


可以在对象中定义一个枚举(enum CodingKeys: String, CodingKey),然后将属性和 JSON 中的键值进行关联。


追问:假如 class 中某些属性不支持 Codable 该怎么办?


将支持 Codable 的属性抽离出来定义在父类中,然后在子类中配合枚举(enum CodingKeys),将不支持的 Codable 的属性单独处理。


10.谈谈 iOS 开发中数据持久化的方案


关键词: #plist #Preference #NSKeyedArchiver #CoreData


数据持久化就是将数据保存在硬盘中,这样无论是断网还是重启,我们都可以访问到之前保存的数据。iOS 开发中有以下几种方案:


  • plist。它是一个 XML 文件,会将某些固定类型的数据存放于其中,读写分别通过 contentsOfFile 和 writeToFile 来完成。一般用于保存 App 的基本参数。
  • Preference。它通过 UserDefaults 来完成 key-value 配对保存。如果需要立刻保存,需要调用 synchronize 方法。它会将相关数据保存在同一个 plist 文件下,同样是用于保存 App 的基本参数信息。
  • NSKeyedArchiver。遵循 NSCoding 协议的对象就就可以实现序列化。NSCoding 有两个必须要实现的方法,即父类的归档 initWithCoder 和解档 encodeWithCoder 方法。存储数据通过 NSKeyedArchiver 的工厂方法 archiveRootObject:toFile: 来实现;读取数据通过 NSKeyedUnarchiver 的工厂方法 unarchiveObjectwithFile:来实现。相比于前两者, NSKeyedArchiver 可以任意指定存储的位置和文件名。
  • CoreData。前面几种方法,都是覆盖存储。修改数据要读取整个文件,修改后再覆盖写入,十分不适合大量数据存储。CoreData 就是苹果官方推出的大规模数据持久化的方案。它的基本逻辑类似于 SQL 数据库,每个表为 Entity,然后我们可以添加、读取、修改、删除对象实例。它可以像 SQL 一样提供模糊搜索、过滤搜索、表关联等各种复杂操作。尽管功能强大,它的缺点是学习曲线高,操作复杂。


以上几种方法是 iOS 开发中最为常见的数据持久化方案。除了这些以外,针对大规模数据持久化,我们还可以用 SQLite3、FMDB、Realm 等方法。相比于 CoreData 和其他方案,Realm 以其简便的操作和丰富的功能广受很多开发者青睐。同时大公司诸如 Google 的 Firebase 也有离线数据库功能。其实没有最佳的方案,只有最合适的方案,应该根据实际开发的 App 来挑选合适的持久化方案。