如果说移动时代的前身是什么,我想一个可能的答案就是网络时代。网络的兴起,让所有设备相连成为了可能,也催生了电商、社交、搜索等多个领域的商业巨头。而移动时代,则是网络时代的必然延伸,它代表着更便捷、更广阔、更深入的连接。
在这个背景之下,我们所开发的 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 网络请求
4.请说明并比较以下类:URLSessionTask,URLSessionDataTask,URLSessionUploadTask,URLSessionDownloadTask
关键词:#URLSession
- URLSessionTask 是个抽象类。通过实现它可以实例化任意网络传输任务,诸如请求、上传、下载任务。它的暂停(cancel)、继续(resume)、终止(suspend)方法有默认实现
- URLSessionDataTask 负责 HTTP GET 请求。它是 URLSessionTask 的具体实现。一般用于从服务器端获取数据,并存放在内存中。
- URLSessionUploadTask 负责 HTTP Post/Put 请求。它继承了 URLSessionDataTask。一般用于上传数据。
- URLSessionDownloadTask 负责下载数据。它是 URLSessionTask 的具体实现。它一般将下载的数据保存在一个临时的文件中;在 cancel 后可将数据保存,并之后继续下载。
它们之间的关系如下图:
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 的基本用法。下面是一种最简单粗暴的写法:
代码语言:txt复制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 点,我们还可以进一步修正代码,增强其可读性,并完善其逻辑。修改后的代码如下:
代码语言:txt复制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 将远程通知推送给响应手机。
具体的流程图如下:
数据处理
9.iOS 开发中如何实现编码和解码?
关键词: #Encodable #Decodable
编码和解码在 Swift 4 中引入了 Encodable 和 Decodable 这两个协议,而 Codable 是 Encodable 和 Decodable 的合集。在 Swift 中,Enum,Struct,Class 都支持 Codable。一个最简单的使用如下:
代码语言:txt复制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 数据进行编码和解码,示范代码如下:
代码语言:txt复制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 来挑选合适的持久化方案。