0%

字符编码

计算机是 0 和 1 的世界,为了表示文本,我们就需要指定字符到数字的映射,这个映射就叫做编码。

ASCII

ASCII(American Standard Code for Information Interchange:美国信息交换标准代码)是由美国国家标准协会制定的单字节字符编码方案。一个字节(8位)可以表示 256 种状态,ASCII 只使用到了一个字节中的 7 位,它将英文字母,数字 0-9 以及一些标点符号和控制字符映射为 0-127 这些整型,所以其最高位一直为 0。

随后,人们基于 ASCII 创建了各种编码系统,并使用了其没有使用的第八位来编码其他字符以期处理英语外的其他语言。但是由于 8 位空间的局限性以及这些编码系统的不兼容性使得一个统一的,全世界每个字符都有一个对应编码的编码标准的出现成为众望所归的事情。它就是 Unicode。

Unicode

Unicode 标准几乎为世界上各种书写系统里的每一个字符或符号定义了一个唯一的数字。这个数字叫做码点(code points),以 U+xxxx 这样的格式写成,格式里的 xxxx 代表四到六个十六进制的数。

最初,Unicode 编码是被设计为 16 位的,提供了 65,536 个字符的空间。后来,Unicode 编码扩展到了 21 位(从 U+0000 到 U+10FFFF)。注意现在的 Unicode 不是 16 位的编码!它是 21 位的。 这 21 位提供了 1,114,112 个码点。编码空间被分为 17 个平面。每个平面有 65,536 个字符。0 号平面叫做基本多文种平面(Basic Multilingual Plane, BMP),涵盖了几乎所有你能遇到的字符,除了 emoji。其它平面叫做补充平面,大多是空的。

UTF

字符和码点之间的映射只完成了一半工作,还需要定义另一种编码来确定码点在内存和硬盘中要如何表示。Unicode 标准为此定义了几种映射,叫做 Unicode 转换格式(Unicode Transformation Formats,简称 UTF)。

UTF-32

UTF-32 是一种固定长度的 Unicode 转换格式。每个码点都使用 32 位存储空间,因为太占用空间了从而在实际中很少使用。

UTF-16

UTF-16 本身是一种长度可变的编码。它是根据有 16 位固定长度的码元(code units)定义的。基本多文种平面(BMP)中的每一个码点都直接与一个码元相映射。鉴于 BMP 几乎囊括了所有常见字符,UTF-16 一般只需要 UTF-32 一半的空间。其它平面里很少使用的码点都是用两个 16 位的码元来编码的,这两个合起来表示一个码点的码元就叫做代理对(surrogate pair)

和所有多字节长度的编码系统一样,UTF-16(以及 UTF-32)还得解决字节顺序的问题。在硬盘里存储或者通过网络传输字符串时,UTF-16 允许在字符串的开头插入一个字节顺序标记(Byte Order Mask,BOM)。字节顺序标记是一个值为 U+FEFF 的码元,通过检查文件的头两个字节,解码器就可以识别出其字节顺序。字节顺序标记不是必须的,Unicode 标准把大端顺序(big-endian byte order)定为默认情况。

UTF-8

UTF-8 使用一到四个字节来编码一个码点。从 0 到 127 的这些码点直接映射成 1 个字节(对于只包含这个范围字符的文本来说,这一点使得 UTF-8 和 ASCII 完全相同)。接下来的 1,920 个码点映射成 2 个字节,在 BMP 里所有剩下的码点需要 3 个字节。Unicode 的其他平面里的码点则需要 4 个字节。UTF-8 是基于 8 位的码元的,因此它并不需要关心字节顺序。有效空间利用及不需要操心字节顺序问题使得 UTF-8 成为存储和交流 Unicode 文本方面的最佳编码。它也已经是文件格式、网络协议以及 Web API 领域里事实上的标准了。

OC 中的 NSString

NSString 对象代表的其实是用 UTF-16 编码的码元组成的数组。 因为在 NSString 开发的时候(它最初是作为 Foundation Kit 的一部分在 1994 年发布的),Unicode 还是 16 位的。

默认情况下,Clang 会把源文件看作以 UTF-8 编码的。只要你确保 Xcode 以 UTF-8 编码保存文件,你就可以直接用字符显示程序插入任意字符。如果你更喜欢用码点,最大到 U+FFFF 这个范围内的码点你可以以 @”\u266A”(♪)的方式输入,BMP 外其它平面的码点则以 @”\U0001F340”(🍀)的方式输入。有意思的是,C99 不允许标准 C 字符集里的字符用通用字符名(universal character name)来指定,因此不能这样写:

1
2
//错误写法
//NSString *s = @"\u0041";

NSString 代表的是用 UTF-16 编码的文本,长度、索引和范围都基于 UTF-16 的码元。因此 length 方法返回的是字符串中码元的数量而不是字符个数。 unichar 类型和characterAtIndex:方法说的都是码元。

-[NSString length] 返回字符串里 unichar 的个数。对于基本多文种平面(BMP)里所有的字符在 UTF-16 里都可以用一个码元表示。但是随着 emoji (在 1 号平面)的流行,实际使用中就会发现代理对,如下:

1
2
NSString *s = @"\U0001F30D"; // 🌭  
NSLog(@"%lu",[s length]);// 2

由于这些组合字符序列的存在,会导致很多不便,幸而, NSString 提供了enumerateSubstringsInRange:options:usingBlock: 方法,当参数为NSStringEnumerationByComposedCharacterSequences时,可以对真正的 Unicode 字符进行遍历。

Swift 中的 String

Swift 中的String类型字符串是例如"hello, world""albatross"这样的有序的 Character 类型的值的集合。每一个字符串都是由编码无关的 Unicode 字符组成,并支持访问字符的多种 Unicode 转换格式(UTF)。

Swift 中每个Character类型的值代表一个可扩展的字形群。一个可扩展的字形群是一个或多个可生成人类可读的字符 Unicode 标量的有序排列。例如字母 é 可以用单一的 Unicode 标量 é(U+00E9)来表示。然而一个标准的字母 e(U+0065) 加上一个急促重音的标量(U+0301),这样一对标量就表示了同样的字母 é。这个急促重音的标量形象的将 e 转换成了 é。在这两种情况中,字母 é 代表了一个单一的 Swift 的Character值,同时代表了一个可扩展的字形群。在第一种情况,这个字形群包含一个单一标量;而在第二种情况,它是包含两个标量的字形群:

1
2
3
let eAcute: Character = "\u{E9}"                         // é
let combinedEAcute: Character = "\u{65}\u{301}" // e 后面加上 ́
// eAcute 是 é, combinedEAcute 也是同一个单一的 Character值 é

可扩展的字符群集可以由一个或者多个 Unicode 标量组成。这意味着不同的字符以及相同字符的不同表示方式可能需要不同数量的内存空间来存储。所以 Swift 中的字符在一个字符串中并不一定占用相同的内存空间数量。

如果想要获得一个字符串中 Character 值的数量,可以使用count属性。需要注意的是通过 count属性返回的字符数量并不总是与包含相同字符的NSStringlength属性相同。NSStringlength属性是利用 UTF-16 表示的十六位代码单元数字,而不是 Unicode 可扩展的字符群集。

原文:URL Loading System

前言

南峰子大佬之前有翻译过这个专题:URL加载系统之一:基本结构,但是随着官方文档的更新,这篇编程指南的原文也从苹果的文档中删除了,因此为复习一下网络框架,就顺手把这篇翻译了,以下开始正文。

URL 加载系统

使用标准的网络协议通过与 URLs 交互,来与服务器进行交流。

概述

URL 加载系统通过使用标准的协议如 https 或者创建的自定义协议提供被 URLs 标识的特定资源的访问。加载是异步执行的,因此你的 app 可以保持响应并且在即将到来的数据或错误信息抵达时做出处理。

使用一个 NSURLSession 实例来创建一个或多个 NSURLSessionTask 实例,它可以用于给你的 app 获取和返回数据,下载文件,或者上传数据和文件到远程地址。你需要使用一个可以控制诸如如何使用 caches 和 cookies 或者是否允许使用蜂窝网络连接等行为的 NSURLSessionConfiguration 对象来配置这个 session 实例。

你可以使用一个 session 重复的创建多个任务,举个例子:一个网页浏览器可能会有不同的 session 用于正常访问和隐私访问,在隐私访问的 session 中并不缓存数据。图 1 展示了使用这些配置的两个 session 如何创建多个任务。

图 1 使用 URL session 创建任务6789dd96-afdc-4c18-b8eb-01f9012dc04d

每个 session 都会与一个代理关联起来用于接收周期性的更新(或者 errors)。默认的代理调用你提供的处理任务完成 completion block;如果你创建了自定义的代理,就不会调用这个 block 了。

你可以配置一个运行在 background 的 session,这样当你的 app 挂起时,系统会代表你下载数据并且唤醒你的 app 来传递结果。

First Steps

从网站获取数据到内存

通过从 URL session 中创建一个 data task 直接接收数据到内存。

概述

对于与远程服务器的简单交互,你可以使用 NSURLSessionDataTask 类将数据接收到内存中(使用NSURLSessionDownloadTask 则不同,它会将数据直接保存到文件系统)。一个 data task 对于访问 web 服务端点来说是很完美的。

使用一个 URL session 来创建 task。如果你的需求很简单,就可以直接使用 NSURLSession 类的 sharedSession 单例对象。如果你想通过代理回调与传输过程交互的话,需要创建一个 session 而不是使用 sharedSession 单例。创建一个 session 时需要一个 NSURLSessionConfiguration 实例,同时还需传入一个实现了 NSURLSessionDelegate 或其子协议的类。Session 可以被重用于创建多个 task,因此可以对每一个独特的配置创建一个 session 并将其作为属性存储到 session 实例中。

注意
注意不要创建超出需求的 session,举例来说,如果你的 app 中几个部分需要差不多配置的 session,只需要创建一个并且共享它就可以了。

一旦有了 session,就可以使用dataTask()的相关方法中的一个来创建 data task 了,新创建的 task 是处于 suspended 状态的,需要通过调用 resume 方法来启动它。

使用 Completion Handler 接收结果

获取数据最简单的方法是使用创建一个使用 completion handler 的 data task。这样 task 会传递服务器端的 response,data 也可能是 error 给你提供的 completion handler。下图展示了 session 和 task 之间的关系,以及结果如何传递到 completion handler 中。

bf4501ff-82b2-4dd4-9ec3-243ef0e70d21

要创建一个使用 completion handler 的 data task,需要调用 URLSession 的dataTaskWithURL:方法,在 completion handler 中要做 3 件事情:

  1. 验证 error 参数是否为空,如果不是则说明传输发生错误;处理错误并退出。
  2. 检查 response 参数,验证代表成功的状态码及 MIME 类型是否是预期值,若不是,则处理服务器错误并退出。
  3. 按需求使用 data 实例。

下面的代码展示了获取 URL 内容的startLoad()方法。通过使用 NSURLSession 类的共享单例创建一个 data task 用于传递结果给 completion handler。在检查了本地和服务端错误后,在 handler 中将数据转化为一个字符串,并用于插入代码到 WKWebView 中。当然,你的 app 可能会将数据用于其他用途,例如解析到数据模型中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func startLoad() {
let url = URL(string: "https://www.example.com/")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
self.handleClientError(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
self.handleServerError(response)
return
}
if let mimeType = httpResponse.mimeType, mimeType == "text/html",
let data = data,
let string = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
self.webView.loadHTMLString(string, baseURL: url)
}
}
}
task.resume()
}

重要
completion handler 是在与创建 task 不同的 GCD queue 中被调用的。因此任何使用 data 或 error 信息来更新 UI ,像更新 webView 的操作,应该像上例中被显式的放到 main queue 中去。

使用代理接收传输详情及结果

为了更进一步随着 task 的运行获取它的状态,你可以在创建一个 data task 时给 session 设置一个代理,而不是提供一个 completion handler。下图展示了这种方案:

730c8e1b-654f-4eb9-9c63-d439a69ac5d2

通过这种方式,一份份的数据随着他们的抵达会提供到 NSURLSessionDataDelegate URLSession:dataTask:didReceiveData:方法,直到传输完成或者失败。随着数据的传输过程代理对象还接收其他的事件。

当你使用这种方式时,你需要创建一个自己的 URLSession 实例,而不是使用共享的单例。创建一个新的 session 允许你设置自己的类作为其代理。正如下面的代码中展示的。

声明你的类实现了一个或多个代理协议(NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate, 和 NSURLSessionDownloadDelegate)。然后使用初始化方法 sessionWithConfiguration:delegate:delegateQueue: 来创建 URL session 的实例。可以在初始化方法中设置配置实例。例如,设置 waitsForConnectivityYES是个好主意,这样 session 在所要求的连接不可用时会等待适当的连接,而不是直接失败。

1
2
3
4
5
6
private lazy var session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.waitsForConnectivity = true
return URLSession(configuration: configuration,
delegate: self, delegateQueue: nil)
}()

下面的代码展示了使用这个 session 启动一个 data task 的startLoad()方法,并使用代理回调来处理接收的 data 和 error。这段代码实现了 3 个代理回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var receivedData: Data?

func startLoad() {
loadButton.isEnabled = false
let url = URL(string: "https://www.example.com/")!
receivedData = Data()
let task = session.dataTask(with: url)
task.resume()
}

// delegate methods
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
guard let response = response as? HTTPURLResponse,
(200...299).contains(response.statusCode),
let mimeType = response.mimeType,
mimeType == "text/html" else {
completionHandler(.cancel)
return
}
completionHandler(.allow)
}

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

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
DispatchQueue.main.async {
self.loadButton.isEnabled = true
if let error = error {
handleClientError(error)
} else if let receivedData = self.receivedData,
let string = String(data: receivedData, encoding: .utf8) {
self.webView.loadHTMLString(string, baseURL: task.currentRequest?.url)
}
}
}

各种各样的代理协议提供了超出上面代码展示的方法。对于处理认证,重定向及其他的情况,在 URLSession 文档的Using a URL Session部分讨论了传输过程中的众多回调方法。

上传数据到网站

从 app 中 post 数据到服务器。

概述

大多数 app 都与能够接收上传诸如图片或文件,或者使用接收如 JSON 这样结构化数据的网络服务 API 端点的服务器协同工作。想要从 app 中上传数据,要使用一个 NSURLSession 实例创建一个 NSURLSessionUploadTask 实例,upload task 使用 URLRequest 实例来描述了上传是如何被执行的。

准备好用于上传的数据

用于上传的数据可以是文件的内容,一个 stream,或者像下面代码中的数据。

很多网站服务端点接收 JSON 格式的数据,可以通过在如数组和字典这样的 Encodable 类型上使用 JSONEncoder类来创建。如下代码所示, 你可以声明一个遵循 Codable 协议的结构体,创建该类型的实例,然后使用JSONEncoder来将实例编码成用于上传的 JSON 数据。

1
2
3
4
5
6
7
8
9
10
11
12
struct Order: Codable {
let customerId: String
let items: [String]
}

// ...

let order = Order(customerId: "12345",
items: ["Cheese pizza", "Diet soda"])
guard let uploadData = try? JSONEncoder().encode(order) else {
return
}

有很多种方式来创建一个数据实例,例如讲一个图片编码成 JPEG 或者 PNG 数据,或者将一个字符串使用 UTF-8 编码转换成数据。

配置一个上传请求

Upload task 需要一个 URLRequest 实例,如下代码所示,根据服务器的支持及预期设置请求的 httpMethod 属性为“POST”或者“PUT”,使用 setValue(_:forHTTPHeaderField:) 方法设置任何你想提供的除Content-Length之外的 HTTP header 值。因为 session 会从数据大小中自动计算出内容长度。

1
2
3
4
let url = URL(string: "https://example.com/post")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

创建并启动一个上传任务

要开始上传,需要调用 NSURLSession 类的 uploadTaskWithRequest:fromData:completionHandler: 方法创建一个用于上传的 NSURLSessionTask 实例,传入之前设置好的 request 实例与 data 实例。鉴于 task 是以 suspend 状态开始的,你需要通过对 task 调用resume方法来开启一个网络加载过程。下面的代码使用了共享的 URLSession 实例,通过一个 completion handler 接收结果。在 handler 中在使用任何返回数据之前检查传输和服务器端的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let task = URLSession.shared.uploadTask(with: request, from: uploadData) { data, response, error in
if let error = error {
print ("error: \(error)")
return
}
guard let response = response as? HTTPURLResponse,
(200...299).contains(response.statusCode) else {
print ("server error")
return
}
if let mimeType = response.mimeType,
mimeType == "application/json",
let data = data,
let dataString = String(data: data, encoding: .utf8) {
print ("got data: \(dataString)")
}
}
task.resume()

另一种选择,通过设置代理上传

作为 completion handler 方式的另一种选择,你可以给你配置的 session 设置一个代理,然后使用 uploadTaskWithRequest:fromData: 方法创建一个 upload task。在这种方案下,你要实现NSURLSessionDelegateNSURLSessionTaskDelegate协议中的方法。这些方法会接收服务器的 response 及任何数据或传输错误。

在后台下载文件

创建一个 task 在 app 处于非活跃状态时下载文件。
###概述
对应长时间的不紧急的传输任务,你可以创建运行在后台的 task,即使你的 app 已经被 suspended 了,这些任务依然可以运行,并且允许你的 app 恢复运行时访问这些已下载的文件

注意
并不是所有的后台网络活动都必须按照这篇文章中描述的那样使用 background session 来完成,那些声明了合适的 background mode 的 app 可以使用默认的 URLSession 和 data task,就跟在前台运行一样。

配置 Background Session

按照如下步骤创建一个 background URL session,下面的代码展示了这个过程:

  1. 提供一个在 app 中唯一的 session 标识符,使用 NSURLSession 的类方法 backgroundSessionConfigurationWithIdentifier: 来创建一个 background NSURLSessionConfiguration 对象。因为大部分 app 只需要少数 background session(通常只需要一个),你可以使用一个固定的字符串作为标识符,而不是动态生成的。这个标识符并不需要全局唯一。
  2. 确保 sessionSendsLaunchEvents 属性被设置为true(默认值),来保证当任务完成且你的 app 在后台时会被系统唤醒。
  3. 对于对时间不敏感的任务,可以设置 discretionary 属性为true,以便于系统在最理想的情况下执行传输任务,例如当设备充电或者连上 Wi-Fi 时。
  4. 使用 NSURLSessionConfiguration 实例来创建一个 NSURLSession 实例,这个 session 必须提供一个代理对象,来接收后台传输的各种事件。
1
2
3
4
5
6
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "MySession")
config.isDiscretionary = true
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()

注意
想要获取更多关于系统如何调度及执行后台任务,从 Bug 报告Profiles and Logs下载并安装后台网络资料到你的 iOS 设备中。

创建并安排 Download Task

你可以提供一个 URL 使用 session 的 downloadTaskWithURL: 方法创建一个 download task,或者提供一个 URLRequest 实例通过 downloadTaskWithRequest: 创建。你可以设置属性来帮助系统优化 task 的行为。

  1. 如下代码所示,使用downloadTaskWithURL创建一个 download task。
  2. 这一步是可选的,设置 earliestBeginDate 属性来安排下载在未来的某个时间点开始,下载并不保证会精确到这个时间进行,但是不会开始的更早。
  3. 为帮助系统更有效的安排网络活动,设置 countOfBytesClientExpectsToSendcountOfBytesClientExpectsToReceive 属性,这些值是期望传输数据的猜测最接近的上限,而且应该把 header 和 body 数据都计算入内。
  4. 调用resume启动这个 task.

在下面的代码中,任务被设置为最早一个小时后,而且配置为发送大约 200 字节的数据,接收大约 500 KB 的数据。

1
2
3
4
5
let backgroundTask = urlSession.downloadTask(with: url)
backgroundTask.earliestBeginDate = Date().addingTimeInterval(60 * 60)
backgroundTask.countOfBytesClientExpectsToSend = 200
backgroundTask.countOfBytesClientExpectsToReceive = 500 * 1024
backgroundTask.resume()

处理 App 挂起

不同的 app 状态会影响到你的后台下载任务如何与 app 交互。在 iOS 中,你的 app 可能处于 foreground,suspended,或者甚至被系统 terminated。查看 Managing Your App’s Life Cycle获取更多关于 app 状态的信息。

如果你的 app 在 background 状态,当其他进程的下载运行时,系统可能会 suspend 你的 app,这种情况下,当下载完成时,系统会恢复(resume)你的 app 并且调用 UIApplicationDelegate 的 application:handleEventsForBackgroundURLSession:completionHandler: 方法。这个方法接收你前面创建的 session 标识符作为第二个参数。

这个代理方法还接受一个 completion handler 作为其最后一个参数。你应该立即将这个 handler 保存在它有意义的地方。或许作为 app delegate 或者实现了NSURLSessionDownloadDelegate的类的属性。在下面代码中,completion handler 被保存在了 app delegate 的名为backgroundCompletionHandler的属性中了。

1
2
3
4
5
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
backgroundCompletionHandler = completionHandler
}

当所有事件传送完成后,系统会调用NSURLSessionDelegateURLSessionDidFinishEventsForBackgroundURLSession: 方法,这时,取出保存在 app delegate 中的backgroundCompletionHandler然后执行它。下面的代码展示了这个过程。

注意URLSessionDidFinishEventsForBackgroundURLSession:方法或许会从其他 queue 中被调用,所有需要显式的切换到 main queue 中执行 handler(这个 handler 是从一个 UIKit 方法中接收的)。

1
2
3
4
5
6
7
8
9
10
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let backgroundCompletionHandler =
appDelegate.backgroundCompletionHandler else {
return
}
backgroundCompletionHandler()
}
}

如果 App 被 terminate 了则会重新创建 Session

如果在 app 处于 suspend 状态时被系统 terminate 了,系统会在后台重启 app。作为启动 的一部分,系统会使用同样的 session 标识符重新创建一个 background session。以便于系统将 download task 与你的 session 重新联系起来。这样不论 app 是被用户还是被系统启动,background session 都已经做好准备了。一旦 app 重新登录,一系列的事件就像上一部分讨论的那样,好像 app 被 suspend 然后 resume 了一样。

注意
假如传输过程是在 app 处于 background 状态时被启动的,session 的配置中discretionary属性会被当做YES

处理下载完成和错误

在 app resume 后(或者在前台了),你的NSURLSessionDownloadDelegate协议方法的实现会接收回调来更新传输的状态。

通过实现 URLSession:downloadTask:didFinishDownloadingToURL: 方法来处理下载完成。检查 download task 的 response 看是否有服务器端的错误如 404 状态码。如果有的话,则并没有得到下载的文件,这时应该退出了。如果下载完成,则最后一个参数是一个存放文件的本地 URL,这个地址只是在这个回调中有效,所以你应该将其转移到其他位置,例如 app 的文档目录下。

下面的代码展示了URLSession:downloadTask:didFinishDownloadingToURL:的实现。这个实现通过检查服务端错误码及移动文件到文档目录下完善了 background download 的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
guard let httpResponse = downloadTask.response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print ("server error")
return
}
do {
let documentsURL = try
FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
let savedURL = documentsURL.appendingPathComponent(
location.lastPathComponent)
try FileManager.default.moveItem(at: location, to: savedURL)
} catch {
print ("file error: \(error)")
}
}

要处理客户端的下载错误,例如连接不到服务器这种,需要实现URLSession:task:didCompleteWithError: 方法。这个方法会在任何 task 完成时被调用;你只需要在最后一个参数不为 nil 的时候处理错误。

遵守后台传输限制

通过 background session 传输,真正的传输过程是被与你的 app 不同的进程所执行的。因为重启你 app 的进程是相当耗费资源的操作,一些特性会变得不可用,导致如下的限制:

  • Session 必须为每一个事件传递提供一个代理(对于上传或下载,代理与进程内传输表现一致)。
  • 仅支持 HTTP 或者 HTTPS 协议(不支持自定义协议)。
  • 总是会执行重定向,因此即使你实现了 URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler: 方法,也不会被调用。
  • 只支持上传文件(上传一个数据实例或者 stream 会在 app 退出后失败)。

Requests 和 Responses

NSURLRequest

NSMutableURLRequest

NSURLResponse

NSHTTPURLResponse

缓存行为

获取缓存数据

控制 URL request 对之前缓存数据的使用。

概述

URL 加载系统会将 response 缓存到内存和磁盘中,以提升性能减少网络阻塞。

NSURLCache 类用于从网络资源中缓存 response,你的 app 可以通过 URLCache 的 sharedURLCache 属性获取到共享的缓存实例,或者也可以,在你的NSURLSessionConfiguration对象上设置不同的 cache 来创建自己的 cache 用于其他用途。

设置 URL Request 的缓存策略

每一个 URLRequest 实例包含一个 URLRequest.CachePolicy 对象来指示是否应该缓存以及缓存应该如何被执行。你可以改变 request 的这个策略来控制它的缓存行为。

方便起见,NSURLSessionConfiguration有一个 requestCachePolicy 的属性,所有通过使用这个配置的 session 创建的 request 都会从配置中继承这个缓存策略。

各种不同的缓存策略的行为描述在下表中。这个表展示了策略对于直接从缓存中加载还是访问数据源即服务器或本地文件系统的偏好。当前,仅有 HTTP 和 HTTPS response 会被缓存,对于 FTP 或者文件 URL,策略的唯一作用就是决定 request 是否被允许访问数据源。

缓存策略 本地缓存 数据源
NSURLRequestReloadIgnoringLocalCacheData 忽略 Accessed exclusively(不好翻译..)
NSURLRequestReturnCacheDataDontLoad Accessed exclusively 忽略
NSURLRequestReturnCacheDataElseLoad 先试一次 仅在需要时访问
NSURLRequestUseProtocolCachePolicy 依据协议 依据协议

查看 NSURLRequestCachePolicy获取更多关于 HTTP 和 HTTPS 中是如何实现useProtocolCachePolicy的说明。useProtocolCachePolicy是 URLRequest 对象的默认值。

注意
useProtocolCachePolicy将 HTTPS response 缓存到磁盘中,这可能对于保护用户数据安全来说不太理想,你可以通过像在Manage Caching Programmatically描述的一样,手动处理缓存行为来修改这种行为。

直接获取缓存

你可以通过 session 的 configuration 对象的 URLCache 属性来获取或者设置 URLSession 对象所使用的缓存对象。

对缓存对象调用 cachedResponseForRequest: 来查找一个给定 request 的 response 缓存。如果这个 request 的缓存数据存在的话就会返回一个 NSCachedURLResponse 对象,否则返回 nil.

你可以检查 cache 对象所使用的资源,currentDiskUsagediskCapacity 属性代表了缓存所使用的文件系统资源。currentMemoryUsagememoryCapacity 属性代表了缓存所使用的内存。

你可以使用 removeCachedResponseForRequest: 方法来删除单个缓存数据。也可以使用 removeCachedResponsesSinceDate: 来同时删除一个给定时间后的多个缓存数据。或者使用 removeAllCachedResponses 删除整个缓存。

通过写程序来管理缓存过程

你可以使用 storeCachedResponse:forRequest: 方法传入一个新的CachedURLResponse对象和一个URLRequest对象通过编写代码来写入缓存。

尤其是URLSessionTask对象处理的 response 的缓存,要在每一个 response 的基础上控制缓存,需要实现NSURLSessionDataDelegate协议的 URLSession:dataTask:willCacheResponse:completionHandler: 方法。注意这个代理方法只会对 upload task 和 data task 调用,而对于有 background 或 ephemeral(不会翻译了,暂时的?) 配置的 session 并不调用。

这个代理接收两个参数:一个CachedURLResponse对象和一个 completion handler。你的代理实现中必须直接调用这个 completion handler,传入下面的参数之一:

  • 提供的CachedURLResponse对象,来照原样缓存 response
  • nil, 来阻止缓存
  • 一个新创建的CachedURLResponse对象,典型的情况是基于提供的对象,按需求修改一下storagePolicyuserInfo 字典。

下面的代码展示了一个urlSession(_:dataTask:willCacheResponse:completionHandler:)的实现代码,其中拦截了 HTTPS 的请求,并且只允许将其缓存存储在内存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,
willCacheResponse proposedResponse: CachedURLResponse,
completionHandler: @escaping (CachedURLResponse?) -> Void) {
if proposedResponse.response.url?.scheme == "https" {
let updatedResponse = CachedURLResponse(response: proposedResponse.response,
data: proposedResponse.data,
userInfo: proposedResponse.userInfo,
storagePolicy: .allowedInMemoryOnly)
completionHandler(updatedResponse)
} else {
completionHandler(proposedResponse)
}
}

认证和证书

处理认证询问

当服务器对一个 URL request 询问认证时进行合适的响应。

概述

当你的 app 使用一个NSURLSessionTask进行请求时,服务器可能会在继续进行前返回一个或多个认证询问。session task 会尝试帮你处理,当处理不了时,会调用你 session 的代理来处理这个问询。

实现这个段落中提到的代理方法来回答与你 app 连接的服务器的询问。如果你不实现这些代理方法,你的 request 可能会被服务器拒绝,你会收到一个包含 HTTP 状态码 401(Forbidden)的 response 而不是你想要的数据。

决定适合的代理方法

依据你接收到的问询的种类,实现下面的一个,或者两个代理认证方法都实现。

  • 实现NSURLSessionDelegate协议的处理整个 session 范围内的认证问询的 URLSession:didReceiveChallenge:completionHandler: 方法。这种问询就与传输层安全(TSL)生效一样,一旦你成功的处理了这种问询,那这个 NSURLSession 创建的所有 task 都会持续生效。
  • 实现NSURLSessionTaskDelegate协议的处理指定 task 的问询的 URLSession:task:didReceiveChallenge:completionHandler: 方法,这种问询是像请求 username/password 认证这种。从一个 session 中创建的每个 task 会提出各自的问询。

注意
查看 NSURLProtectionSpace Authentication Method Constants了解哪个问询方法是作用于整个 session 的,哪个是针对 task 的。

一个简单的例子,考虑一下当你请求一个被 HTTP 基本认证保护的 http URL(如RFC 7617.中定义的),因为是一个指定 task 的认证,你需要实现URLSession:task:didReceiveChallenge:completionHandler:方法来处理它。

注意
如果你是通过 https 连接的,你还会接收到一个服务器信任的认证,查看Performing Manual Server Trust Authentication获取处理这种作用于 session 的认证的信息。

下图展示了回复 HTTP 基本认证的策略759d2099-d938-415f-ac8a-1a0cac9dea4b
下面的章节描述了这个策略的实现

决定认证问询的种类

当你接收到一个认证问询时,使用代理方法来决定认证的种类。代理方法接收一个描述所提出的问询NSURLAuthenticationChallenge实例,这个实例包含一个protectionSpace属性,它的authenticationMethod属性表明了服务器所提出的认证种类(例如请求一个用户名和密码,或者一个客户端证书)。你使用该值来决定是否能处理这个认证问询。

你通过直接调用传入的 completion handler 来回复这个认证询问,向 handler 中传入一个NSURLSessionAuthChallengeDisposition 来表明你对认证问询的回复。你可以使用这个 disposition 参数来提供如下选择中合适的:一个认证,取消请求,或者允许默认的处理来进行。

下面代码检查了authenticationMethod是否是所期待的 HTTP 基本类型。如果authenticationMethod属性指明了其他种类的问询,它会调用 completion handler 并传入NSURLSessionAuthChallengePerformDefaultHandling处理参数。告诉 task 使用满足认证问询的默认处理;否则 task 会继续直到下一个认证问询再次调用这个代理方法。这个过程会继续执行直到等到你要处理的 HTTP 基本认证。

1
2
3
4
5
let authMethod = challenge.protectionSpace.authenticationMethod
guard authMethod == NSURLAuthenticationMethodHTTPBasic else {
completionHandler(.performDefaultHandling, nil)
return
}

创建一个凭证实例

你需要依据接收到的认证问询的种类来提交一个合适的凭证来回复认证。对于 HTTP 基本认证和 HTTP 摘要认证,你可以提供一个 username 和 password。下面的代码展示了一个从用户界面创建 NSURLCredential实例的帮助方法。

1
2
3
4
5
6
7
8
func credentialsFromUI() -> URLCredential? {
guard let username = usernameField.text, !username.isEmpty,
let password = passwordField.text, !password.isEmpty else {
return nil
}
return URLCredential(user: username, password: password,
persistence: .forSession)
}

在这个例子中,返回的NSURLCredential实例有个 NSURLCredentialPersistenceForSession 持久化策略,所以它只会保存在创建这个 task 的NSURLSession实例中,对于其他 session 实例创建的 task 你需要提供新的NSURLCredential实例,对于 app 以后运行时也需要创建新的实例。

调用 Completion Handler

一旦你尝试创建 credential 实例,你必须要调用 completion handler 来回复认证问询。

下面的代码展示了这两种情况

1
2
3
4
5
guard let credential = credentialOrNil else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
completionHandler(.useCredential, credential)

如果你传入了一个服务器接受的凭证,这个 task 会继续执行上传或下载数据。

重要
你可以将 completion handler 传给其他方法或者将其短暂的保存在属性中,用于等待用户完成 username/password 输入。但是你最终还是要调用 completion handler 来完成认证使得 task 继续执行,即使你选择像下面的失败情况处理中那样取消认证。

优雅的处理失败情况

如果凭证被拒绝,系统会再次调用你的代理方法。这种情况下,回调会将你被拒绝的凭证作为NSURLAuthenticationChallenge参数的属性proposedCredential传回给你。这个NSURLAuthenticationChallenge实例中还包含一个previousFailureCount属性,这个属性表示你的凭证被拒绝的次数。你可以使用这个参数来决定接下来怎么做。举个例子,如果previousFailureCount 比 0 大,你可以在使用已提交的凭证的字符串来在用户界面上插入一个重新输入 username/password 的界面。

执行服务器信任鉴定

在你的 app 中进行服务器安全凭证的评估。

概述

当你通过 URL request 使用安全连接时(例如 https),你的 NSURLSessionDelegate会接收到一个 NSURLAuthenticationMethodServerTrust 认证类型的认证问询。与其他的服务器要求你的 app 证明自己身份的问询不同,这里是你来鉴定服务器的凭证的机会。

决定何时进行服务器信任鉴定

大部分情况下,你应该将评估服务器信任度的工作交由 URL 加载系统的默认处理流程去做。当你没有设置代理或者没有处理认证问询时就会执行这些默认的行为。然后在下面这些场景中自己操作评估过程可能会更有用:

  • 你想要接受服务端证书而在默认情况下会被系统拒绝的情况。举个例子,你的 app 想要与使用自签名证书的开发服务器之间建立安全连接,而这个证书一般又不会与系统的信任证书的存储中的内容相匹配。
  • 你想要拒绝这个证书而在默认情况下会被系统接受。举个例子,你想要你的 app 固定在几个由你控制的 key 或证书上,而不是接受任何有效的证书。

下图展示了你的 app 如何通过提供一个代理方法来处理认证问询来执行手动的证书校验。这样就绕过了系统的默认处理。代理会直接比较服务器证书或公钥与存储在 app bundle 中的 证书或公钥(或哈希值等等)的拷贝。如果代理中判断这个服务器凭证是有效的,他就会信任这个服务器并允许继续连接。
851916ce-5c1c-4c23-b45b-c4632dacf24c

注意
如果你要连接的域名开启了 App Transport Security (ATS) ,NSURLSession 会强制使用。它应用于证书, TSL 版本,连接用的密码等安全要求。你不能放松一个使用 ATS 保护的域名的服务器的信任要求,但是你可以使用文章中面熟的技术收紧要求。查看 Information Property List Key Reference文章中的 NSAppTransportSecurity 获取更详细的信息。

处理服务器信任认证问询

实现NSURLSessionDelegateURLSession:didReceiveChallenge:completionHandler:方法来手动处理服务器信任认证。当这个方法被调用时,你的实现中首先需要检查:

  • 问询种类是服务器信任,而不是其他类型的问询。
  • 问询的主机名与你想要处理证书校验的证书匹配。

下面的代码展示了这些情况,根据传入URLSession:didReceiveChallenge:completionHandler:的 challenge 参数的 protectionSpace 来检查上面列出的两项。首先,从 protection space 中获取 authenticationMethod 检查认证问询的类型是不是 NSURLAuthenticationMethodServerTrust,然后保证 protection space 中的 host匹配期待的主机名 example.com,如果这两个条件有一个没有满足,则会调用 completion handler 并传入 NSURLSessionAuthChallengePerformDefaultHandling来让系统采用默认的方法处理。

1
2
3
4
5
6
7
let protectionSpace = challenge.protectionSpace
guard protectionSpace.authenticationMethod ==
NSURLAuthenticationMethodServerTrust,
protectionSpace.host.contains("example.com") else {
completionHandler(.performDefaultHandling, nil)
return
}

校验 Challenge 中的凭证

获取 protection space 的 serverTrust属性(SecTrustRef类的实例)来访问服务器的凭证。下面的代码展示了如何访问服务器凭证并接受或拒绝它。代码首先尝试从 protection space 中获取serverTrust属性,如果为空的话就回退到默认的处理去。然后将服务器凭证传给一个私有方法checkValidity(of:)来比较服务器凭证中的证书或公钥是否有 app bundle 中保存的有效值匹配。

1
2
3
4
5
6
7
8
9
10
11
12
guard let serverTrust = protectionSpace.serverTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
if checkValidity(of: serverTrust) {
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
// Show a UI here warning the user the server credentials are
// invalid, and cancel the load.
completionHandler(.cancelAuthenticationChallenge, nil)
}

一旦代码决定了服务器凭证的有效性,它会采取下面的两个动作:

Tip
查看 Certificate, Key, and Trust Services 了解更多关于如何校验SecTrustRef 实例或者从中获取证书或公钥。

创建长期的服务器认证策略

如果你需要在某些情况下手动验证服务器认证信息,安排好你 app 的工作如果你想要修改服务器凭证,要考虑下列准则:

  • 将你的服务器凭证与一个公钥匹配,而不是在 app bundle 中存储一个单独的证书。这允许你对同样的 key 重新发布证书来更新服务器,而不需要更新 app。
  • 比较发布的 certificate authority’s(CA’s) key,而不是比较末端的 key,这样你可以部署包含新 key 的使用同一个 CA 签名的证书。
  • 使用一组 keys 或 CAs ,这样装换服务器凭证时更方便。

NSURLAuthenticationChallenge

NSURLCredential

NSURLCredentialStorage

NSURLProtectionSpace

Cookie

NSHTTPCookie

NSHTTPCookieStorage

Errors

URL Loading System Error Codes

URL Loading System Error Info Keys

遗留版本

Legacy URL Loading Systems

将你的代码从使用这些遗留版本的对象迁移到新的。

原文:Understanding JavaScript Bind ()

前言

稍微写多点儿 JS 的人应该都见过var self = this这种写法,它是为了解决在不同函数调用时,this所指向的上下文参数变化的问题,你可以通过MDN this这篇文章先了解一下 JS 中的this。下文提供了将函数与其想要的this绑定的方法,以下是翻译正文。

理解 JS 中的 Bind()

当你初学 JavaScript 时你可能并不关心函数绑定的问题,但是当你需要一个在其他函数中保持this内容的解决方案时,你可能并没有意识到你真正需要的就是Function.prototype.bind()函数。

第一次遇到这个问题时,当你切换上下文时可能会将this赋值给一个可以引用的变量。大多数人会选择self_thiscontext作为变量名,这种方法是可用的,并不会出错,但是还有一种更好、更优雅的方式。

Jake Archibald发推讨论过捕获this的问题:

Ohhhh I would do anything for scope, but I won’t do that = this — Jake Archibald (@jaffathecake) February 20, 2013

Sindre Sorhus讨论这个问题时,答案已经很显然了:

@benhowdle $this for jQuery, for plain JS i don’t, use .bind() — Sindre Sorhus (@sindresorhus) February 22, 2013

但是我却忽略了好几个月。

我们想要解决什么问题?

在下面的代码中,将上下文对象赋值给一个变量是情有可原的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var myObj = {
specialFunction: function () {
},

anotherSpecialFunction: function () {
},

getAsyncData: function (cb) {
cb();
},

render: function () {
var that = this;
this.getAsyncData(function () {
that.specialFunction();
that.anotherSpecialFunction();
});
}
};

myObj.render();

如果我们在上面的代码中直接调用this.specialFunction(),那么就会看到如下的错误信息:

1
Uncaught TypeError: Object [object global] has no method 'specialFunction'

我们需要持有myObj对象的上下文用于回调函数的调用,调用that.specialFunction()函数使我们持有上下文并且正确的执行我们的函数。但是使用Function.prototype.bind()是更优雅的方法。

我们写个例子:

1
2
3
4
5
6
render: function () {
this.getAsyncData(function () {
this.specialFunction();
this.anotherSpecialFunction();
}.bind(this));
}

上例中做了什么?

.bing()只是创建了一个新的函数,当它被调用时会将this关键字设置为之前提供的值。这样我们就可以向.bind()函数传入期望的上下文参数即this(在这里就是myObj),然后当回调函数执行时,this就引用了myObj对象。

如果你想看看Function.prototype.bind()这个函数内部是如何运行的,可以看下面这个简单的例子:

1
2
3
4
5
6
Function.prototype.bind = function (scope) {
var fn = this;
return function () {
return fn.apply(scope);
};
}

下面是个简单的用例:

1
2
3
4
5
6
7
8
9
10
var foo = {
x: 3
}
var bar = function(){
console.log(this.x);
}

bar(); // undefined
var boundFunc = bar.bind(foo);
boundFunc(); // 3

我们创建了一个新的函数,其执行时会将this设置为foo对象,而不是像例子中直接调用bar()时,this默认指向的全局对象。

浏览器支持

浏览器 支持的版本
Chrome 7
Firefox(Gecko) 4.2(2)
Internet Explore0 9
Opera 11.60
Safari 5.1.4

如上所示,在 Internet Explorer 8 及以下版本中并不支持Function.prototype.bind()函数,所以你需要一个备用方案。

幸好,MDN提供了一个可靠的备选方案,用于没有在本地实现.bind()方法的浏览器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}

var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};

fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();

return fBound;
};
}

用法范例

我发现当学习东西时,不仅要透彻的学习它的概念,还要将它用在实践中。幸好,下面的几个例子可以用于你的代码中或者解决你的问题。

点击事件处理

通常用于记录点击事件(或者点击后执行一个动作),这时就需要将信息保存在一个对象中,如下:

1
2
3
4
5
6
7
var logger = {
x: 0,
updateCount: function(){
this.x++;
console.log(this.x);
}
}

我们可能会像下面这样添加点击事件处理,在其中调用logger对象的updateCount()方法:

1
2
3
document.querySelector('button').addEventListener('click', function(){
logger.updateCount();
});

但是为使得updateCount()函数中的this关键字表示正确的值,我们需要创建一个并不必须的匿名函数。

可以像下面这样优化:

1
document.querySelector('button').addEventListener('click', logger.updateCount.bind(logger));

我们可以使用方便的.bind()函数来创建一个新的函数,然后将作用域设置的绑定到logger对象。

SetTimeout

如果你尝试过模板引擎(例如Handlebars)或者某种 MV* 框架(如 Backbone.js),你可能会碰到这样的问题:当你渲染模板时,在调用了渲染方法后,想要立即获取新的 DOM 节点可能就会出错。

假设我们再初始化一个 jQuery 插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
var myView = {
template: '/* a template string containing our <select /> */',
$el: $('#content'),
afterRender: function () {
this.$el.find('select').myPlugin();
},
render: function () {
this.$el.html(this.template());
this.afterRender();
}
}

myView.render();

你可能会发现这段代码可以正常运行,但并不总是这样。问题就在这里,它产生了一种竞争的情况:有时候渲染先执行完成,有时候插件的初始化先执行完成。

有些人可能不知道,我们可以通过使用setTimeout()函数来解决问题。

像下面这样简单的修改下代码,使得我们可以在 DOM 节点展示完成后立马安全的初始化我们的 jQuery 插件:

1
2
3
4
5
6
7
8
9
10
//
afterRender: function () {
this.$el.find('select').myPlugin();
},

render: function () {
this.$el.html(this.template());
setTimeout(this.afterRender, 0);
}
//

然而,我们会看到找不到.afterRender()函数的报错信息。

是时候祭出我们的.bind()方法了:

1
2
3
4
5
6
7
8
9
10
//
afterRender: function () {
this.$el.find('select').myPlugin();
},

render: function () {
this.$el.html(this.template());
setTimeout(this.afterRender.bind(this), 0);
}
//

现在,我们的afterRender()函数就会执行在正确的上下文环境中了。

整理通过 querySelectorAll 绑定的事件

自从添加了诸如querySelector,querySelectorAllclassList等有用的方法后,DOM API 提升了很多。

然而,到目前为止并没有一个原生的方法来对一个NodeList中的所有节点添加事件,因此我们还需要使用Array.prototype中的forEach方法来循环添加:

1
2
3
Array.prototype.forEach.call(document.querySelectorAll('.klasses'), function(el){
el.addEventListener('click', someFunction);
});

我们可以通过使用.bind()方法来优化一下:

1
2
3
4
5
6
var unboundForEach = Array.prototype.forEach,
forEach = Function.prototype.call.bind(unboundForEach);

forEach(document.querySelectorAll('.klasses'), function (el) {
el.addEventListener('click', someFunction);
});

现在我们有一个整齐的方法来循环我们的 DOM 节点了。

结论

如你所见,JS 的bind()函数可以巧妙的用于各种用途或者代码的整理。期望你能在需要时将.bind()添加进你的代码中来驾驭this值转换的能力。

算法

LeetCode-Swift
【图解数据结构】 目录(持续更新)
swift-algorithm-club
swift-algorithm-club翻译

TCP/IP及网络相关

TCP 协议简介
协议森林
面试官,不要再问我三次握手和四次挥手
IP,TCP 和 HTTP
终于有人把 HTTPS 原理讲清楚了!

TCP 的那些事儿 上
TCP 的那些事儿 下

WebSocket 教程
微信,QQ这类IM app怎么做——谈谈Websocket

为什么这么设计系列文章
不为人知的网络编程(一):浅析TCP协议中的疑难杂症(上篇)
TCP/IP指南
HTTP API 认证授权术
网络数字身份认证术
HTTP2简介和基于HTTP2的Web优化

编译

扯淡:大白话聊聊编译那点事儿
大前端开发者需要了解的基础编译原理和语言知识
链接器:符号是怎么绑定到地址上的?

iOS 微信编译速度优化分享
深入剖析 iOS 编译 Clang / LLVM
深入剖析 iOS 编译 Clang / LLVM 直播的 Slides
肆 链接 - 不同的代码如何协同
Mach-O 可执行文件
编译器

数据库

iOS端数据库解决方案分析
微信iOS SQLite源码优化实践
微信客户端 SQLite 数据库修复实践
在 iOS 的 SQLite 数据库中应用 FMDB 库
iOS端数据库解决方案分析
MMKV for iOS/macOS

C++

iOS 开发者应该掌握些 C++ 知识

C

C 语言中的指针与数组

Unicode

8D85-8D8A-6280-672F ?
NSString 与 Unicode

OPENGL

20分钟让你了解OpenGL ——OpenGL全流程详细解读
从零讲解 iOS 中 OpenGL ES 的纹理渲染
Learning OpenGL(ES) —— OpenGL Model, Pipeline and Practices
OpenGL ES落影合集

计算机程序的构造与解释

Learning-SICP

科学上网

写给非专业人士看的 Shadowsocks 简介
科学上网
Surge 官方中文指引:理解 Surge 原理

Git

Git 使用规范流程
🥡Git 菜单

Shell

The Linux Commond Line

正则表达式

learn-regex
正则表达式
正则表达式30分钟入门教程
NSPredicate Cheatsheet

机器学习入门

机器学习原来这么有趣!第一章:全世界最简单的机器学习入门指南
https://microsoft.github.io/ML-For-Beginners

前端

6k 字总结 flexbox 布局 ,收藏就行
JavaScript权威面试指南

注:这篇基本上是重新翻看《Objective-C 基础教程》时的一些阅读笔记,内容偏入门级。熟悉 ObjC 的就不需要翻啦(ー`´ー)。

历史

早在 20 世纪 80 年代初,Brad Cox 为了融合流行的、可移植的 C 语言和优雅的 Smalltalk 语言的优势,设计出了 Objective-C 语言,它是 C 语言的一个扩展集。1985年,Steve Jobs 创立了 NeXT 公司,他们使用 Objective-C 语言基于 Unix 开发了 NeXTSTEP 操作系统。而在 Apple 收购了 NeXT 之后,从 NeXTSTEP 和 OPENSTEP 编程环境演化出来了著名的 Cocoa 编程工具箱,从此 Cocoa 和 Objective-C 就成了 Apple 公司 OS X 和 iOS 操作系统的核心。

Objective-C 小知识点

  • Xcode 通过 .m 扩展名来表示文件使用的时 Objective-C 代码,应由 Objective-C 编译器处理。而 C 编译器处理 .c 文件,C++ 编译器处理 .cpp 文件。所有这些编译工作默认由 LLVM 处理。(扩展名 .m 表示 message)
  • 通过#import导入的头文件使用预编译头文件(压缩的、摘要形式的头文件)的方式来加快读取速度。
  • 导入头文件使头文件和源文件之间建立了一种紧密的依赖关系。如果头文件有任何变化,那么所以依赖 它的文件都得重新编译。
  • 头文件中的@class指令用于创建一个前向引用,在编译器只需要知道这是一个类,后面只会通过指针去引用它时提供了一个缩短编译时间的好方法,此外,还可以有效解决两个类之间循环依赖的问题。但是在诸如继承时则不能使用,因为编译器需要知道所有超类的信息才能成功为其子类编译@interface部分。
  • @selector()返回一个指向有特定名称的选择器的 SEL 指针。什么是选择器呢?选择器只是一个方法名称,但它以 Objective-C 运行时使用的特殊方式编码,以快速的执行查询,可以使用@selector()编译指令圆括号中的方法名称来指定选择器。
  • @protocol()返回一个指向有特定名称的协议的 Protocol * 指针。
  • Objective-C 运行时生成一个类的时候,会创建一个代表该类的类对象。类对象包含了指向超类、类名和类方法列表的指针,还包含一个 long 型的数据,为新创建的实例对象指定大小。用来创建新对象的类方法称为工厂方法。

SEL-Methods-IMP

布尔类型

在早期的 32 位系统下,BOOL实际上是一种对带符号的字符类型(signed char)的typedef,它使用 8 位的存储空间,通过#define指令把YES定义为 1,NO定义为 0。编译器只将BOOL认作 8 位二进制数,所以将大于 1 字节的整型值赋给一个BOOL变量,那么只有低位字节会被用作BOOL值。
Objective-C中的真值类型和数值图:BOOL / bool / Boolean / NSCFBoolean
目前在64位 iOS, tvOS, watchOS 系统中 BOOL 其实是 bool 的 typedef,也就是说 BOOL 只有0(NO),1(YES)两个值。

1
2
3
4
// iOS, tvOS, watchOS:
typedef bool BOOL;
// macOS
typedef signed char BOOL;

OOP

OOP 是一种编程架构,可构建由多个对象组成的软件。软件就好比存在于计算机中的小零件,它们通过互相传递信息来完成工作。
过程式编程建立在函数之上,数据为函数服务,而面向对象编程则以程序的数据为中心,函数为数据服务。数据可以通过间接方式引用代码,代码可以对数据进行操作。
对象到底是什么呢?对象是一种包含值和指向其类的隐藏指针的结构体。类是一种能够实例化成对象的结构体,类含有一个指针用于指向实现某个功能的代码。(类对象有什么用呢?让每个对象直接指向各自的代码不是更简单嘛?确实是更简单一些,而且某些 OOP 系统也是这样做的。但是拥有类对象会具备极大的优势,如果在运行时改变某个类,则该类的所有对象都会自动继承这些变化。)
在 Objective-C 中调用方法时,一个名为 self 的秘密隐藏参数将被传递给接收对象,而这个参数引用的就是该接收对象,例如,在代码[circle setFillColor:kRedColor]中,方法将 circle 作为 self 参数进行传递。由此方法可以使用此隐藏的 self 参数查找并操作对象的数据。

继承

方法调度

对象在收到消息时,如何知道要执行哪个方法呢?当代码发送消息时,Objective-C 的方法调度机制将在当前类中搜索相应的方法,如果无法在接受消息的对象的类文件中找到相应的方法,它就会在该对象的超类中进行查找。

支持继承程序中的方法调度

实例变量

在创建一个新类时,其对象首先会从它的超类继承实例变量,然后根据自身情况添加自己的实例变量。

1
2
3
4
5
@interface RoundedRectangle : Shape
{
int radius;
}
@end

下图展示了RoundedRectangle对象的内存布局。
对象中实例变量的布局
最上面是 NSObject 对象声明的名为 isa 的实例变量,它保存着指向对象当前类的指针,接下来是由 Shape 类声明的两个实例变量 fillColor 和 bounds,最后是由 RoundedRectangle 类声明的实例变量 radius。
每个方法调用都获得了一个名为 self 的隐藏参数,它是一个指向接收消息的对象的指针,self 指向继承链中第一个类的第一个实例变量,如上图所示也就是 isa 变量。因为编译器已经看到了所有这些类的 @interface 声明,也就知道了对象中的实例变量的布局,根据这个基地址再加上偏移地址,编译器就可以查找其他实例变量的位置了。
脆弱的基类问题:在 Snow Leopard 和 iOS4.0 系统中引入 64 位的 Objective-C 运行时之前,即使苹果工程师想在 NSObject 中添加其他的实例变量也是无法做到的,因为在编译器生成的程序中,那些偏移位置是通过硬编码实现的。在引入运行时之后它使用间接寻址方式确定了变量的位置(把实例变量当做一种存储偏移量所用的特殊变量,交由类对象管理,偏移量会在运行时查找,如果类的定义变了,那么存储的偏移量也就变了。因此任何时候都能访问到实例变量正确的偏移量,甚至可以在运行时向类中新增实例变量,这就是稳固的 ABI 机制,通过这个机制我们可以在类扩展或实现文件中定义实例变量),从而解决了这个问题。

super

为了调用继承的方法在父类中的实现,需要使用 super 作为方法调用的目标。super 既不是参数也不是实例变量。当你向 super 发送消息时,实际上是在请求 Objective-C 向该类的超类发送消息。 如果超类中没有定义该消息,Objective-C 会和平常一样继续在继承链上一级中查找。
调用超类的方法

存取方法

如果要对其他对象中的属性进行操作,应该尽量使用对象提供的存取方法,绝对不能直接改变对象里面的值,例如:main()函数不应该直接访问 Car 类的 engine 实例变量(通过car->engine的方法)来改变 engine 的属性,而应该使用 setter 方法进行更改。
在 Objective-C 中所有对象间的交互都是通过指针实现的。

Foundation

为什么诸如 CGRect, CGPoint, CGSize 等数据类型是 C 的 struct 而不是对象呢?原因在于性能!GUI 程序通常会使用许多临时的坐标、大小和矩形区域来完成工作。但是所有的 Objective-C对象都是动态分配的,而动态分配是一个代价较大的操作,它会消耗大量的时间。

NSString

C 字符串是将字符串作为简单的字符数组进行处理,并且在数组最后添加尾部的零字节作为结束标志。
NSString 的 length 实例方法能够精确无误的处理各种语言的字符串。因为一个字符占用的可能多余一个字节。这样在 C 语言的strlen()函数只能计算字节数,就会返回错误的数值。

NSArray

NSArray 是用来存储对象的有序列表,你可以在 NSArray 中放入任意类型的对象,但是它只能存储 Objective-C 对象,而不能存储原始的 C 语言基础数据类型,如 int, float, enum, struct 和 NSArray 中的随机指针。此外,它还不能存储 nil。
没有创建 NSMutableArray 和 NSMutableDictionary 的字面量语法。
对可变数组进行枚举操作时,需要注意不能通过添加或删除这类方式来改变数组的容量
NSArray 中添加了通过代码块来枚举对象的方法:

1
2
3
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];

为什么有了快速枚举还要代码块枚举呢?因为通过代码块可以让循环操作并发执行,而通过快速枚举,执行操作要一项项地线性完成。

NSDictionary

为什么不用数组存储而要用字典呢? 因为字典(也被称为散列表)使用的是键查询的优化方式,可以立即找出要查询的数据而不需要遍历整个数组。
尽量不要创建 NSString, NSArray, NSDictionary 的子类,因为它们都是以类簇的方式实现的。

NSValue

NSValue 可以封装任意值。

NSPredicate (谓词)

NSPredicate用于指定数据被过滤的条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface Engine: NSObject
@property (nonatomic, assign) NSInteger horsepower;
@end

@implementation Engine
@end

@interface Car : NSObject
@property (nonatomic, strong) Engine * engine;
@end

@implementation Car
@end

Engine *engine = [[Engine alloc] init];
engine.horsepower = 300;
Car *car = [[Car alloc]init];
car.engine = engine;

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"engine.horsepower >= 120"];
BOOL match = [predicate evaluateWithObject:car];

-evaluateWithObject:计算指定对象 car 是否满足谓词 predicate 中的条件。本类的谓词中使用 engine.horsepower 作为键路径,对 car 对象应用valueForKeyPath:方法获取引擎的马力。然后比较其是否大于等于 120。
NSPredicate 一般用于对集合类中数据的过滤,使用方法可以参考 NSHipster 的这篇文章:NSPredicate。另外,在谓词字符串中可以使用 LIKE 如:"name LIKE '???er*'" 将会匹配 er 前有3个字符,后面还有一些字符的 name 字符串变量。 也可以是使用 MATCHES 运算符类赋给它一个正则表达式,从而来选择匹配的值。

内存管理

如果一个对象内有指针指向其他对象的实例变量,则称该对象拥有这些对象。如果一个函数创建了一个对象,则称该函数拥有这个对象。“拥有一个对象”意味着该实体要负责确保对其所有的对象进行清理。
NSObject 类提供了一个-(id)autorelease;的方法,当给一个对象发送 autorelease 消息时,实际上是将该对象添加到了自动释放池中。当自动释放池被销毁时,会向该池中的所有对象发送 release 消息。例子如下:

1
2
3
4
- (NSString *)description {
NSString *description = [NSString alloc] initWithFormat:@"I am %d years old",4]];
return [description autorelease];
}

内存管理规则:

  1. 使用 new 、 alloc 和 copy 方法创建一个对象时,该对象的引用计数值为 1,当不再使用该对象时,你应该想该对象发送一条 release 或 autorelease 消息。这样对象将在使用寿命结束时被销毁。
  2. 当使用其他方法获得一个对象时,则假设该对象的保留计数器值为 1,而且已经被设置为自动释放了。
  3. 如果你保留了某个对象,就需要释放或者自动释放该对象,必须保持 retain 方法和 release 方法的使用次数相等。

自动释放池的释放时间是完全确定的:要么是在代码中自己手动销毁,要么是使用 AppKit 时在时间循环结束时销毁。自动释放池以栈的形式实现:当你创建了一个新的自动释放池时,它就被添加到栈顶。接收 autorelease 消息的对象将被放入最顶端的自动释放池中。
Objective-C 的垃圾回收器是新型的垃圾回收器,它定期检查变量和对象并且跟踪它们之间的指针,当发现没有任何变量指向某个对象时,就将该对象视为应该丢弃的垃圾。与自动释放池一样,垃圾回收也是在时间循环结束时触发的。

ARC

iOS 无法使用垃圾回收,垃圾回收期在运行时工作,通过返回的代码来定期检查对象。ARC 是在编译时进行工作的。它在代码中插入了合适的 retain 和 release 语句。
ARC 只对可保留的对象指针有效,主要有三种:

  • 代码块指针
  • Objective-C 对象指针
  • 通过 _attribute((NSObject)) 类型定义的指针

声明变量时使用 __weak 关键字或对属性使用 weak 特性的归零弱引用会在指向的对象释放之后,将这些弱引用设置为零(nil)。
使用 ARC 时要注意:

  • 属性名称不能以 new 开头。
  • @property 声明的对象其内存管理特性默认为 assign。

拥有者权限

ARC 中的可保留对象指针可以与非可保留对象指针通过桥接转换的 C 语言技术来进行转换并对其指针的所有权进行管理。

  • __bridge type 操作符:可以使 void *id 对象指针相互转换,这个类型转换会传递指针但是不会传递它的所有权。
1
2
3
NSString *nsString = @"aString";
CFStringRef cfString = (__bridge CFStringRef)nsString;
//cfString接收了指针,但指针的所有权仍然由 nsString 保留,使用完 cfString 变量之后不需要调用 CFRelease 函数去释放它。
  • __bridge_retained CF type 操作符:这个类型转换会使要转换赋值的变量也持有所赋值的对象。会给对象的保留计数器加 1。__bridge_retained 转换与 retain 类似。
1
2
3
4
NSString *nsString = @"aString";
CFStringRef cfString = (__bridge_retained CFStringRef)nsString;
CFRelease(cfString);
//cfString 和 nsString 同时持有对象。使用完后 nsString 由 ARC 负责释放,但是 cfString 需要你调用 CFRelease() 函数释放。
  • __bridge_transfer Objective-C type 操作符,与上一个执行相反的操作,被转换的变量所持有的对象在该变量被赋值给转换目标后随之释放。__bridge_transfer 转换与 release 相似。
1
2
3
4
5
const char *cString = "cString";
CFStringRef cfString = CFStringCreateWithCString(NULL, cString, kCFStringEncodingASCII);
NSString *nsString = (__bridge_transfer NSString *)cfString;
NSLog(@"%@",nsString);
//nsString 持有该对象,使用完后 nsString 由 ARC 负责释放, cfString 在转换完成后释放。

在 struct 和 union 中是不能使用保留对象的。可以通过使用 void* 和桥接转换来解决这个问题。

记录一些疑惑:

1
2
3
4
5
6
7
8
9
int main(int argc, const char * argv[])
{
@autoreleasepool {
NSString *ocString = @"aString";
CFStringRef cfString = (__bridge_retained CFStringRef)ocString;
CFShow(cfString);
}
return 0;
}

以上代码按照上面的理解,cfString 和 ocString 都是持有了对象的,但是用 Xcode9 的 analyze 来分析并没有曝出内存泄露问题? 不太能理解,希望看到的大神讲解一下。求教育!

对象初始化

分配对象

向某个类发送 alloc 消息就是从操作系统获得一块内存,并将其指定为存放对象的实例变量的位置。alloc 方法还顺便将这块内存区域全部初始化为 0,如 BOOL 类型变量初始化为 NO, float类型变量初始化为 0.0,指针初始化为 nil。刚分配的对象不能立即使用,需要先初始化,不然会出现奇怪的行为。

初始化

为什么要嵌套调用 alloc 和 init 方法?

1
Car *car = [[Car alloc] init];

而不是这样:

1
2
Car *car = [Car alloc];
[car init];

因为初始化方法返回的对象可能与分配的对象不同。像 NSString 和 NSArray 这样的类事件上是以类簇的方式实现的,所以 init 方法可以检查它的参数,并决定返回另一个类的对象更合适。
我们经常这样写初始化方法:

1
2
3
4
5
6
- (instancetype)init{
if (self = [super init]) {
//自定义初始化行为
}
return self;
}

代码中调用了[super init],其作用是让超类完成自身的初始化工作。由于 self 参数是通过固定的距离来寻找实例变量所在的内存位置的,如果从 init 方法返回一个新对象,则需要更新 self,以便其后的实例变量的引用可以被映射到正确的内存位置。而且这个赋值操作只影响该 init 方法中 self 的值,而不影响该方法范围以外的任何内容。如果在初始化一个对象时出现问题,则 init 方法可能会返回 nil。

指定初始化函数

类中某个初始化函数被指派为初始化函数,该类的所有初始化方法都使用指定初始化函数执行初始化操作,而子类使用其超类的指定初始化函数进行超类的初始化,通常接受参数最多的初始化方法是最终的指定初始化方法。
如果创建了一个指定初始化函数,则一定要在你自己的指定初始化函数中调用超类的指定初始化函数。

属性

  • @property 预编译指令的作用是自动声明属性的 setter 和 getter 方法。
  • @synthesize 预编译指令的作用是实现该属性的访问方法。所有属性都是基于变量的,当在 synthesize getter 和 setter 方法时,编译器会自动创建适当类型的实例变量,并且在属性名前加下划线,作为实例变量的名字。如果你没有声明这些变量,编译器也会声明的。注:Xcode 4.5 之后,可以不必使用 synthesize 了。
  • @dynamic 预编译指令告诉编译器不要自动生成任何代码或创建相应的实例变量。我们可以自己去写实现方法。

实例变量的声明可以放在头文件和实现文件中,区别在于若有一个子类,并且要从子类直接通过属性访问变量,那么变量就必须声明在头文件中。
在使用属性时,同时可以指定其各种特性,如:

1
@property (nonatomic, readwrite, assign) CGRect size;

展示了属性的默认的一些特性,其中比较重要的是这些内存管理语义的:

  • assign “设置方法”只会执行针对 scalar type 的简单复制操作,如: CGFloat, NSInter
  • strong 定义了一种”拥有关系“,设置方法会先保留新值,并释放旧值,然后将新值设置上去。
  • weak 定义了一种”非拥有关系,设置方法与 assign 类似,但是在属性所指的对象释放时,属性值也会被设置为 nil。
  • unsafe_unretained 语义与 assign 相同,但它适用于 object type ,表示“非拥有关系”,而且在目标对象释放时,属性值也不会被设置为 nil,所以是 unsafe 的。
  • copy 所属关系与 strong 类似,然而设置方法并不保留新值,而是拷贝它。通常用于 NSString, NSArray, NSDictionaty 及其子类。当源字符串是 NSString 时, copy 操作只是做了次浅拷贝,当源字符串是 NSMutableString 时, copy 操作是深拷贝,属性值指向拷贝生成的新对象。

在对象之外访问实例变量时,总是应该通过属性来做,然而在对象内部既可以使用“点语法”通过存取方法来访问实例变量,也可以直接访问实例变量。这两种方法有以下区别:

  1. 直接访问实例变量不经过 Objective-C 的方法派发,因此速度比较快。
  2. 直接访问实例变量,不会调用其“设置方法”,因此绕过了相关属性所定义的“内存管理语义”。
  3. 直接访问实例变量,不会触发“键值观察”。

因此在对象内部写入实例变量时,应该通过其“设置方法”来做,而在读取实例变量时,直接访问它。例外情况是在初始化方法及 dealloc 方法中应该总是直接访问实例变量。因为子类可能会 override 设置方法。这时在基类中通过设置方法来访问实例变量时将会调用子类的设置方法。(但是若使用了惰性初始化技术,则必须通过存取方法来访问属性)。

类别(Category)

利用 Objective-C 的动态运行时分配机制,可以为现有的类添加新方法。可以在类别中添加属性(必须是 @dynamic 类型的),但是不能添加实例变量,类别没有空间容纳实例变量,添加属性的好处在于可以通过点语法调用 setter 和 getter 方法。
使用类别时要注意避免命名冲突,当发生命名冲突时,类别具有更高的优先级,类别方法将完全取代初始方法。
类别主要有三个用途:

  1. 将类的实现代码分散到多个不同的文件或框架中(使用分类中方法时要引入分类的头文件。有时编写程序库时,将分类的头文件不随程序库一起公开,从而使用者就不知道库里还有这些私有方法)。
  2. 创建对私有方法的前向引用(在类别中声明该私有方法,然后将该类别置于实现文件的最前端,编译器就知道该方法已经存在,不会发出警告了。主要用于不方便在类的 @interface 部分列出方法或者使用的是尚未发布的私有方法。)。
  3. 向对象添加非正式协议,用于实现委托(创建一个 NSObject 的类别,然后在你的类中实现想要实现的方法。这也意味着只要对象实现了委托方法,任何类的对象都可以成为委托对象。)

类扩展是唯一能声明实例变量的分类,也可以改变属性的读写权限等,类扩展必须定义在其所接续的那个类的实现文件里,而且它没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。与其他分类不同,它没有名字。
为什么能在类扩展中定义方法和实例变量呢?因为有“稳固的 ABI 机制”,使得我们无需知道对象大小即可使用它,由于类的使用者无需知道实例变量的内存布局,所以他们就不必须定义在公共接口中了。
实例变量也可以定义在“实现块”里,如下所示:

1
2
3
@implementation EOCPerson {
int _anInstansceVariable;
}

从语法上来说,这与直接添加到类扩展中等效。

协议

Objective-C 不支持多重继承,但是我们可以通过协议这种方式描述接口,让类遵循协议,然后实现协议中的方法来扩展类的功能。协议最常见的用途是实现委托模式,不过也有其他用法。

委托模式

“委托模式”是一种实现对象间通信的编程设计模式,该模式的主旨是:定义一套接口,某对象若想要接受另一个对象的委托,则需遵从此接口,以便称为其“委托对象”(delegate)。而“另一个对象“则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。
有了协议之后,类就可以用一个属性来存放其委托对象了:

1
@property (nonatomic, weak) id<XXXDelegate> delegate;

需要注意的是这个属性一般都定义为 weak, 因为通常情况下扮演 delegate 的那个对象也要持有本对象,因此为了避免 retain cycle,存放委托对象的那个属性就得定义为weak 或者 unsafe_unretained。
在调用 delegate 对象的方法时,总是应该把发起委托的实例也一并传入方法中(通过协议方法的声明),这样, delegate 对象在实现相关方法时,就能根据传入的实例分别执行不同的代码了。
有时候需要优化委托对象是否能响应某个协议方法时(调用if([delegate respondsToSelector:@selector(xxx)])),可以将此信息缓存在某个结构体实例变量中。

匿名对象

有时候对象类型并不重要,重要的是对象有没有实现某些方法,在这种情况下可以用”匿名对象“来表达这一概念。如:id<XXXDelegate>,不需要知道此对象所属的类型,只有遵循 XXXDelegate 协议就好了。

数据持久化

数据持久化就是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称。数据模型可以是任何数据结构或对象模型,存储模型可以是关系模型、XML、二进制流等。iOS 开发中常用的数据持久化技术有:plist 文件,NSKeyedArchiver,SQLite3,NSUserDefaults,CoreData 等。

plist 文件

plist 文件可以存储 NSArray, NSDictionary, NSString, NSNumber, NSData, NSDate类及其可变类的对象。一般有两种方式进行读写操作:

  • NSArray, NSDictionary, NSData及其子类可以直接调用writeToFile:atomically:方法将对象写入 plist 文件。
1
2
3
4
5
6
7
8
//写入
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"test.plist"];
NSArray *array = @[@1,@"string"];
[array writeToFile:filePath atomically:YES];
//... 读取
NSArray *otherArray = [NSArray arrayWithContentsOfFile:filePath];
NSLog(@"%@",otherArray);
  • NSPropertyListSerialization 类可以为存储和加载 plist 的行为提供很多设定项(比如修改数据的为可变类型的),它可以将 plist 的数据内容以二进制的形式写入文件,因此其提供的其实是 NSArray 和 NSDictionary 与 NSData 之间的转换功能。
1
2
3
4
5
6
7
8
9
//写入
NSDictionary *dic =@{@"one":@"1",@"two":@2};
NSError *error;
NSData *serializedData = [NSPropertyListSerialization dataWithPropertyList:dic format:NSPropertyListBinaryFormat_v1_0 options:0 error:&error];
if (serializedData) {
[serializedData writeToFile:filePath atomically:YES];
}
//...读取
NSMutableDictionary *otherDic = [NSPropertyListSerialization propertyListWithData:serializedData options:NSPropertyListMutableContainersAndLeaves format:NULL error:&error];

NSKeyedArchiver 和 NSKeyedUnarchiver

遵循 NSCoding 协议并实现了其方法的对象都可以将它的实例变量和其他数据编码为数据块,然后保存在磁盘中,需要的时候再读会内存中创建新对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ZAThing : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger number;
@property (nonatomic, strong) NSMutableArray *subThings;
@end

@implementation ZAThing
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
_name = [aDecoder decodeObjectForKey:@"name"];
_number = [aDecoder decodeIntegerForKey:@"number"];
_subThings = [aDecoder decodeObjectForKey:@"subThings"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.number forKey:@"number"];
[aCoder encodeObject:self.subThings forKey:@"subThings"];
}
@end
1
2
3
4
5
6
7
ZAThing *thing = [[ZAThing alloc] init];
thing.name = @"111";
thing.number = 222;
NSArray *things = @[@1,@2];
thing.subThings = [things mutableCopy];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:thing];
ZAThing *otherThing = [NSKeyedUnarchiver unarchiveObjectWithData:data];

值得注意的是,在对象中还有嵌套的对象时,如上面的 subThings,在归档和反归档时会递归调用嵌套对象的 encode 和 decode 方法。(注:如果 subThings 中包含 thing 对象,这样循环包含的话,Cocoa 的归档和反归档也可以对其进行处理,但是不要用试图 NSLog 来打印, NSLog 不够智能不能处理这种情况)。

How is SDWebImage better than X?

开篇,我先翻译一下 SDWebImage 官方介绍中的这篇文章:How is SDWebImage better than X?

  • 从 iOS5.0 开始 NSURLCache 会处理磁盘缓存,那么使用 SDWebImage 相比于单纯使用 NSURLRequest 强在哪里呢?

    确实从iOS5.0开始,NSURLCache 会在内存及磁盘中缓存 HTTP 响应的原始数据。但是每次缓存命中,应用都需要数据解析(HTTP 原始数据是编码的)、内存拷贝等大量的操作将原生的缓存数据转换进UIImage对象中。
    另一方面,SDWebImage 在内存中缓存了 UIImage 对象,并在磁盘中存储了图片文件的原始压缩数据(已解码)。UIImage 对象使用 NSCache 按原样存储在内存中,所以在使用时并不需要拷贝,而且可以在应用或系统需要时随时释放内存。
    除此之外,SDWebImageDecoder 将 UIImageView 首次使用 UIImage 对象的解压缩工作放在了后台线程中而不是通常的主线程中,可以减少主线程堵塞。
    最后,SDWebImage 完全绕过了复杂且易出错的 HTTP 缓存控制配置,从而极大的加速了缓存查找。

  • 既然 AFNetworking 为 UIImageView 提供了相似的功能,SDWebImage 还有用嘛?

    大概没啥用,AFNetworking 也是利用了基于 Foundation 框架的 URL 加载系统缓存:NSURLCache,还为 UIImageView 和 UIButton 提供了默认使用 NSCached 的可配置内存缓存。缓存行为可以根据相应的 NSURLRequest 来配置。AFNetworking 还提供了图片数据的后台解压缩等 SDWebImage 的特性。
    因此如果你已经使用了 AFNetworking,而且只想要简单的异步图片加载分类,内置的 UIKIT 框架也够用了。

问题

月初收到七牛云的邮件扫了一眼大概是关于测试域名回收的问题,由于我只是用到了七牛云存储来做博客图床也就没在意。今天一上博客发现有些图片已经刷不出来了,请求图片的链接返回:

1
{error: "no such domain"}

这不就很蛋疼了嘛,上后台一看,果然是测试域名的问题:测试域名使用规范,图片上传后生成域名以 clouddn.com 结尾的 URL 在域名回收后自然不能访问了。

官方给出的解决途径是绑定自定义域名,然而这个域名是需要在公安网备案的,对我来说为使用图床还得搞个备案的域名未免太麻烦,索性先不用七牛云的图床了。当我尝试点击下载文件时:

yun

网页的顶端给了我一个错误提示:

bucket_error

这就很尴尬了,域名被回收了直接下载都下载不了,怎么破呢?只好祭出 google 大法。

解决办法

搜索后发现原来七牛云提供了命令行辅助工具qrsctl来对存储资源进行操作,下载下来通过命令chmod +x qrsctl为文件添加可执行权限。然后使用如下命令进行操作

1
2
3
4
5
6
#登录
./qrsctl login <User> <Passwd>
#登录成功后查看所有存放资源的空间(buckets)
./qrsctl buckets
#列出bucket中以prefix开头的所有资源
./qrsctl listprefix <bucket> <prefix>

因为我们要获取所有的图片资源,所以prefix参数使用了''空字符串,所得结果如下图所示:

terminal_buckets

在获取到所有资源名称后,就可以调用qrsctl下载资源的接口了:

1
2
#key为资源文件的名称 destFile为下载的目标路径
./qrsctl get <Bucket> <Key> <DestFile>

使用下面的脚本来完成下载任务更加方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
#``反引号是命令替换,可以先执行其中的命令并暂时保存在变量 imgs 中,``的替换操作也可使用$()操作
imgs=`./qrsctl listprefix 176zane ''`
i=0
#打印$imgs执行结果 然后使用管道符合 | 将结果传给 tr 命令将空格转化为换行符,再将结果逐行读取到 line 变量
echo $imgs | tr " " "\n" | while read line
do
#(())用于算数运算比较,此处的判断用于将上面结果中第一行的 ‘marker:‘这个非资源名的信息打印过滤掉
if(($i>0))
then
echo $line
./qrsctl get 176zane $line ./$line
fi
i=$(($i+1))
done

虽然对shell脚本不是太熟悉,但是上面的逻辑还是挺简单的,尝试一下很简单就把图片资源下载下来了,但是检查了一下就发现并没有下载完所有的资源,其实这里面还有一个坑:tr " " "\n"命令将之前处理结果分成一行一行的字符串时也将文件名中带有空格的如上图中用绿框框起来的文件名给破坏了,在后台中删除了该文件命名中的空格后重新运行脚本就搞定了。当然这是简单的做法,当你有很多资源名中含有空格时,可以在获取资源名后,在后续的命令中对文件名加上引号来下载:

1
./qrsctl get 176zane '2018-07-12 10_40_14.gif' ./'2018-07-12 10_40_14.gif'

这样就拿回了所有保存在七牛云上的图片资源,鉴于暂时没找到合适的图床,索性就直接用 github 来保存吧,虽然加载速度会慢点,但还是比较可靠的。全局替换图床链接http://oztca4xvs.bkt.clouddn.com/为本地地址/images/后,将图片复制到source/images/目录下,重新部署即可。

黑客与画家的共同之处,在于他们都是创作者。