iOS Swift GCD 开发教程
本教程將帶你詳細了解 GCD 的概念和用法,通過文中的代碼示例和附帶的 Github 示例工程,可以進一步加深對這些概念的體會。附帶的示例工程是一個完整可運行的 App 項目:DispatchQueueTest,項目地址點此處。本教程提供離線版,閱讀體驗更佳: HTML 版 、PDF 版。
GCD 全稱是 Grand Central Dispatch,翻譯過來就是大規(guī)模中央調(diào)度。根據(jù)官方文檔,它的作用是:“通過向系統(tǒng)管理的調(diào)度隊列中提交任務,在多核硬件上同時執(zhí)行代碼。”。它提供了一套機制,讓你可以充分利用硬件的多核性能,并且讓你不用再調(diào)用那些繁瑣的底層線程 API,編寫易于理解和修改的代碼。
1. 隊列和任務的概念
GCD 的核心就是為了解決如何讓程序有序、高效的運行,由此衍生出隊列等概念和一系列的方法。為了弄清楚這些概念,我們先來看看程序執(zhí)行存在哪些問題需要解決。
理解任務
在 GCD 中把程序執(zhí)行時做的事情都當成任務,一段代碼、一個 API 調(diào)用、一個方法、函數(shù)、閉包等,都是任務,一個應用就是由很多任務組成的。任務的執(zhí)行需要時間和相應的順序,耗時有長短,順序有先后,任務只有按照正確的時間和順序進行編排,應用才能按照你的預期運行。我們舉音樂播放的例子來看看關于任務有哪些需求。
以上列舉了 6 個經(jīng)典的任務執(zhí)行需要的特性,在 GCD 中分別提供了以下方法來支持它們:
下面我們先從隊列開始分析。
2. 創(chuàng)建隊列
在系統(tǒng)底層,程序是運行在線程之中的,如果我們直接在線程層面進行操作,我們就需要告訴程序它應該運行在哪個線程、何時開始、何時結束等,這一列的操作都非常繁瑣,而且很容易出錯。為了簡化線程的操作,GCD 封裝了隊列的概念。
可以把隊列想象成辦事窗口,有些類型窗口一次只能受理一個任務,通常只有一個辦事員(線程),所有任務按進入的先后順序來辦理,而且不允許插隊(阻塞線程),這是串行隊列。
有些類型窗口一次可以受理多個任務,多個任務可以同時辦理,通常有多個辦事員(線程),而且同一個任務在辦理過程中允許被插隊(阻塞線程),這是并行隊列。
在后面我們會詳細討論隊列的特性。
創(chuàng)建隊列非常的簡單。
串行隊列
系統(tǒng)為串行隊列一般只分配一個線程(也有特例,下一章任務特性部分有解釋),隊列中如果有任務正在執(zhí)行時,是不允許隊列中的其他任務插隊的(即暫停當前任務,轉而執(zhí)行其他任務),這個特性也可以理解為:串行隊列中執(zhí)行任務的線程不允許被當前隊列中的任務阻塞(此時會死鎖),但可以被別的隊列任務阻塞。
創(chuàng)建時指定 label 便于調(diào)試,一般使用 Bundle Identifier 類似的命名方式:
let queue = DispatchQueue(label: "com.xxx.xxx.queueName") 復制代碼并行隊列
系統(tǒng)會為并行隊列至少分配一個線程,線程允許被任何隊列的任務阻塞。
let queue = DispatchQueue(label: "com.xxx.xxx.queueName", attributes: .concurrent) 復制代碼其實在我們手動創(chuàng)建隊列之前,系統(tǒng)已經(jīng)幫我們創(chuàng)建好了 6 條隊列,1 條系統(tǒng)主隊列(串行),5 條全局并發(fā)隊列(不同優(yōu)先級),它們是我們創(chuàng)建的所有隊列的最終目標隊列(后面會解釋),這 6 個隊列負責所有隊列的線程調(diào)度。
系統(tǒng)主隊列
主隊列是一個串行隊列,它主要處理 UI 相關任務,也可以處理其他類型任務,但為了性能考慮,盡量讓主隊列執(zhí)行 UI 相關或少量不耗時間和資源的操作。它通過類屬性獲取:
let mainQueue = DispatchQueue.main 復制代碼系統(tǒng)全局并發(fā)隊列
全局并發(fā)隊列,存在 5 個不同的 QoS 級別,可以使用默認優(yōu)先級,也可以單獨指定:
let globalQueue = DispatchQueue.global() // qos: .default let globalQueue = DispatchQueue.global(qos: .background) // 后臺運行級別 復制代碼3. 添加隊列任務
有些任務我們必須等待它的執(zhí)行結果才能進行下一步,這種執(zhí)行任務的方式稱為同步,簡稱同步任務;有些任務只要把它放入隊列就可以不管它了,可以繼續(xù)執(zhí)行其他任務,按這種方式執(zhí)行的任務,稱為異步任務。
同步任務
特性:任務一經(jīng)提交就會阻塞當前線程(當前線程可以理解為下方代碼示例中執(zhí)行 sync 方法所在的線程 thread0),并請求隊列立即安排其執(zhí)行,執(zhí)行任務的線程 thread1 默認等于 thread0,即同步任務直接在當前線程運行,任務完成后恢復線程原任務。
任務提交方式如下:
// current thread - thread0 queue.sync {// current thread - thread1 == thread0// do something } 復制代碼我們分別根據(jù)下圖中的 4 種情況舉 4 個例子,來說明同步任務的特性。
看例子前先介紹兩個輔助方法:
1.打印當前線程,使用 Thread.current 屬性:
/// 打印當前線程 func printCurrentThread(with des: String, _ terminator: String = "") {print("\(des) at thread: \(Thread.current), this is \(Thread.isMainThread ? "" : "not ")main thread\(terminator)") } 復制代碼2.測試任務是否在指定隊列中,通過給隊列設置一個標識,使用 DispatchQueue.getSpecific 方法來獲取這個標識,如果能獲取到,說明任務在該隊列中:
/// 隊列類型 enum DispatchTaskType: String {case serialcase concurrentcase maincase global }// 定義隊列 let serialQueue = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue") let concurrentQueue = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.concurrentQueue",attributes: .concurrent) let mainQueue = DispatchQueue.main let globalQueue = DispatchQueue.global()// 定義隊列 key let serialQueueKey = DispatchSpecificKey<String>() let concurrentQueueKey = DispatchSpecificKey<String>() let mainQueueKey = DispatchSpecificKey<String>() let globalQueueKey = DispatchSpecificKey<String>()// 初始化隊列 key init() {serialQueue.setSpecific(key: serialQueueKey, value: DispatchTaskType.serial.rawValue)concurrentQueue.setSpecific(key: concurrentQueueKey, value: DispatchTaskType.concurrent.rawValue)mainQueue.setSpecific(key: mainQueueKey, value: DispatchTaskType.main.rawValue)globalQueue.setSpecific(key: globalQueueKey, value: DispatchTaskType.global.rawValue) }/// 測試任務是否在指定隊列中 func testIsTaskInQueue(_ queueType: DispatchTaskType, key: DispatchSpecificKey<String>) {let value = DispatchQueue.getSpecific(key: key)let opnValue: String? = queueType.rawValueprint("Is task in \(queueType.rawValue) queue: \(value == opnValue)") } 復制代碼下面我們看看這 4 個例子:
代碼示例
本章對應的代碼見示例工程中 QueueTestListTableViewController+createQueueWithTask.swift, CreateQueueWithTask.swift.
示例 3.1:串行隊列中新增同步任務
/// 串行隊列中新增同步任務 func testSyncTaskInSerialQueue() {self.printCurrentThread(with: "start test")serialQueue.sync {print("\nserialQueue sync task--->")self.printCurrentThread(with: "serialQueue sync task")self.testIsTaskInQueue(.serial, key: serialQueueKey)print("--->serialQueue sync task\n")}self.printCurrentThread(with: "end test") } 復制代碼執(zhí)行結果,任務是在主線程中執(zhí)行的,結束后又回到了主線程,可以理解為這個同步任務把主線程阻塞了,讓自己優(yōu)先插隊執(zhí)行:
start test at thread: <NSThread: 0x1c4260900>{number = 1, name = main}, this is main thread
serialQueue sync task--->
serialQueue sync task at thread: <NSThread: 0x1c4260900>{number = 1, name = main}, this is main thread
Is task in serial queue: true
--->serialQueue sync task
end test at thread: <NSThread: 0x1c4260900>{number = 1, name = main}, this is main thread
示例 3.2 串行隊列任務中嵌套本隊列的同步任務
/// 串行隊列任務中嵌套本隊列的同步任務 func testSyncTaskNestedInSameSerialQueue() {printCurrentThread(with: "start test")serialQueue.async {print("\nserialQueue async task--->")self.printCurrentThread(with: "serialQueue async task")self.testIsTaskInQueue(.serial, key: self.serialQueueKey)self.serialQueue.sync {print("\nserialQueue sync task--->")self.printCurrentThread(with: "serialQueue sync task")self.testIsTaskInQueue(.serial, key: self.serialQueueKey)print("--->serialQueue sync task\n")} // Thread 9: EXC_BREAKPOINT (code=1, subcode=0x101613ba4)print("--->serialQueue async task\n")}printCurrentThread(with: "end test") } 復制代碼執(zhí)行結果,執(zhí)行到嵌套任務時程序就崩潰了,這是死鎖導致的。其中有個有意思的現(xiàn)象,這里串行隊列的第一個任務運行在非主線程上,在異步任務部分會解釋。這里死鎖是由兩個因素導致:串行隊列、同步任務,回顧一下串行隊列的特性就好解釋了:串行隊列中執(zhí)行任務的線程不允許被當前隊列中的任務阻塞。下個例子我們試試:并行隊列 + 同步任務,看看會不會導致死鎖。
start test at thread: <NSThread: 0x1c006db80>{number = 1, name = main}, this is main thread
serialQueue async task--->
end test at thread: <NSThread: 0x1c006db80>{number = 1, name = main}, this is main thread
serialQueue async task at thread: <NSThread: 0x1c4466340>{number = 3, name = (null)}, this is not main thread
Is task in serial queue: true
(lldb)
示例 3.3 并行隊列任務中嵌套本隊列的同步任務
/// 并行隊列任務中嵌套本隊列的同步任務 func testSyncTaskNestedInSameConcurrentQueue() {printCurrentThread(with: "start test")concurrentQueue.async {print("\nconcurrentQueue async task--->")self.printCurrentThread(with: "concurrentQueue async task")self.testIsTaskInQueue(.concurrent, key: self.concurrentQueueKey)self.concurrentQueue.sync {print("\nconcurrentQueue sync task--->")self.printCurrentThread(with: "concurrentQueue sync task")self.testIsTaskInQueue(.concurrent, key: self.concurrentQueueKey)print("--->concurrentQueue sync task\n")}print("--->concurrentQueue async task\n")}printCurrentThread(with: "end test") } 復制代碼執(zhí)行結果,嵌套的同步任務執(zhí)行的非常順利,而且印證了同步任務的另一個特性:同步任務直接在當前線程運行。
start test at thread: <NSThread: 0x1c4263a40>{number = 1, name = main}, this is main thread
concurrentQueue async task--->
end test at thread: <NSThread: 0x1c4263a40>{number = 1, name = main}, this is main thread
concurrentQueue async task at thread: <NSThread: 0x1c426cd80>{number = 3, name = (null)}, this is not main thread
Is task in concurrent queue: true
concurrentQueue sync task--->
concurrentQueue sync task at thread: <NSThread: 0x1c426cd80>{number = 3, name = (null)}, this is not main thread
Is task in concurrent queue: true
--->concurrentQueue sync task
--->concurrentQueue async task
示例 3.4:串行隊列中嵌套其他隊列的同步任務
/// 串行隊列中嵌套其他隊列的同步任務 func testSyncTaskNestedInOtherSerialQueue() {// 創(chuàng)新另一個串行隊列let serialQueue2 = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue2")let serialQueueKey2 = DispatchSpecificKey<String>()serialQueue2.setSpecific(key: serialQueueKey2, value: "serial2")self.printCurrentThread(with: "start test")serialQueue.sync {print("\nserialQueue sync task--->")self.printCurrentThread(with: "nserialQueue sync task")self.testIsTaskInQueue(.serial, key: self.serialQueueKey)serialQueue2.sync {print("\nserialQueue2 sync task--->")self.printCurrentThread(with: "serialQueue2 sync task")self.testIsTaskInQueue(.serial, key: self.serialQueueKey)let value = DispatchQueue.getSpecific(key: serialQueueKey2)let opnValue: String? = "serial2"print("Is task in serialQueue2: \(value == opnValue)")print("--->serialQueue2 sync task\n")}print("--->serialQueue sync task\n")} } 復制代碼執(zhí)行結果,串行隊列嵌套的同步任務執(zhí)行成功了,和前面的例子不一樣啊。是的,因為這里嵌套的是另一個隊列的任務,雖然它們都運行在同一個線程上,一個串行隊列可以對另一個串行隊列視而不見。不同隊列復用線程這是系統(tǒng)級的隊列作出的優(yōu)化,但是在同一個串行隊列內(nèi)部,任務一定都是按順序執(zhí)行的,這是自定義隊列的最本質(zhì)作用。
start test at thread: <NSThread: 0x1c4263a40>{number = 1, name = main}, this is main thread
serialQueue sync task--->
nserialQueue sync task at thread: <NSThread: 0x1c4263a40>{number = 1, name = main}, this is main thread
Is task in serial queue: true
serialQueue2 sync task--->
serialQueue2 sync task at thread: <NSThread: 0x1c4263a40>{number = 1, name = main}, this is main thread
Is task in serial queue: false
Is task in serialQueue2: true
--->serialQueue2 sync task
--->serialQueue sync task
異步任務
特性:任務提交后不會阻塞當前線程,會由隊列安排另一個線程執(zhí)行。
任務提交方式如下:
// current thread - thread0 queue.async {// current thread - thread1 != thread0// do something } 復制代碼我們分別根據(jù)下圖舉 3 個例子,來說明異步任務的特性。
下面我們看看這 3 個例子:
代碼示例
示例3.5:并行隊列中新增異步任務
/// 并行隊列中新增異步任務 func testAsyncTaskInConcurrentQueue() {printCurrentThread(with: "start test")concurrentQueue.async {print("\nconcurrentQueue async task--->")self.printCurrentThread(with: "concurrentQueue async task")self.testIsTaskInQueue(.concurrent, key: self.concurrentQueueKey)print("--->concurrentQueue async task\n")}printCurrentThread(with: "end test") } 復制代碼執(zhí)行結果,執(zhí)行異步任務時新開了一個線程。
start test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread
end test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread
concurrentQueue async task--->
concurrentQueue async task at thread: <NSThread: 0x1c04799c0>{number = 3, name = (null)}, this is not main thread
Is task in concurrent queue: true
--->concurrentQueue async task
示例3.6:串行隊列中新增異步任務
/// 串行隊列中新增異步任務 func testAsyncTaskInSerialQueue() {printCurrentThread(with: "start test")serialQueue.async {print("\nserialQueue async task--->")self.printCurrentThread(with: "serialQueue async task")self.testIsTaskInQueue(.serial, key: self.serialQueueKey)print("--->serialQueue async task\n")}printCurrentThread(with: "end test") } 復制代碼執(zhí)行結果,同樣新開了一個線程。
start test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread
end test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread
serialQueue async task--->
serialQueue async task at thread: <NSThread: 0x1c4473740>{number = 4, name = (null)}, this is not main thread
Is task in serial queue: true
--->serialQueue async task
示例3.7:串行隊列任務中嵌套本隊列的異步任務
/// 串行隊列任務中嵌套本隊列的異步任務 func testAsyncTaskNestedInSameSerialQueue() {printCurrentThread(with: "start test")serialQueue.sync {print("\nserialQueue sync task--->")self.printCurrentThread(with: "serialQueue sync task")self.testIsTaskInQueue(.serial, key: self.serialQueueKey)self.serialQueue.async {print("\nserialQueue async task--->")self.printCurrentThread(with: "serialQueue async task")self.testIsTaskInQueue(.serial, key: self.serialQueueKey)print("--->serialQueue async task\n")}print("--->serialQueue sync task\n")}printCurrentThread(with: "end test") } 復制代碼執(zhí)行結果,這個例子再一次刷新了對串行隊列的認識:串行隊列并不是只能運行一個線程。第一層的同步任務運行在主線程上,第二層的異步任務運行在其他線程上,但它們在時間片上是分開的。這里再嚴格定義一下:串行隊列同一時間只會運行一個線程,只有碰到異步任務時,才會使用不同于當前的線程,但都是按時間順序執(zhí)行,只有前一個任務完成了,才會執(zhí)行下一個任務。
start test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread
serialQueue sync task--->
serialQueue sync task at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread
Is task in serial queue: true
--->serialQueue sync task
serialQueue async task--->
end test at thread: <NSThread: 0x1c0078080>{number = 1, name = main}, this is main thread
serialQueue async task at thread: <NSThread: 0x1c4473a40>{number = 5, name = (null)}, this is not main thread
Is task in serial queue: true
--->serialQueue async task
這里我們總結一下隊列和任務的特性:
- 串行隊列同一時間只會使用同一線程、運行同一任務,并嚴格按照任務順序執(zhí)行。
- 并行隊列同一時間可以使用多個線程、運行多個任務,執(zhí)行順序不分先后。
- 同步任務會阻塞當前線程,并在當前線程執(zhí)行。
- 異步任務不會阻塞當前線程,并在與當前線程不同的線程執(zhí)行。
- 如何避免死鎖:不要在串行或主隊列中嵌套執(zhí)行同步任務。
下面介紹兩個特殊的任務類型:柵欄任務、迭代任務。
柵欄任務
柵欄任務的主要特性是可以對隊列中的任務進行阻隔,執(zhí)行柵欄任務時,它會先等待隊列中已有的任務全部執(zhí)行完成,然后它再執(zhí)行,在它之后加入的任務也必須等柵欄任務執(zhí)行完后才能執(zhí)行。
這個特性更適合并行隊列,而且對柵欄任務使用同步或異步方法效果都相同。
- 創(chuàng)建方式,先創(chuàng)建 WorkItem,標記為:barrier,再添加至隊列中:
下面看看柵欄任務的例子:
代碼示例
示例3.8:并行隊列中執(zhí)行柵欄任務
/// 柵欄任務 func barrierTask() {let queue = concurrentQueuelet barrierTask = DispatchWorkItem(flags: .barrier) {print("\nbarrierTask--->")self.printCurrentThread(with: "barrierTask")print("--->barrierTask\n")}printCurrentThread(with: "start test")queue.async {print("\nasync task1--->")self.printCurrentThread(with: "async task1")print("--->async task1\n")}queue.async {print("\nasync task2--->")self.printCurrentThread(with: "async task2")print("--->async task2\n")}queue.async {print("\nasync task3--->")self.printCurrentThread(with: "async task3")print("--->async task3\n")}queue.async(execute: barrierTask) // 柵欄任務queue.async {print("\nasync task4--->")self.printCurrentThread(with: "async task4")print("--->async task4\n")}queue.async {print("\nasync task5--->")self.printCurrentThread(with: "async task5")print("--->async task5\n")}queue.async {print("\nasync task6--->")self.printCurrentThread(with: "async task6")print("--->async task6\n")}printCurrentThread(with: "end test") } 復制代碼執(zhí)行結果,任務 1、2、3 都在柵欄任務前同時執(zhí)行,任務 4、5、6 都在柵欄任務后同時執(zhí)行:
start test at thread: <NSThread: 0x1c407e7c0>{number = 1, name = main}, this is main thread
end test at thread: <NSThread: 0x1c407e7c0>{number = 1, name = main}, this is main thread
async task1--->
async task2--->
async task2 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task2
async task3--->
async task3 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task3
async task1 at thread: <NSThread: 0x1c066c600>{number = 3, name = (null)}, this is not main thread
--->async task1
barrierTask--->
barrierTask at thread: <NSThread: 0x1c066c600>{number = 3, name = (null)}, this is not main thread
--->barrierTask
async task5--->
async task5 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task5
async task6--->
async task6 at thread: <NSThread: 0x1c066c480>{number = 4, name = (null)}, this is not main thread
--->async task6
async task4--->
async task4 at thread: <NSThread: 0x1c066c600>{number = 3, name = (null)}, this is not main thread
--->async task4
迭代任務
并行隊列利用多個線程執(zhí)行任務,可以提高程序執(zhí)行的效率。而迭代任務可以更高效地利用多核性能,它可以利用 CPU 當前所有可用線程進行計算(任務小也可能只用一個線程)。如果一個任務可以分解為多個相似但獨立的子任務,那么迭代任務是提高性能最適合的選擇。
使用 concurrentPerform 方法執(zhí)行迭代任務,迭代任務的后續(xù)任務需要等待它執(zhí)行完成才會繼續(xù)。本方法類似于 Objc 中的 dispatch_apply 方法,創(chuàng)建方式如下:
DispatchQueue.concurrentPerform(iterations: 10) {(index) -> Void in // 10 為迭代次數(shù),可修改。// do something } 復制代碼迭代任務可以單獨執(zhí)行,也可以放在指定的隊列中:
let queue = DispatchQueue.global() // 全局并發(fā)隊列 queue.async {DispatchQueue.concurrentPerform(iterations: 100) {(index) -> Void in// do something}//可以轉至主線程執(zhí)行其他任務DispatchQueue.main.async {// do something} } 復制代碼下面看看迭代任務的例子:
代碼示例
示例3.9:迭代任務
本示例查找 1-10000 之間能被 13 整除的整數(shù),我們直接使用 10000 次迭代對每個數(shù)進行判斷,符合的通過異步方法寫入到結果數(shù)組中:
/// 迭代任務 func concurrentPerformTask() {printCurrentThread(with: "start test")/// 判斷一個數(shù)是否能被另一個數(shù)整除func isDividedExactlyBy(_ divisor: Int, with number: Int) -> Bool {return number % divisor == 0}let array = Array(1...100)var result: [Int] = []globalQueue.async {//通過concurrentPerform,循環(huán)變量數(shù)組print("concurrentPerform task start--->")DispatchQueue.concurrentPerform(iterations: 100) {(index) -> Void inif isDividedExactlyBy(13, with: array[index]) {self.printCurrentThread(with: "find a match: \(array[index])")self.mainQueue.async {result.append(array[index])}}}print("--->concurrentPerform task over")//執(zhí)行完畢,主線程更新結果。DispatchQueue.main.sync {print("back to main thread")print("result: find \(result.count) number - \(result)")}}printCurrentThread(with: "end test") } 復制代碼iPhone 7 Plus 執(zhí)行結果,使用了 2 個線程,iPhone 8 的 CPU 有 6 個核心,據(jù)說可以同時開啟,手頭有的可以試一下:
start test at thread: <NSThread: 0x1c4076900>{number = 1, name = main}, this is main thread
end test at thread: <NSThread: 0x1c4076900>{number = 1, name = main}, this is main thread
concurrentPerform task start--->
find a match: 13 at thread: <NSThread: 0x1c0469bc0>{number = 3, name = (null)}, this is not main thread
find a match: 39 at thread: <NSThread: 0x1c0469bc0>{number = 3, name = (null)}, this is not main thread
find a match: 52 at thread: <NSThread: 0x1c0469bc0>{number = 3, name = (null)}, this is not main thread
find a match: 65 at thread: <NSThread: 0x1c0469bc0>{number = 3, name = (null)}, this is not main thread
find a match: 26 at thread: <NSThread: 0x1c0469cc0>{number = 4, name = (null)}, this is not main thread
find a match: 91 at thread: <NSThread: 0x1c0469cc0>{number = 4, name = (null)}, this is not main thread
find a match: 78 at thread: <NSThread: 0x1c0469bc0>{number = 3, name = (null)}, this is not main thread
--->concurrentPerform task over
back to main thread
result: find 7 number - [13, 39, 52, 65, 26, 91, 78]
Mac 上使用 Xcode 模擬器執(zhí)行結果,使用了 4 個線程:
start test at thread: <NSThread: 0x604000070c40>{number = 1, name = main}, this is main thread
concurrentPerform task start--->
end test at thread: <NSThread: 0x604000070c40>{number = 1, name = main}, this is main thread
find a match: 26 at thread: <NSThread: 0x60400047b800>{number = 3, name = (null)}, this is not main thread
find a match: 13 at thread: <NSThread: 0x60000046ec80>{number = 4, name = (null)}, this is not main thread
find a match: 65 at thread: <NSThread: 0x60400047b800>{number = 3, name = (null)}, this is not main thread
find a match: 91 at thread: <NSThread: 0x60400047b800>{number = 3, name = (null)}, this is not main thread
find a match: 78 at thread: <NSThread: 0x60000046ec80>{number = 4, name = (null)}, this is not main thread
find a match: 39 at thread: <NSThread: 0x60000046ed80>{number = 5, name = (null)}, this is not main thread
find a match: 52 at thread: <NSThread: 0x604000475140>{number = 6, name = (null)}, this is not main thread
--->concurrentPerform task over
back to main thread
result: find 7 number - [26, 13, 65, 91, 78, 39, 52]
4. 隊列詳細屬性
下面介紹一下在創(chuàng)建隊列時,可以設置的一些更豐富的屬性。創(chuàng)建隊列的完整方法如下:
convenience init(label: String, qos: DispatchQoS = default, attributes: DispatchQueue.Attributes = default, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = default, target: DispatchQueue? = default) 復制代碼QoS
隊列在執(zhí)行上是有優(yōu)先級的,更高的優(yōu)先級可以享受更多的計算資源,從高到低包含以下幾個等級:
- userInteractive
- userInitiated
- default
- utility
- background
Attributes
包含兩個屬性:
- concurrent:標識隊列為并行隊列
- initiallyInactive:標識運行隊列中的任務需要動手觸發(fā)(未添加此標識時,向隊列中添加任務會自動運行),觸發(fā)時通過 queue.activate() 方法。
AutoreleaseFrequency
這個屬性表示 autorelease pool 的自動釋放頻率, autorelease pool 管理著任務對象的內(nèi)存周期。
包含三個屬性:
- inherit:繼承目標隊列的該屬性
- workItem:跟隨每個任務的執(zhí)行周期進行自動創(chuàng)建和釋放
- never:不會自動創(chuàng)建 autorelease pool,需要手動管理。
一般任務采用 .workItem 屬性就夠了,特殊任務如在任務內(nèi)部大量重復創(chuàng)建對象的操作可選擇 .never 屬性手動創(chuàng)建 autorelease pool。
Target
這個屬性設置的是一個隊列的目標隊列,即實際將該隊列的任務放入指定隊列中運行。目標隊列最終約束了隊列優(yōu)先級等屬性。
在程序中手動創(chuàng)建的隊列,其實最后都指向系統(tǒng)自帶的 主隊列 或 全局并發(fā)隊列。
你也許會問,為什么不直接將任務添加至系統(tǒng)隊列中,而是自定義隊列,因為這樣的好處是可以將任務進行分組管理。如單獨阻塞隊列中的任務,而不是阻塞系統(tǒng)隊列中的全部任務。如果阻塞了目標隊列,所有指向它的原隊列也將被阻塞。
在 Swift 3 及之后,對目標隊列的設置進行了約束,只有兩種情況可以顯式地設置目標隊列(原因參考):
- 初始化方法中,指定目標隊列。
- 初始化方法中,attributes 設定為 initiallyInactive,然后在隊列執(zhí)行 activate() 之前可以指定目標隊列。
在其他地方都不能再改變目標隊列。
關于目標隊列的詳細闡述,可以參考這篇文章:GCD Target Queues。
5. 延遲加入隊列
有時候你并不需要立即將任務加入隊列中運行,而是需要等待一段時間后再進入隊列中,這時候可以使用 asyncAfter 方法。
例如,我們封裝一個方法指定延遲加入隊列的時間:
class AsyncAfter {/// 延遲執(zhí)行閉包static func dispatch_later(_ time: TimeInterval, block: @escaping ()->()) {let t = DispatchTime.now() + timeDispatchQueue.main.asyncAfter(deadline: t, execute: block)} }AsyncAfter.dispatch_later(2) {print("打個電話 at: \(Date())") // 將在 2 秒后執(zhí)行 } 復制代碼這里要注意延遲的時間是加入隊列的時間,而不是開始執(zhí)行任務的時間。
下面我們構造一個復雜一點的例子,我們封裝一個方法,可以延遲執(zhí)行任務,在計時結束前還可以取消任務或者將原任務替換為一個新任務。主要的思路是,將延遲后實際執(zhí)行的任務代碼進行替換,替換為空閉包則相當于取消了任務,或者替換為你想執(zhí)行的其他任務:
class AsyncAfter {typealias ExchangableTask = (_ newDelayTime: TimeInterval?,_ anotherTask:@escaping (() -> ())) -> Void/// 延遲執(zhí)行一個任務,并支持在實際執(zhí)行前替換為新的任務,并設定新的延遲時間。////// - Parameters:/// - time: 延遲時間/// - yourTask: 要執(zhí)行的任務/// - Returns: 可替換原任務的閉包static func delay(_ time: TimeInterval, yourTask: @escaping ()->()) -> ExchangableTask {var exchangingTask: (() -> ())? // 備用替代任務var newDelayTime: TimeInterval? // 新的延遲時間let finalClosure = { () -> Void inif exchangingTask == nil {DispatchQueue.main.async(execute: yourTask)} else {if newDelayTime == nil {DispatchQueue.main.async {print("任務已更改,現(xiàn)在是:\(Date())")exchangingTask!()}}print("原任務取消了,現(xiàn)在是:\(Date())")}}dispatch_later(time) { finalClosure() }let exchangableTask: ExchangableTask ={ delayTime, anotherTask inexchangingTask = anotherTasknewDelayTime = delayTimeif delayTime != nil {self.dispatch_later(delayTime!) {anotherTask()print("任務已更改,現(xiàn)在是:\(Date())")}}}return exchangableTask} } 復制代碼簡單說明一下:
delay 方法接收兩個參數(shù),并返回一個閉包:
- TimeInterval:延遲時間
- @escaping () -> (): 要延遲執(zhí)行的任務
- 返回:可替換原任務的閉包,我們?nèi)チ艘粋€別名:ExchangableTask
ExchangableTask 類型定義的閉包,接收一個新的延遲時間,和一個新的任務。
如果不執(zhí)行返回的閉包,則在delay 方法內(nèi)部,通過 dispatch_later 方法會繼續(xù)執(zhí)行原任務。
如果執(zhí)行了返回的 ExchangableTask 閉包,則會選擇執(zhí)行新的任務。
代碼示例
本章對應的代碼見示例工程中 QueueTestListTableViewController+AsyncAfter.swift, AsyncAfter.swift.
示例 5.1:延遲執(zhí)行任務,在計時結束前取消。
extension QueueTestListTableViewController {/// 延遲任務,在執(zhí)行前臨時取消任務。 func ayncAfterCancelButtonTapped(_ sender: Any) {print("現(xiàn)在是:\(Date())")let task = AsyncAfter.delay(2) {print("打個電話 at: \(Date())")}// 立即取消任務task(0) {}} } 復制代碼根據(jù)我們封裝的方法,只要提供一個空的閉包 {} 來替換原任務即相當于取消任務,同時還可以指定取消的時間,task(0) {} 表示立即取消,task(nil) {} 表示按原計劃時間取消。
執(zhí)行結果,可以看到任務立即就被替換了,但延遲 2 秒的任務還在,只是變成了一個空任務:
現(xiàn)在是:2018-03-14 01:38:20 +0000
任務已更改,現(xiàn)在是:2018-03-14 01:38:20 +0000
原任務取消了,現(xiàn)在是:2018-03-14 01:38:22 +0000
示例 5.2:延遲執(zhí)行任務,在執(zhí)行前臨時替換為新的任務。
extension QueueTestListTableViewController { func ayncAfterNewTaskButtonTapped(_ sender: Any) {print("現(xiàn)在是:\(Date())")let task = AsyncAfter.delay(2) {print("打個電話 at: \(Date())")}// 3 秒后改為執(zhí)行一個新任務task(3) {print("吃了個披薩,現(xiàn)在是:\(Date())")}} } 復制代碼執(zhí)行結果,可以看到 3 秒后執(zhí)行了新的任務:
現(xiàn)在是:2018-03-14 03:14:08 +0000
原任務取消了,現(xiàn)在是:2018-03-14 03:14:10 +0000
吃了個披薩,現(xiàn)在是:2018-03-14 03:14:11 +0000
任務已更改,現(xiàn)在是:2018-03-14 03:14:11 +0000
6. 掛起和喚醒隊列
GCD 提供了一套機制,可以掛起隊列中尚未執(zhí)行的任務,已經(jīng)在執(zhí)行的任務會繼續(xù)執(zhí)行完,后續(xù)還可以手動再喚醒隊列。
這兩個方法是屬于 DispatchObject 對象的方法,而這個對象是 DispatchQueue、DispatchGroup、DispatchSource、DispatchIO、DispatchSemaphore 這幾個類的父類,但這兩個方法只有 DispatchQueue、DispatchSource 支持,調(diào)用時需注意。
掛起使用 suspend(),喚醒使用 resume()。對于隊列,這兩個方法調(diào)用時需配對,因為可以多次掛起,調(diào)用喚醒的次數(shù)應等于掛起的次數(shù)才能生效,喚醒的次數(shù)更多則會報錯,所以使用時最好設置一個計數(shù)器,或者封裝一個掛起、喚醒的方法,在方法內(nèi)部進行檢查。
而對于 DispatchSource 則有所不同,它必須先調(diào)用 resume() 才能接收消息,所以此時喚醒的數(shù)量等于掛起的數(shù)量加一。
下面通過例子看看實現(xiàn):
/// 掛起、喚醒測試類 class SuspendAndResum {let createQueueWithTask = CreateQueueWithTask()var concurrentQueue: DispatchQueue {return createQueueWithTask.concurrentQueue}var suspendCount = 0 // 隊列掛起的次數(shù)// MARK: ---------隊列方法------------/// 掛起測試func suspendQueue() {createQueueWithTask.printCurrentThread(with: "start test")concurrentQueue.async {self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task1")}concurrentQueue.async {self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task2")}// 通過柵欄掛起任務let barrierTask = DispatchWorkItem(flags: .barrier) {self.safeSuspend(self.concurrentQueue)}concurrentQueue.async(execute: barrierTask)concurrentQueue.async {self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task3")}concurrentQueue.async {self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task4")}concurrentQueue.async {self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task5")}createQueueWithTask.printCurrentThread(with: "end test")}/// 喚醒測試func resumeQueue() {self.safeResume(self.concurrentQueue)}/// 安全的掛失操作func safeSuspend(_ queue: DispatchQueue) {suspendCount += 1queue.suspend()print("任務掛起了")}/// 安全的喚醒操作func safeResume(_ queue: DispatchQueue) {if suspendCount == 1 {queue.resume()suspendCount = 0print("任務喚醒了")} else if suspendCount < 1 {print("喚醒的次數(shù)過多")} else {queue.resume()suspendCount -= 1print("喚醒的次數(shù)不夠,還需要 \(suspendCount) 次喚醒。")}}} 復制代碼通過按鈕調(diào)用測試:
let suspendAndResum = SuspendAndResum()extension QueueTestListTableViewController {// 掛起 func suspendButtonTapped(_ sender: Any) {suspendAndResum.suspendQueue()}// 喚醒 func resumeButtonTapped(_ sender: Any) {suspendAndResum.resumeQueue()} } 復制代碼掛起的執(zhí)行結果:
start test at thread: <NSThread: 0x17d357d0>{number = 1, name = main}, this is main thread
end test at thread: <NSThread: 0x17d357d0>{number = 1, name = main}, this is main thread
concurrentQueue async task1 at thread: <NSThread: 0x17d5c560>{number = 3, name = (null)}, this is not main thread
concurrentQueue async task2 at thread: <NSThread: 0x17d5c560>{number = 3, name = (null)}, this is not main thread
任務掛起了
喚醒的執(zhí)行結果:
任務喚醒了
concurrentQueue async task4 at thread: <NSThread: 0x17d5c560>{number = 3, name = (null)}, this is not main thread
concurrentQueue async task5 at thread: <NSThread: 0x17d5c560>{number = 3, name = (null)}, this is not main thread
concurrentQueue async task3 at thread: <NSThread: 0x17eae370>{number = 4, name = (null)}, this is not main thread
如果再按一次喚醒按鈕,則會提示:
喚醒的次數(shù)過多
7. 任務組
任務組相當于一系列任務的松散集合,它可以來自相同或不同隊列,扮演著組織者的角色。它可以通知外部隊列,組內(nèi)的任務是否都已完成。或者阻塞當前的線程,直到組內(nèi)的任務都完成。所有適合組隊執(zhí)行的任務都可以使用任務組,且任務組更適合集合異步任務(如果都是同步任務,直接使用串行隊列即可)。
創(chuàng)建任務組
創(chuàng)建的方式相當簡單,無需任何參數(shù):
let queueGroup = DispatchGroup() 復制代碼將任務加入到任務組中
有兩種方式加入任務組:
- 添加任務時指定任務組
- 使用 Group.enter()、 Group.leave() 配對方法,標識任務加入任務組。
兩種加入方式在對任務處理的特性上是沒有區(qū)別的,只是便利之處不同。如果任務所在的隊列是自己創(chuàng)建或引用的系統(tǒng)隊列,那么直接使用第一種方式直接加入即可。如果任務是由系統(tǒng)或第三方的 API 創(chuàng)建的,由于無法獲取到對應的隊列,只能使用第二種方式將任務加入組內(nèi),例如將 URLSession 的 addDataTask 方法加入任務組中:
extension URLSession {func addDataTask(to group: DispatchGroup,with request: URLRequest,completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)-> URLSessionDataTask {group.enter() // 進入任務組return dataTask(with: request) { (data, response, error) incompletionHandler(data, response, error)group.leave() // 離開任務組}} } 復制代碼任務組通知
等待任務組中的任務全部完成后,可以統(tǒng)一對外發(fā)送通知,有兩種方式:
- group.notify 方法,它可以在所有任務完成后通知指定隊列并執(zhí)行一個指定任務,這個通知的操作是異步的(意味著通知后續(xù)的代碼不需要等待任務,可以繼續(xù)執(zhí)行):
- group.wait 方法,它會在所有任務完成后再執(zhí)行當前線程中后續(xù)的代碼,因此這個操作是起到阻塞的作用:
wait 方法中還可以指定具體的時間,它表示將等待不超過這個時間,如果任務組在指定時間之內(nèi)完成則立即恢復當前線程,否則將等到時間結束時再恢復當前線程。
- 方式1,使用 DispatchTime,它表示一個時間間隔,精確到納秒(1/1000,000,000 秒):
- 方式2,使用 DispatchWallTime,它表示當前的絕對時間戳,精確到微秒(1/1000,000 秒),通常使用字面量即可設置延時時間,也可以使用 timespec 結構體來設置一個精確的時間戳,具體參見附錄章節(jié)的《時間相關的結構體說明 - DispatchWallTime》:
代碼示例
本章對應的代碼見示例工程中 QueueTestListTableViewController+DispatchGroup.swift, DispatchGroup.swift.
示例 7.1:創(chuàng)建任務組,并以常規(guī)方式添加任務。
示例中我們通過一個按鈕觸發(fā),創(chuàng)建一個任務組、通過常規(guī)方式添加任務、任務完成時通知主線程。
extension QueueTestListTableViewController { func creatTaskGroupButtonTapped(_ sender: Any) {let groupTest = DispatchGroupTest()let group = groupTest.creatAGroup()let queue = DispatchQueue.global()groupTest.addTaskNormally(to: group, in: queue)groupTest.notifyMainQueue(from: group)} }/// 任務組測試類,驗證任務組相關的特性。 class DispatchGroupTest {/// 創(chuàng)建一個新任務組func creatAGroup() -> DispatchGroup{return DispatchGroup()}/// 通知主線程任務組中的任務都完成func notifyMainQueue(from group: DispatchGroup) {group.notify(queue: DispatchQueue.main) {print("任務組通知:任務都完成了。\n")}}/// 創(chuàng)建常規(guī)的異步任務,并加入任務組中。func addTaskNormally(to group: DispatchGroup, in queue: DispatchQueue) {queue.async(group: group) {print("任務:喝一杯牛奶\n")}queue.async(group: group) {print("任務:吃一個蘋果\n")}} } 復制代碼執(zhí)行結果:
任務:吃一個蘋果
任務:喝一杯牛奶
任務組通知:任務都完成了。
示例 7.2:添加系統(tǒng)任務至任務組
我們通過封裝系統(tǒng) SDK 中的 URLSession 的 dataTask API,將系統(tǒng)任務加入至任務組中,使用 Group.enter()、 Group.leave() 配對方法進行標識。
本示例中,我們將通過封裝后的 API 嘗試從豆瓣同時下載兩本書的標簽集,當下載任務完成后返回一個打印任務的閉包,在主線程收到任務組全部完成的通知后,執(zhí)行該打印閉包。
extension QueueTestListTableViewController { func addSystemTaskToGroupButtonTapped(_ sender: Any) {let groupTest = DispatchGroupTest()let group = groupTest.creatAGroup()let book1ID = "5416832" // https://book.douban.com/subject/5416832/let book2ID = "1046265" // https://book.douban.com/subject/1046265/// 根據(jù)書籍 ID 下載一本豆瓣書籍的標簽集,并返回一個打印前 5 個標簽的任務閉包。let printBookTagBlock1 = groupTest.getBookTag(book1ID, in: group)let printBookTagBlock2 = groupTest.getBookTag(book2ID, in: group)// 下載任務完成后,通知主線程完成打印任務。groupTest.notifyMainQueue(from: group) {printBookTagBlock1("辛亥:搖晃的中國")printBookTagBlock2("挪威的森林")}} }class DispatchGroupTest {/// 根據(jù)書籍 ID 下載一本豆瓣書籍的標簽集,并返回一個打印前 5 個標簽的任務閉包。此任務將加入指定的任務組中執(zhí)行。func getBookTag(_ bookID: String, in taskGroup: DispatchGroup) -> (String)->() {let url = "https://api.douban.com/v2/book/\(bookID)/tags"var printBookTagBlock: (_ bookName: String)->() = {_ in print("還未收到返回的書籍信息") }// 創(chuàng)建網(wǎng)絡信息獲取成功后的任務let completion = {(data: Data?, response: URLResponse?, error: Error?) inprintBookTagBlock = { bookName inif error != nil{print(error.debugDescription)} else {guard let data = data else { return }print("書籍 《\(bookName)》的標簽信息如下:")BookTags.printBookPreviousFiveTags(data)}}}print("任務:下載書籍 \(bookID) 的信息 \(Date())")// 獲取網(wǎng)絡信息httpGet(url: url, in: taskGroup, completion: completion)let returnBlock: (String)->() = { bookName inprintBookTagBlock(bookName)}return returnBlock} }/// 執(zhí)行 http get 方法,并加入指定的任務組。 func httpGet(url: String,getString: String? = nil,session: URLSession = URLSession.shared,in taskGroup: DispatchGroup,completion: @escaping (Data?, URLResponse?, Error?) -> Void) {let httpMethod = "GET"let urlStruct = URL(string: url) //創(chuàng)建URL對象var request = URLRequest(url: urlStruct!) //創(chuàng)建請求對象var dataTask: URLSessionTaskrequest.httpMethod = httpMethodrequest.httpBody = getString?.data(using: .utf8)dataTask = session.addDataTask(to: taskGroup,with: request,completionHandler: completion)dataTask.resume() // 啟動任務 }extension URLSession {/// 將數(shù)據(jù)獲取的任務加入任務組中func addDataTask(to group: DispatchGroup,with request: URLRequest,completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)-> URLSessionDataTask {group.enter()return dataTask(with: request) { (data, response, error) inprint("下載結束:\(Date())")completionHandler(data, response, error)group.leave()}} } 復制代碼執(zhí)行結果,可以看到兩本書幾乎是同時開始下載的,全部下載結束后再進行打印:
任務:下載書籍 5416832 的信息 2018-03-13 03:21:29 +0000
任務:下載書籍 1046265 的信息 2018-03-13 03:21:29 +0000
下載結束:2018-03-13 03:21:30 +0000
下載結束:2018-03-13 03:21:30 +0000
任務組通知:任務都完成了。
書籍 《辛亥:搖晃的中國》的標簽信息如下:
歷史
張鳴
辛亥革命
民國
中國近代史
書籍 《挪威的森林》的標簽信息如下:
村上春樹
挪威的森林
小說
日本文學
日本
示例 7.3:添加系統(tǒng)及自定義任務至任務組
本示例中,我們將先從豆瓣下載一本書的標簽集,并設置一個很短的等待時間,等待過后開啟打印任務。然后再加入一個自定義隊列的任務,以及里一個書籍下載任務,當這兩個任務都完成后,再打印第二本書籍標簽信息。
extension QueueTestListTableViewController { func addSystemTaskToGroupButtonTapped(_ sender: Any) {let groupTest = DispatchGroupTest()let group = groupTest.creatAGroup()let queue = DispatchQueue.global()let book1ID = "5416832" // https://book.douban.com/subject/5416832/let book2ID = "1046265" // https://book.douban.com/subject/1046265/// 根據(jù)書籍 ID 下載一本豆瓣書籍的標簽集,并返回一個打印前 5 個標簽的任務閉包。let printBookTagBlock1 = groupTest.getBookTag(book1ID, in: group)groupTest.wait(group: group, after: 0.01) // 等待前面的任務執(zhí)行不超過 0.01 秒printBookTagBlock1("辛亥:搖晃的中國") // 等待后進行打印// 創(chuàng)建常規(guī)的異步任務,并加入任務組中。groupTest.addTaskNormally(to: group, in: queue)// 再次進行下載任務let printBookTagBlock2 = groupTest.getBookTag(book2ID, in: group)// 全部任務完成后,通知主線程完成打印任務。groupTest.notifyMainQueue(from: group) {printBookTagBlock2("挪威的森林")}} } 復制代碼執(zhí)行結果,可以看到由于等待時間太短,第一本書還未下載完就開始打印了,因此只打印了空信息。而第二本書等待正常下載完再打印的:
任務:下載書籍 5416832 的信息 2018-03-13 03:42:21 +0000
還未收到返回的書籍信息
任務:喝一杯牛奶
任務:下載書籍 1046265 的信息 2018-03-13 03:42:21 +0000
任務:吃一個蘋果
下載結束:2018-03-13 03:42:22 +0000
下載結束:2018-03-13 03:42:22 +0000
任務組通知:任務都完成了。
書籍 《挪威的森林》的標簽信息如下:
村上春樹
挪威的森林
小說
日本文學
日本
8. DispatchSource
GCD 中提供了一個 DispatchSource 類,它可以幫你監(jiān)聽系統(tǒng)底層一些對象的活動,例如這些對象: Mach port、Unix descriptor、Unix signal、VFS node,并允許你在這些活動發(fā)生時,向隊列提交一個任務以進行異步處理。
這些可監(jiān)聽的對象都有具體的類型,你可以使用 DispatchSource 的類方法來構建這些類型,這里就不一一列舉了。下面以文件監(jiān)聽為例說明 DispatchSource 的用法。
例子中監(jiān)聽了一個指定目錄下文件的寫入事件,創(chuàng)建監(jiān)聽主要有幾個步驟:
- 通過 makeFileSystemObjectSource 方法創(chuàng)建 source
- 通過 setEventHandler 設定事件處理程序,setCancelHandler 設定取消監(jiān)聽的處理。
- 執(zhí)行 resume() 方法開始接收事件
在 iOS 中這套 DispatchSource API 并不常用(DispatchSourceTimer 可能用的多點),而且僅上面的文件監(jiān)聽例子經(jīng)常接收不到事件,在 Mac 中情況可能好點。對于需要經(jīng)常和底層打交道的人來說,這里面還有很多坑需要去填。 DispatchSource 的更多例子還可以 參考這里。
9. DispatchIO
DispatchIO 對象提供一個操作文件描述符的通道。簡單講你可以利用多線程異步高效地讀寫文件。
發(fā)起讀寫操作一般步驟如下:
- 創(chuàng)建 DispatchIO 對象,或者說創(chuàng)建一個通道,并設置結束處理閉包。
- 調(diào)用 read / write 方法
- 調(diào)用 close 方法關閉通道
- 在 close 方法后系統(tǒng)將自動調(diào)用結束處理閉包
下面介紹下各方法的使用。
初始化方法
一般使用兩種方式初始化:文件描述符,或者文件路徑。
文件描述符方式
文件描述符使用 open方法創(chuàng)建:open(_ path: UnsafePointer<CChar>, _ oflag: Int32, _ mode: mode_t) -> Int32,第一個參數(shù)是 UnsafePointer<Int8> 類型的路徑,oflag 、mode 指文件的操作權限,一個是系統(tǒng) API 級的,一個是文件系統(tǒng)級的,可選項如下:
oflag:
| O_RDONLY | 以只讀方式打開文件 | 此三種讀寫類型只能有一種 |
| O_WRONLY | 以只寫方式打開文件 | 此三種讀寫類型只能有一種 |
| O_RDWR | 以讀和寫的方式打開文件 | 此三種讀寫類型只能有一種 |
| O_CREAT | 打開文件,如果文件不存在則創(chuàng)建文件 | 創(chuàng)建文件時會使用Mode參數(shù)與Umask配合設置文件權限 |
| O_EXCL | 如果已經(jīng)置O_CREAT且文件存在,則強制open()失敗 | 可以用來檢測多個進程之間創(chuàng)建文件的原子操作 |
| O_TRUNC | 將文件的長度截為0 | 無論打開方式是RD,WR,RDWR,只要打開就會把文件清空 |
| O_APPEND | 強制write()從文件尾開始不care當前文件偏移量所處位置,只會在文件末尾開始添加 | 如果不使用的話,只會在文件偏移量處開始覆蓋原有內(nèi)容寫文件 |
mode:包含 User、Group、Other 三個組對應的權限掩碼。
| S_IRWXU | S_IRWXG | S_IRWXO | 可讀、可寫、可執(zhí)行 |
| S_IRUSR | S_IRGRP | S_IROTH | 可讀 |
| S_IWUSR | S_IWGR | S_IWOTH | 可寫 |
| S_IXUSR | S_IXGRP | S_IXOTH | 可執(zhí)行 |
創(chuàng)建的通道有兩種類型:
- 連續(xù)數(shù)據(jù)流:DispatchIO.StreamType.stream,這個方式是對文件從頭到尾完整操作的。
- 隨機片段數(shù)據(jù):DispatchIO.StreamType.random,這個方式是在文件的任意一個位置(偏移量)開始操作的。
文件路徑方式
let io = DispatchIO(type: .stream, path: filePath.utf8String!, oflag: (O_RDWR | O_CREAT | O_APPEND), mode: (S_IRWXU | S_IRWXG), queue: queue, cleanupHandler: cleanupHandler) 復制代碼數(shù)據(jù)塊大小閥值
DispatchIO 支持多線程操作的原因之一就是它將文件拆分為數(shù)據(jù)塊進行并行操作,你可以設置數(shù)據(jù)塊大小的上下限,系統(tǒng)會采取合適的大小,使用這兩個方法即可:setLimit(highWater: Int)、setLimit(lowWater: Int),單位是 byte。
io.setLimit(highWater: 1024*1024) 復制代碼數(shù)據(jù)塊如果設置小一點(如 1M),則可以節(jié)省 App 的內(nèi)存,如果內(nèi)存足夠則可以大一點換取更快速度。在進行讀寫操作時,有一個性能問題需要注意,如果同時讀寫的話一般分兩個通道,且讀到一個數(shù)據(jù)塊就立即寫到另一個數(shù)據(jù)塊中,那么寫通道的數(shù)據(jù)塊上限不要小于讀通道的,否則會造成內(nèi)存大量積壓無法及時釋放。
讀操作
方法示例:
ioRead.read(offset: 0, length: Int.max, queue: ioReadQueue) { doneReading, data, error inif (error > 0) {print("讀取發(fā)生錯誤了,錯誤碼:\(error)")return}if (data != nil) {// 使用數(shù)據(jù)}if (doneReading) {ioRead.close()} } 復制代碼offset 指定讀取的偏移量,如果通道是 stream 類型,值不起作用,寫為 0 即可,將從文件開頭讀起;如果是 random 類型,則指相對于創(chuàng)建通道時文件的起始位置的偏移量。
length 指定讀取的長度,如果是讀取文件全部內(nèi)容,設置 Int.max 即可,否則設置一個小于文件大小的值(單位是 byte)。
每讀取到一個數(shù)據(jù)塊都會調(diào)用你設置的處理閉包,系統(tǒng)會提供三個入?yún)⒔o你:結束標志、本次讀取到的數(shù)據(jù)塊、錯誤碼:
- 在所有數(shù)據(jù)讀取完成后,會額外再調(diào)用一個閉包,通過結束標志告訴你操作結束了,此時 data 大小是 0,錯誤碼也是 0。
- 如果讀取中間發(fā)生了錯誤,則會停止讀取,結束標志會被設置為 true,并返回相應的錯誤碼,錯誤碼表參考稍后的【關閉通道】小節(jié):
寫操作
方法示例:
ioWrite.write(offset: 0, data: data!, queue: ioWriteQueue) { doneWriting, data, error inif (error > 0) {print("寫入發(fā)生錯誤了,錯誤碼:\(error)")return}if doneWriting {//...ioWrite.close()} } 復制代碼寫操作與讀操作的唯一區(qū)別是:每當寫完一個數(shù)據(jù)塊時,回調(diào)閉包返回的 data 是剩余的全部數(shù)據(jù)。同時注意如果是 stream 類型,將接著文件的末尾寫數(shù)據(jù)。
關閉通道
當讀寫正常完成,或者你需要中途結束操作時,需要調(diào)用 close 方法,這個方法帶一個 DispatchIO.CloseFlags 類型參數(shù),如果不指定將默認值為 DispatchIO.CloseFlags.stop。
這個方法傳入 stop 標志時將會停止所有未完成的讀寫操作,影響范圍是所有 I/O channel,其他 DispatchIO 對象進行中的讀寫操作將會收到一個 ECANCELED 錯誤碼,rawValue 值是 89,這個錯誤碼是 POSIXError 結構的一個屬性,而 POSIXError 又是 NSError 中預定義的一個錯誤域。
因此如果要在不同 DispatchIO 對象中并行讀取操作互不影響, close 方法標志可以設置一個空值:DispatchIO.CloseFlags()。如果設置了 stop 標志,則要做好不同 IO 之間的隔離,通過任務組的enter、leave、wait 方法可以做到較好的隔離。
ioWrite.close() // 停止標志 ioWrite.close(flags: DispatchIO.CloseFlags()) // 空標志 復制代碼POSIXError 碼表:
EPERM = 1 // 無 ENOENT = 2 // No such file or directory. ESRCH = 3 // No such process. EINTR = 4 // Interrupted system call. EIO = 5 // Input/output error. ENXIO = 6 // Device not configured. E2BIG = 7 // Argument list too long. ENOEXEC = 8 // Exec format error. EBADF = 9 // Bad file descriptor. ECHILD = 10 // No child processes. EDEADLK = 11 // Resource deadlock avoided. ENOMEM = 12 // Cannot allocate memory. EACCES = 13 // Permission denied. EFAULT = 14 // Bad address. ENOTBLK = 15 // Block device required. EBUSY = 16 // Device / Resource busy. EEXIST = 17 // File exists. EXDEV = 18 // Cross-device link. ENODEV = 19 // Operation not supported by device. ENOTDIR = 20 // Not a directory. EISDIR = 21 // Is a directory. EINVAL = 22 // Invalid argument. ENFILE = 23 // Too many open files in system. EMFILE = 24 // Too many open files. ENOTTY = 25 // Inappropriate ioctl for device. ETXTBSY = 26 // Text file busy. EFBIG = 27 // File too large. ENOSPC = 28 // No space left on device. ESPIPE = 29 // Illegal seek. EROFS = 30 // Read-only file system. EMLINK = 31 // Too many links. EPIPE = 32 // Broken pipe. EDOM = 33 // math software. Numerical argument out of domain. ERANGE = 34 // Result too large. EAGAIN = 35 // non-blocking and interrupt i/o. Resource temporarily unavailable. EWOULDBLOCK = 35 // Operation would block. EINPROGRESS = 36 // Operation now in progress. EALREADY = 37 // Operation already in progress. ENOTSOCK = 38 // ipc/network software – argument errors. Socket operation on non-socket. EDESTADDRREQ = 39 // Destination address required. EMSGSIZE = 40 // Message too long. EPROTOTYPE = 41 // Protocol wrong type for socket. ENOPROTOOPT = 42 // Protocol not available. EPROTONOSUPPORT = 43 // Protocol not supported. ESOCKTNOSUPPORT = 44 // Socket type not supported. ENOTSUP = 45 // Operation not supported. EPFNOSUPPORT = 46 // Protocol family not supported. EAFNOSUPPORT = 47 // Address family not supported by protocol family. EADDRINUSE = 48 // Address already in use. EADDRNOTAVAIL = 49 // Can’t assign requested address. ENETDOWN = 50 // ipc/network software – operational errors Network is down. ENETUNREACH = 51 // Network is unreachable. ENETRESET = 52 // Network dropped connection on reset. ECONNABORTED = 53 // Software caused connection abort. ECONNRESET = 54 // Connection reset by peer. ENOBUFS = 55 // No buffer space available. EISCONN = 56 // Socket is already connected. ENOTCONN = 57 // Socket is not connected. ESHUTDOWN = 58 // Can’t send after socket shutdown. ETOOMANYREFS = 59 // Too many references: can’t splice. ETIMEDOUT = 60 // Operation timed out. ECONNREFUSED = 61 // Connection refused. ELOOP = 62 // Too many levels of symbolic links. ENAMETOOLONG = 63 // File name too long. EHOSTDOWN = 64 // Host is down. EHOSTUNREACH = 65 // No route to host. ENOTEMPTY = 66 // Directory not empty. EPROCLIM = 67 // quotas & mush. Too many processes. EUSERS = 68 // Too many users. EDQUOT = 69 // Disc quota exceeded. ESTALE = 70 // Network File System. Stale NFS file handle. EREMOTE = 71 // Too many levels of remote in path. EBADRPC = 72 // RPC struct is bad. ERPCMISMATCH = 73 // RPC version wrong. EPROGUNAVAIL = 74 // RPC prog. not avail. EPROGMISMATCH = 75 // Program version wrong. EPROCUNAVAIL = 76 // Bad procedure for program. ENOLCK = 77 // No locks available. ENOSYS = 78 // Function not implemented. EFTYPE = 79 // Inappropriate file type or format. EAUTH = 80 // Authentication error. ENEEDAUTH = 81 // Need authenticator. EPWROFF = 82 // Intelligent device errors. Device power is off. EDEVERR = 83 // Device error e.g. paper out. EOVERFLOW = 84 // Value too large to be stored in data type. EBADEXEC = 85 // Program loading errors. Bad executable. EBADARCH = 86 // Bad CPU type in executable. ESHLIBVERS = 87 // Shared library version mismatch. EBADMACHO = 88 // Malformed Macho file. ECANCELED = 89 // Operation canceled. EIDRM = 90 // Identifier removed. ENOMSG = 91 // No message of desired type. EILSEQ = 92 // Illegal byte sequence. ENOATTR = 93 // Attribute not found. EBADMSG = 94 // Bad message. EMULTIHOP = 95 // Reserved. ENODATA = 96 // No message available on STREAM. ENOLINK = 97 // Reserved. ENOSR = 98 // No STREAM resources. ENOSTR = 99 // Not a STREAM. EPROTO = 100 // Protocol error. ETIME = 101 // STREAM ioctl timeout. ENOPOLICY = 103 // No such policy registered. ENOTRECOVERABLE = 104 // State not recoverable. EOWNERDEAD = 105 // Previous owner died. EQFULL = 106 // Interface output queue is full. 復制代碼代碼示例
示例 9.1:將兩個大文件(通過壓縮工具拆分的包)合并為一個文件。
實現(xiàn)思路:分別創(chuàng)建一個讀、寫通道,使用同一個串行隊列處理數(shù)據(jù),每讀到一個數(shù)據(jù)塊就提交一個寫數(shù)據(jù)的任務,同時要保證按照讀取的順序提交寫任務,在第一個文件讀寫完成后再開始第二個文件的讀寫操作。
測試文件地址:WWDC 2016-720,通過 Zip 壓縮拆分為兩個文件(Normal 方式),設置按 350M 進行分割。注意測試時,建議使用模擬器,更方便讀寫 Mac 本地文件,后續(xù)類似例子相同。
class DispatchIOTest {/// 利用很小的內(nèi)存空間及同一隊列讀寫方式合并文件static func combineFileWithOneQueue() {let files: NSArray = ["/Users/xxx/Downloads/gcd.mp4.zip.001","/Users/xxx/Downloads/gcd.mp4.zip.002"]let outFile: NSString = "/Users/xxx/Downloads/gcd.mp4.zip"let ioQueue = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue")let queueGroup = DispatchGroup()let ioWriteCleanupHandler: (Int32) -> Void = { errorNumber inprint("寫入文件完成 @\(Date())。")}let ioReadCleanupHandler: (Int32) -> Void = { errorNumber inprint("讀取文件完成。")}let ioWrite = DispatchIO(type: .stream,path: outFile.utf8String!,oflag: (O_RDWR | O_CREAT | O_APPEND),mode: (S_IRWXU | S_IRWXG),queue: ioQueue,cleanupHandler: ioWriteCleanupHandler)ioWrite?.setLimit(highWater: 1024*1024)// print("開始操作 @\(Date()).")files.enumerateObjects { fileName, index, stop inif stop.pointee.boolValue {return}queueGroup.enter()let ioRead = DispatchIO(type: .stream,path: (fileName as! NSString).utf8String!,oflag: O_RDONLY,mode: 0,queue: ioQueue,cleanupHandler: ioReadCleanupHandler)ioRead?.setLimit(highWater: 1024*1024)print("開始讀取文件: \(fileName) 的數(shù)據(jù)")ioRead?.read(offset: 0, length: Int.max, queue: ioQueue) { doneReading, data, error inprint("當前讀線程:\(Thread.current)--->")if (error > 0 || stop.pointee.boolValue) {print("讀取發(fā)生錯誤了,錯誤碼:\(error)")ioWrite?.close()stop.pointee = truereturn}if (data != nil) {let bytesRead: size_t = data!.countif (bytesRead > 0) {queueGroup.enter()ioWrite?.write(offset: 0, data: data!, queue: ioQueue) {doneWriting, data, error inprint("當前寫線程:\(Thread.current)--->")if (error > 0 || stop.pointee.boolValue) {print("寫入發(fā)生錯誤了,錯誤碼:\(error)")ioRead?.close()stop.pointee = truequeueGroup.leave()return}if doneWriting {queueGroup.leave()}print("--->當前寫線程:\(Thread.current)")}}}if (doneReading) {ioRead?.close()if (files.count == (index+1)) {ioWrite?.close()}queueGroup.leave()}print("--->當前讀線程:\(Thread.current)")}_ = queueGroup.wait(timeout: .distantFuture)}} } 復制代碼執(zhí)行結果,可以看到串行隊列利用了好幾個線程來處理讀寫操作,但是細看同一時間只運行了一個線程,符合我們前面總結的串行隊列的特點:
開始讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.001 的數(shù)據(jù)
當前讀線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}--->
--->當前讀線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}
當前讀線程:<NSThread: 0x6000006628c0>{number = 4, name = (null)}--->
--->當前讀線程:<NSThread: 0x6000006628c0>{number = 4, name = (null)}
當前寫線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}--->
--->當前寫線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}
當前讀線程:<NSThread: 0x6000006628c0>{number = 4, name = (null)}--->
--->當前讀線程:<NSThread: 0x6000006628c0>{number = 4, name = (null)}
......
當前讀線程:<NSThread: 0x600000662840>{number = 6, name = (null)}--->
--->當前讀線程:<NSThread: 0x600000662840>{number = 6, name = (null)}
當前寫線程:<NSThread: 0x600000662ac0>{number = 7, name = (null)}--->
--->當前寫線程:<NSThread: 0x600000662ac0>{number = 7, name = (null)}
......
讀取文件完成。
......
當前寫線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}--->
--->當前寫線程:<NSThread: 0x60400027e500>{number = 3, name = (null)}
寫入文件完成。
關閉 print 后的內(nèi)存占用情況見下圖,可以看到在讀寫過程中只額外占用了 1M 左右內(nèi)存,用時 1s 左右,非常的棒。
開始操作 @2018-xx-xx 13:51:52 +0000.
開始讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.001 的數(shù)據(jù)
讀取文件完成。
開始讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.002 的數(shù)據(jù)
讀取文件完成。
寫入文件完成 @2018-xx-xx 13:51:53 +0000。
示例 9.2:利用多個隊列將兩個大文件合并為一個文件。
這個例子在上面例子的基礎上,各使用兩個隊列來進行讀、寫操作,驗證利用地址偏移的方式多線程同時讀寫文件的效率。
這里對讀寫文件時的偏移量 offset 再做個簡單說明:文件開頭的偏移量是 0,后續(xù)逐漸遞增,直到文件末尾的偏移量是 (按字節(jié)計算的文件大小 - 1)。
/// 利用很小的內(nèi)存空間及雙隊列讀寫方式合并文件 static func combineFileWithMoreQueues() {let files: NSArray = ["/Users/xxx/Downloads/gcd.mp4.zip.001","/Users/xxx/Downloads/gcd.mp4.zip.002"] // 真機運行時可使用以下地址(需手動將文件放入工程中) // let files: NSArray = [Bundle.main.path(forResource: "gcd.mp4.zip", ofType: "001")!, // Bundle.main.path(forResource: "gcd.mp4.zip", ofType: "002")!]var filesSize = files.map {return (try! FileManager.default.attributesOfItem(atPath: $0 as! String)[FileAttributeKey.size] as! NSNumber).int64Value}let outFile: NSString = "/Users/xxx/Downloads/gcd.mp4.zip" // 真機運行時可使用以下地址(需手動將文件放入工程中) // let outFile: NSString = "\(NSTemporaryDirectory())/gcd.mp4.zip" as NSString// 每個分塊文件各一個讀、寫隊列let ioReadQueue1 = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue1")let ioReadQueue2 = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue2")let ioWriteQueue1 = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue3")let ioWriteQueue2 = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue4")let ioReadQueueArray = [ioReadQueue1, ioReadQueue2]let ioWriteQueueArray = [ioWriteQueue1, ioWriteQueue2]let ioWriteCleanupHandler: (Int32) -> Void = { errorNumber inprint("寫入文件完成 @\(Date())。")}let ioReadCleanupHandler: (Int32) -> Void = { errorNumber inprint("讀取文件完成 @\(Date())。")}let queueGroup = DispatchGroup()print("開始操作 @\(Date()).")let ioWrite = DispatchIO(type: .random, path: outFile.utf8String!, oflag: (O_RDWR | O_CREAT | O_APPEND), mode: (S_IRWXU | S_IRWXG), queue: ioWriteQueue1, cleanupHandler: ioWriteCleanupHandler)ioWrite?.setLimit(highWater: 1024 * 1024)ioWrite?.setLimit(lowWater: 1024 * 1024)filesSize.insert(0, at: 0)filesSize.removeLast()for (index, file) in files.enumerated() {DispatchQueue.global().sync {queueGroup.enter()let ioRead = DispatchIO(type: .stream, path: (file as! NSString).utf8String!, oflag: O_RDONLY, mode: 0, queue: ioReadQueue1, cleanupHandler: ioReadCleanupHandler)ioRead?.setLimit(highWater: 1024 * 1024)ioRead?.setLimit(lowWater: 1024 * 1024)var writeOffsetTemp = filesSize[0...index].reduce(0) { offset, size inreturn offset + size}ioRead?.read(offset: 0, length: Int.max, queue: ioReadQueueArray[index]) {doneReading, data, error in // print("讀取文件: \(file),線程:\(Thread.current)--->")if (error > 0) {print("讀取文件: \(file) 發(fā)生錯誤了,錯誤碼:\(error)")return}if (doneReading) {ioRead?.close()queueGroup.leave()}if (data != nil) {let bytesRead: size_t = data!.countif (bytesRead > 0) {queueGroup.enter()ioWrite?.write(offset: writeOffsetTemp, data: data!, queue: ioWriteQueueArray[index]) {doneWriting, writeData, error in // print("寫入文件: \(file), 線程:\(Thread.current)--->")if (error > 0) {print("寫入文件: \(file) 發(fā)生錯誤了,錯誤碼:\(error)")ioRead?.close()return}if doneWriting {queueGroup.leave()} // print("--->寫入文件: \(file), 線程:\(Thread.current)")}writeOffsetTemp = writeOffsetTemp + Int64(data!.count)}} // print("--->讀取文件: \(file) ,線程:\(Thread.current)")}}}_ = queueGroup.wait(timeout: .distantFuture)ioWrite?.close()} 復制代碼執(zhí)行結果,可以看到 4 個串行隊列同時都在運行:
開始操作 @2018-04-03 03:57:00 +0000.
讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.001,線程:<NSThread: 0x60400047d940>{number = 3, name = (null)}--->
讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.002,線程:<NSThread: 0x60400047d9c0>{number = 4, name = (null)}--->
--->讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.002 ,線程:<NSThread: 0x60400047d9c0>{number = 4, name = (null)}
--->讀取文件: /Users/xxx/Downloads/gcd.mp4.zip.001 ,線程:<NSThread: 0x60400047d940>{number = 3, name = (null)}
......
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x60400047d940>{number = 3, name = (null)}--->
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.002, 線程:<NSThread: 0x600000672980>{number = 5, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x60400047d940>{number = 3, name = (null)}
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.002, 線程:<NSThread: 0x600000672980>{number = 5, name = (null)}
......
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}
讀取文件完成 @2018-04-03 03:57:04 +0000。
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
讀取文件完成 @2018-04-03 03:57:04 +0000。
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}
......
寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}--->
--->寫入文件: /Users/xxx/Downloads/gcd.mp4.zip.001, 線程:<NSThread: 0x6000006720c0>{number = 9, name = (null)}
寫入文件完成 @2018-04-03 03:57:04 +0000。
關閉 print 后的內(nèi)存占用情況見下圖,可以看到在讀寫過程中額外占用了 3M 左右內(nèi)存,用時 2s 左右。這個結果中,內(nèi)存占用比單隊列大(這個比較好理解),但速度還更慢了,性能瓶頸很有可能是在磁盤讀寫上。所以涉及文件寫操作時,并不是線程越多越快,要考慮傳輸速度、文件大小等因素。
開始操作 @2018-04-03 04:05:44 +0000.
讀取文件完成 @2018-04-03 04:05:45 +0000。
讀取文件完成 @2018-04-03 04:05:46 +0000。
寫入文件完成 @2018-04-03 04:05:46 +0000。
10. DispatchData
DispatchData 對象可以管理基于內(nèi)存的數(shù)據(jù)緩沖區(qū)。這個數(shù)據(jù)緩沖區(qū)對外表現(xiàn)為連續(xù)的內(nèi)存區(qū)域,但內(nèi)部可能由多個獨立的內(nèi)存區(qū)域組成。
DispatchData 對象很多特性類似于 Data 對象,且 Data 對象可以轉換為 DispatchData 對象,而通過 DispatchIO 的 read 方法獲得的數(shù)據(jù)也是封裝為 DispatchData 對象的。
下面再看個示例,通過 Data、DispatchData、DispatchIO 這三種類型結合,完成內(nèi)存占用更小也同樣快速的文件讀寫操作。
代碼示例
示例 10.1:將兩個大文件合并為一個文件(與示例 9.1 類似)。
實現(xiàn)思路:首先將兩個文件轉換為 Data 對象,再轉換為 DispatchData 對象,然后拼接兩個對象為一個 DispatchData 對象,最后通過 DispatchIO 的 write 方法寫入文件中。看起來有多次的轉換過程,實際上 Data 類型讀取文件時支持虛擬隱射的方式,而 DispatchData 類型更是支持多個數(shù)據(jù)塊虛擬拼接,也不占用什么內(nèi)存。
實際上完全使用 Data 類型也能完成文件合并,利用 append、write 方法即可,但是 append 方法是要占用比文件大小稍大的內(nèi)存,write 方法也要占用額外內(nèi)存空間。即使使用 NSMutableData 類型不占用內(nèi)存的 append 方法通過虛擬隱射方式讀文件(即讀文件、拼接數(shù)據(jù)都不占用內(nèi)存),但是 NSMutableData 類型的 write 方法還是要占用額外內(nèi)存,雖然要比 Data 類型內(nèi)存少很多,但是也不少了。因此 DispatchData 類型在內(nèi)存占用上更有優(yōu)勢。
/// 利用 DispatchData 類型快速合并文件 static func combineFileWithDispatchData() {let filePathArray = ["/Users/xxx/Downloads/gcd.mp4.zip.001","/Users/xxx/Downloads/gcd.mp4.zip.002"]let outputFilePath: NSString = "/Users/xxx/Downloads/gcd.mp4.zip"let ioWriteQueue = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue")let ioWriteCleanupHandler: (Int32) -> Void = { errorNumber inprint("寫入文件完成 @\(Date()).")}let ioWrite = DispatchIO(type: .stream,path: outputFilePath.utf8String!,oflag: (O_RDWR | O_CREAT | O_APPEND),mode: (S_IRWXU | S_IRWXG),queue: ioWriteQueue,cleanupHandler: ioWriteCleanupHandler)ioWrite?.setLimit(highWater: 1024*1024*2)print("開始操作 @\(Date()).")// 將所有文件合并為一個 DispatchData 對象let dispatchData = filePathArray.reduce(DispatchData.empty) { data, filePath in// 將文件轉換為 Datalet url = URL(fileURLWithPath: filePath)let fileData = try! Data(contentsOf: url, options: .mappedIfSafe)var tempData = data// 將 Data 轉換為 DispatchDatalet dispatchData = fileData.withUnsafeBytes {(u8Ptr: UnsafePointer<UInt8>) -> DispatchData inlet rawPtr = UnsafeRawPointer(u8Ptr)let innerData = Unmanaged.passRetained(fileData as NSData)return DispatchData(bytesNoCopy:UnsafeRawBufferPointer(start: rawPtr, count: fileData.count),deallocator: .custom(nil, innerData.release))}// 拼接 DispatchDatatempData.append(dispatchData)return tempData}//將 DispatchData 對象寫入結果文件中ioWrite?.write(offset: 0, data: dispatchData, queue: ioWriteQueue) {doneWriting, data, error inif (error > 0) {print("寫入發(fā)生錯誤了,錯誤碼:\(error)")return}if data != nil { // print("正在寫入文件,剩余大小:\(data!.count) bytes.")}if (doneWriting) {ioWrite?.close()}} } 復制代碼執(zhí)行結果:
開始操作 @2018-xx-xx 13:32:37 +0000.
正在寫入文件,剩余大小:640096267 bytes.
正在寫入文件,剩余大小:639047691 bytes.
......
正在寫入文件,剩余大小:464907 bytes.
寫入文件完成 @2018-xx-xx 13:32:40 +0000.
關閉 print 后的內(nèi)存占用情況見下圖,可以看到在整個讀寫過程中幾乎沒有額外占用內(nèi)存,速度很快在 1s 左右,這個讀寫方案堪稱完美,這要歸功于 DispatchData 的虛擬拼接和 DispatchIO 的分塊讀寫大小控制。這里順便提一下 DispatchIO 數(shù)據(jù)閥值上限 highWater,經(jīng)過測試,如果設置為 1M,將耗時 4s 左右,設為 2M 及以上時,耗時均為 1s 左右,非常快速,而所有閥值的內(nèi)存占用都很少。所以設置合理的閥值,對性能的改善也是有幫助的。
11. 信號量
DispatchSemaphore,通常稱作信號量,顧名思義,它可以通過計數(shù)來標識一個信號,這個信號怎么用呢,取決于任務的性質(zhì)。通常用于對同一個資源訪問的任務數(shù)進行限制。
例如,控制同一時間寫文件的任務數(shù)量、控制端口訪問數(shù)量、控制下載任務數(shù)量等。
信號量的使用非常的簡單:
- 首先創(chuàng)建一個初始數(shù)量的信號對象
- 使用 wait 方法讓信號量減 1,再安排任務。如果此時信號量仍大于或等于 0,則任務可執(zhí)行,如果信號量小于 0,則任務需要等待其他地方釋放信號。
- 任務完成后,使用 signal 方法增加一個信號量。
- 等待信號有兩種方式:永久等待、可超時的等待。
下面看個簡單的例子
代碼示例
示例 11.1:限制同時運行的任務數(shù)。
/// 信號量測試類 class DispatchSemaphoreTest {/// 限制同時運行的任務數(shù)static func limitTaskNumber() {let queue = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.concurrentQueue",attributes: .concurrent)let semaphore = DispatchSemaphore(value: 2) // 設置數(shù)量為 2 的信號量semaphore.wait()queue.async {task(index: 1)semaphore.signal()}semaphore.wait()queue.async {task(index: 2)semaphore.signal()}semaphore.wait()queue.async {task(index: 3)semaphore.signal()}}/// 任務static func task(index: Int) {print("Begin task \(index) --->")Thread.sleep(forTimeInterval: 2)print("Sleep for 2 seconds in task \(index).")print("--->End task \(index).")}} 復制代碼執(zhí)行結果,示例中設置了同時只能運行 2 個任務,可以看到任務 3 在前兩個任務完成后才開始運行:
Begin task 2 --->
Begin task 1 --->
Sleep for 2 seconds in task 2.
Sleep for 2 seconds in task 1.
--->End task 2.
--->End task 1.
Begin task 3 --->
Sleep for 2 seconds in task 3.
--->End task 3.
12. 任務對象
在隊列和任務組中,任務實際上是被封裝為一個 DispatchWorkItem 對象的。任務封裝最直接的好處就是可以取消任務。
前面提到的柵欄任務就是通過封裝任務對象實現(xiàn)的。
創(chuàng)建任務
先看看它的創(chuàng)建,其中 qos、flags 參數(shù)都有默認值,可以不填:
let workItem = DispatchWorkItem(qos: .default, flags: DispatchWorkItemFlags()) {// Do something } 復制代碼qos 前面提到過了,這里說一下 DispatchWorkItemFlags,它有以下幾個靜態(tài)屬性(詳細解釋可參考 官方源碼 ):
- assignCurrentContext: 標記應該為任務分配創(chuàng)建它時的上下文屬性(例如:QoS、os_activity_t、可能存在的當前 IPC 請求屬性)。如果直接調(diào)用任務,任務對象將在它的持續(xù)時間內(nèi)在調(diào)用線程中應用這些屬性。如果提交任務至隊列中,則會替換提交任務時的上下文屬性默認值。
- barrier: 標記任務為柵欄任務,提交至并行隊列時生效,如果直接運行該任務對象則無此效果。
- detached: 標記任務在執(zhí)行時應該剝離當前執(zhí)行上下文屬性(例如:QoS、os_activity_t、可能存在的當前 IPC 請求屬性)。如果直接調(diào)用任務,任務對象將在它的持續(xù)時間內(nèi)從調(diào)用線程中刪除這些屬性(如果存在屬性,且應用于任務之前)。如果提交任務至隊列中,將使用隊列的屬性(或專門分配給任務對象的任何屬性)進行執(zhí)行。如果創(chuàng)建任務時指定了 QoS,則該 QoS 將優(yōu)先于 flag 對應的 QoS 值。
- enforceQoS: 標記任務提交至隊列執(zhí)行時,任務對象被分配的 QoS (提交任務時的值)應優(yōu)先于隊列的 QoS,這樣做不會降低 QoS。當任務提交至隊列同步執(zhí)行時,或則直接執(zhí)行任務時,這個 flag 是默認值。
- inheritQoS: 標記任務提交至隊列執(zhí)行時,隊列的 QoS 應優(yōu)先于任務對象被分配的 QoS (提交任務時的值),后一個 QoS 值只會在隊列的 QoS 有問題時才會采用,這樣做會導致 QoS 不會低于繼承自隊列的 QoS。當任務提交至隊列異步執(zhí)行時,這個 flag 是默認值,且直接執(zhí)行任務時該標志無效。
- noQoS: 標記任務不應指定 QoS,如果直接執(zhí)行,將以調(diào)用線程的 QoS 執(zhí)行。如果提交至隊列,則會替換提交任務時的 QoS 默認值。
執(zhí)行任務
執(zhí)行任務時,調(diào)用任務項對象的 perform() 方法,這個調(diào)用是同步執(zhí)行的:
workItem.perform() 復制代碼或則在隊列中執(zhí)行:
let queue = DispatchQueue.global() queue.async(execute: workItem) 復制代碼取消任務
在任務未實際執(zhí)行之前可以取消任務,調(diào)用 cancel() 方法,這個調(diào)用是異步執(zhí)行的:
workItem.cancel() 復制代碼取消任務將會帶來以下結果:
- 取消將導致 任何 將來的任務在執(zhí)行時立即返回,但不會影響已在執(zhí)行的任務。
- 與任務對象關聯(lián)的任何資源的釋放都會延遲,直到下一次嘗試執(zhí)行任務對象(或者任何正在進行中的執(zhí)行已完成)。因此需要注意確保可能被取消的任務對象不要捕獲任何需要實際執(zhí)行才能釋放的資源,例如使用 malloc(3) 進行內(nèi)存分配,而在任務中調(diào)用 free(3) 釋放。 如果由于取消而從未執(zhí)行任務,則會導致內(nèi)存泄露。
任務通知
任務對象也有一個通知方法,在任務執(zhí)行完成后可以向指定隊列發(fā)送一個異步調(diào)用閉包:
workItem.notify(queue: queue) {// Do something } 復制代碼這個通知方法有一些地方需要注意:
- 任務不支持在被多次調(diào)用結束后再發(fā)出通知,運行時將會報錯,通知只能響應一次完整的調(diào)用(如果在發(fā)出通知時,還有另一次執(zhí)行未完成,這種情況也視為只有一次調(diào)用)。需要在多次執(zhí)行結束后發(fā)出通知,使用任務組的通知更合適。
- 可以多次發(fā)出通知,但通知執(zhí)行的順序是不確定的。
- 任務只要提交至隊列中,即使調(diào)用 cancel() 方法被取消了,通知也可以生效。
任務等待
任務對象支持等待方法,類似于任務組的等待,也是阻塞型的,需要等待已有的任務完成才能繼續(xù)執(zhí)行,也可以指定等待時間:
workItem.perform() workItem.wait() workItem.wait(timeout: DispatchTime) // 指定等待時間 workItem.wait(wallTimeout: DispatchWallTime) // 指定等待時間 // 等待任務完成 // do something 復制代碼下面看個完整的例子:
代碼示例
示例 12.1:任務對象測試。
/// 任務對象測試 func dispatchWorkItemTestButtonTapped(_ sender: Any) {DispatchWorkItemTest.workItemTest() }/// 任務對象測試類 class DispatchWorkItemTest {static func workItemTest() {var value = 10let workItem = DispatchWorkItem {print("workItem running start.--->")value += 5print("value = ", value)print("--->workItem running end.")}let queue = DispatchQueue.global()queue.async(execute: workItem)queue.async {print("異步執(zhí)行 workItem")workItem.perform()print("任務2取消了嗎:\(workItem.isCancelled)")workItem.cancel()print("異步執(zhí)行 workItem end")}workItem.notify(queue: queue) {print("notify 1: value = ", value)}workItem.notify(queue: queue) {print("notify 2: value = ", value)}workItem.notify(queue: queue) {print("notify 3: value = ", value)}queue.async {print("異步執(zhí)行2 workItem")Thread.sleep(forTimeInterval: 2)print("任務3取消了嗎:\(workItem.isCancelled)")workItem.perform()print("異步執(zhí)行2 workItem end")}}} 復制代碼執(zhí)行結果,可以看到任務第一次執(zhí)行完成后,發(fā)出了 3 次通知,而且未按照代碼的順序。在發(fā)出通知前,任務還有一次執(zhí)行未完成,并未造成通知報錯。第二次執(zhí)行任務后,取消了任務,因此任務第三次未正常執(zhí)行:
workItem running start.--->
異步執(zhí)行 workItem
異步執(zhí)行2 workItem
value = 15
workItem running start.--->
value = 20
--->workItem running end.
任務2取消了嗎:false
異步執(zhí)行 workItem end
notify 2: value = 20
notify 3: value = 20
notify 1: value = 20
--->workItem running end.
任務3取消了嗎:true
異步執(zhí)行2 workItem end
附:時間相關的結構體說明
DispatchTime
它通過時間間隔的方式來表示一個時間點,初始時間從系統(tǒng)最近一次開機時間開始計算,而且在系統(tǒng)休眠時暫停計時,等系統(tǒng)恢復后繼續(xù)計時,精確到納秒(1/1000,000,000 秒)。可以直接使用 + 運算符設定延時,如果使用變量延時要使用 TimeInterval 類型:
DispatchTime.now() // 表示當前時間與開機時間的間隔let twoSecondAfter = DispatchTime.now() + 2.1 // 當前時間之后 2.1 秒 復制代碼DispatchWallTime
它表示一個絕對時間的時間戳,可以直接使用字面量表示延時,也可以借用 timespec 結構體來表示,以微秒為單位(1/1000,000 秒)。
// 使用字面量設置 var wallTime = DispatchWallTime.now() + 2.0 // 表示從當前時間開始后 2 秒,數(shù)字字面量也可以改為使用 TimeInterval 類型變量// 獲取當前時間,以 timeval 結構體的方式表示 var getTimeval = timeval() gettimeofday(&getTimeval, nil)// 轉換為 timespec 結構體 let time = timespec(tv_sec: __darwin_time_t(getTimeval.tv_sec), tv_nsec: Int(getTimeval.tv_usec * 1000))// 轉換為 DispatchWallTime let wallTime = DispatchWallTime(timespec: time) 復制代碼如何通過字符串字面量創(chuàng)建 DispatchWallTime 時間戳
首先需要做一些擴展:
extension Date {/// 通過字符串字面量創(chuàng)建 DispatchWallTime 時間戳////// - Parameter dateString: 時間格式字符串,如:"2016-10-05 13:11:12"/// - Returns: DispatchWallTime 時間戳static func getWallTime(from dateString: String) -> DispatchWallTime? {let dateformatter = DateFormatter()dateformatter.dateFormat = "YYYY-MM-dd HH:mm:ss"dateformatter.timeZone = TimeZone(secondsFromGMT: 0)var newDate = dateformatter.date(from: dateString)guard let timeInterval = newDate?.timeIntervalSince1970 else {return nil}var time = timespec(tv_sec: __darwin_time_t(timeInterval), tv_nsec: 0)return DispatchWallTime(timespec: time)} } 復制代碼下面通過字符串即可創(chuàng)建時間戳:
let time = Date.getWallTime(from: "2018-03-08 13:30:00") 復制代碼timespec
這是 Darwin 內(nèi)核中的一個結構體,用于表示一個絕對時間點,它描述的是從格林威治時間 1970年1月1日零點 開始指定時間間隔后的時間點,精確到納秒,結構如下:
struct timespec {__darwin_time_t tv_sec; // 表示時間的秒數(shù)long tv_nsec; // 表示時間的1秒內(nèi)的部分(相當于小數(shù)部分),以納秒為 1 個單位計數(shù)。 };let time = timespec(tv_sec: __darwin_time_t(86400), tv_nsec: 10) // 表示 1970-1-2 號第 10 納秒 復制代碼timeval
這是 Darwin 內(nèi)核中的一個結構體,也用于表示一個絕對時間點,它描述的是從格林威治時間 1970年1月1日零點 開始指定時間間隔后的時間點,精確到微秒,結構如下:
struct timeval {__darwin_time_t tv_sec; // 表示時間的秒數(shù)__darwin_suseconds_t tv_usec; // 表示時間的1秒內(nèi)的部分(相當于小數(shù)部分),以微秒為 1 個單位計數(shù)。 }; 復制代碼gettimeofday
這是 Unix 系統(tǒng)中的一個獲取當前時間的方法,它接收兩個指針參數(shù),執(zhí)行后將修改指針對應的結構體值,一個參數(shù)為 timeval 類型的時間結構體指針,另一個為時區(qū)結構體指針(時區(qū)在此方法中已不再使用,設為 nil 即可)。方法返回 0 時表示獲取成功,返回 -1 時表示獲取失敗:
var getTimeval = timeval() // 原始時間 let time = gettimeofday(&getTimeval, nil) // 再次讀取 getTimeval 即為當前時間 復制代碼問答習題
最后留下幾個問題給大家思考。
隊列與任務特性
擴展閱讀
源碼
官方 GCD Swift 源碼
官方 Operation Swift 源碼 (推薦看一下,更易懂好用的 Operation 類原來封裝起來這么簡單。)
鳴謝
本教程在撰寫過程中,參考或從以下文章中獲得靈感,感謝以下文章及作者的幫助:
- 行走的少年郎: iOS多線程:『GCD』詳盡總結
- onevcat: Swifter - Swift 開發(fā)者必備 Tips - GCD 和延時調(diào)用
- nekno: Large file copy with GCD - Dispatch IO consumes large amounts of memory
- THE GRAND CENTRAL DISPATCH: SOURCES - EXAMPLES (TIMER)
- 戴銘: 細說GCD(Grand Central Dispatch)如何用
- bestswifter: 深入了解GCD
- 酸菜Amour:
- GCD源碼的分析
- dispatch_sync 的分析
- dispatch_async 的分析
- Sindri Lin: 奇怪的GCD
- GABRIEL THEODOROPOULOS(譯者:小鍋): Swift 3 中的 GCD 與 Dispatch Queue
- Florian Kugler(譯者:破船): 并發(fā)編程:API 及挑戰(zhàn)
- Mike Ash: Intro to Grand Central Dispatch, Part IV: Odds and Ends
- Dave Rahardja: GCD Target Queues
歡迎訪問 我的個人網(wǎng)站 ,文章。
題圖:Mission San Xavier del Bac - Matt Artz @unsplash
《新程序員》:云原生和全面數(shù)字化實踐50位技術專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的iOS Swift GCD 开发教程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2D平台游戏王牌英雄的AI寻路解决方案
- 下一篇: [网络流24题]最小路径覆盖问题