久久精品国产精品国产精品污,男人扒开添女人下部免费视频,一级国产69式性姿势免费视频,夜鲁夜鲁很鲁在线视频 视频,欧美丰满少妇一区二区三区,国产偷国产偷亚洲高清人乐享,中文 在线 日韩 亚洲 欧美,熟妇人妻无乱码中文字幕真矢织江,一区二区三区人妻制服国产

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

iOS Swift GCD 开发教程

發(fā)布時間:2025/6/15 编程问答 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 iOS Swift GCD 开发教程 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.



本教程將帶你詳細了解 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í)行需要時間和相應的順序,耗時有長短,順序有先后,任務只有按照正確的時間和順序進行編排,應用才能按照你的預期運行。我們舉音樂播放的例子來看看關于任務有哪些需求。

  • 默認情況下,程序是按代碼順序執(zhí)行的,但我們有時希望應用能同時做多件事情,比如同時下載歌詞和音樂。這就有了第一個需求:讓多個任務同時進行。
  • 對于下載這個任務,可以一次下載多首音樂,各下各的,不需要互相等待;然而當全部下載完了播放時,通常是一首接一首的播放,播放一首音樂這個任務是需要等待前面的播放任務完成了才能進行。這就有了第二個需求:有的任務需要等待它完成了才能進行下一個任務,有的任務不需要等待它完成。
  • 如果一首音樂還沒下載,我們就點了播放鍵,我們看看需要做哪些事情:它需要把歌詞、音樂分別下載了,等他們都下載完了,告訴應用你可以播放了,然后應用把歌詞、音樂同時播放。那我們怎么知道歌詞、音樂都下載完了呢?這就有了第三個需求:如果有個東西能把幾個任務捆綁到一起就好了,當整個包都完成了再通知我。
  • 還是下載,如果我們勾選了一堆的音樂要下載,中間我想暫停一下,過一會再讓它繼續(xù),這就要求這一系列的下載任務要可以暫停和繼續(xù)。
  • 一般下載工具都可以設置同時最大下載數(shù),這就要求有一個方法可以控制同時進行的任務數(shù)。
  • 很多播放器會有一個功能:播放 20 分鐘后就停止,非常適合睡覺前用。這個時候需要有個任務,在 20 分鐘后把音樂關了。延遲執(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 個例子,來說明同步任務的特性。

  • 隊列中如果沒有任務在執(zhí)行,那么提交同步任務后,將立即執(zhí)行該任務,并阻塞線程 thread0,任務完成后再恢復線程 thread0 中被阻塞的任務。
  • 如果串行隊列中有任務在執(zhí)行,如果該任務又向該隊列提交了一個同步任務,將會立即發(fā)生死鎖。
  • 如果并行隊列中有任務在執(zhí)行,如果該任務又向該隊列提交了一個同步任務,那么當前線程會轉而執(zhí)行新的同步任務,結束后再回到原任務。
  • 如果隊列中有任務在執(zhí)行,如果該任務向另一個隊列提交了一個同步任務,那么當前線程會轉而執(zhí)行新的同步任務,結束后再回到原任務。
  • 看例子前先介紹兩個輔助方法:

    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,再添加至隊列中:
    let queue = DispatchQueue(label: "queueName", attributes: .concurrent) let task = DispatchWorkItem(flags: .barrier) {// do something } queue.async(execute: task) queue.sync(execute: task) // 與 async 效果一樣 復制代碼

    下面看看柵欄任務的例子:

    代碼示例

    示例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í)行前臨時取消任務。@IBAction 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 {@IBAction 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 {// 掛起@IBAction func suspendButtonTapped(_ sender: Any) {suspendAndResum.suspendQueue()}// 喚醒@IBAction 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() 復制代碼

    將任務加入到任務組中

    有兩種方式加入任務組:

    • 添加任務時指定任務組
    let queue = DispatchQueue.global() queue.async(group: queueGroup) {print("喝一杯牛奶") } 復制代碼
    • 使用 Group.enter()、 Group.leave() 配對方法,標識任務加入任務組。
    queueGroup.enter() queue.async {print("吃一個蘋果")queueGroup.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í)行):
    let group = DispatchGroup()let queueBook = DispatchQueue(label: "book") queueBook.async(group: group) {// do something 1 } let queueVideo = DispatchQueue(label: "video") queueVideo.async(group: group) {// do something 2 }group.notify(queue: DispatchQueue.main) {print("all task done") }print("do something else.")// 執(zhí)行結果 // do something else. // do something 1(任務 1、2 完成順序不固定) // do something 2 // all task done 復制代碼
    • group.wait 方法,它會在所有任務完成后再執(zhí)行當前線程中后續(xù)的代碼,因此這個操作是起到阻塞的作用:
    let group = DispatchGroup()let queueBook = DispatchQueue(label: "book") queueBook.async(group: group) {// do something 1 } let queueVideo = DispatchQueue(label: "video") queueVideo.async(group: group) {// do something 2 }group.wait()print("do something else.")// 執(zhí)行結果 // do something 1(任務 1、2 完成順序不固定) // do something 2 // do something else. 復制代碼

    wait 方法中還可以指定具體的時間,它表示將等待不超過這個時間,如果任務組在指定時間之內(nèi)完成則立即恢復當前線程,否則將等到時間結束時再恢復當前線程。

    • 方式1,使用 DispatchTime,它表示一個時間間隔,精確到納秒(1/1000,000,000 秒):
    let waitTime = DispatchTime.now() + 2.0 // 表示從當前時間開始后 2 秒,數(shù)字字面量也可以改為使用 TimeInterval 類型變量 group.wait(timeout: waitTime) 復制代碼
    • 方式2,使用 DispatchWallTime,它表示當前的絕對時間戳,精確到微秒(1/1000,000 秒),通常使用字面量即可設置延時時間,也可以使用 timespec 結構體來設置一個精確的時間戳,具體參見附錄章節(jié)的《時間相關的結構體說明 - DispatchWallTime》:
    // 使用字面量設置 var wallTime = DispatchWallTime.now() + 2.0 // 表示從當前時間開始后 2 秒,數(shù)字字面量也可以改為使用 TimeInterval 類型變量 復制代碼

    代碼示例

    本章對應的代碼見示例工程中 QueueTestListTableViewController+DispatchGroup.swift, DispatchGroup.swift.

    示例 7.1:創(chuàng)建任務組,并以常規(guī)方式添加任務。

    示例中我們通過一個按鈕觸發(fā),創(chuàng)建一個任務組、通過常規(guī)方式添加任務、任務完成時通知主線程。

    extension QueueTestListTableViewController {@IBAction 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 {@IBAction 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 {@IBAction 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() 方法開始接收事件
    class DispatchSourceTest {var filePath: Stringvar counter = 0let queue = DispatchQueue.global()init() {filePath = "\(NSTemporaryDirectory())"startObserve {print("File was changed")}}func startObserve(closure: @escaping () -> Void) {let fileURL = URL(fileURLWithPath: filePath)let monitoredDirectoryFileDescriptor = open(fileURL.path, O_EVTONLY)let source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredDirectoryFileDescriptor,eventMask: .write, queue: queue)source.setEventHandler(handler: closure)source.setCancelHandler {close(monitoredDirectoryFileDescriptor)}source.resume()}func changeFile() {DispatchSourceTest.createFile(name: "DispatchSourceTest.md", filePath: NSTemporaryDirectory())counter += 1let text = "\(counter)"try! text.write(toFile: "\(filePath)/DispatchSourceTest.md", atomically: true, encoding: String.Encoding.utf8)print("file writed.")}static func createFile(name: String, filePath: String){let manager = FileManager.defaultlet fileBaseUrl = URL(fileURLWithPath: filePath)let file = fileBaseUrl.appendingPathComponent(name)print("文件: \(file)")// 寫入 "hello world"let exist = manager.fileExists(atPath: file.path)if !exist {let data = Data(base64Encoded:"aGVsbG8gd29ybGQ=" ,options:.ignoreUnknownCharacters)let createSuccess = manager.createFile(atPath: file.path,contents:data,attributes:nil)print("文件創(chuàng)建結果: \(createSuccess)")}} } 復制代碼

    在 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

    Flag備注功能
    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 三個組對應的權限掩碼。

    UserGroupOther說明
    S_IRWXUS_IRWXGS_IRWXO可讀、可寫、可執(zhí)行
    S_IRUSRS_IRGRPS_IROTH可讀
    S_IWUSRS_IWGRS_IWOTH可寫
    S_IXUSRS_IXGRPS_IXOTH可執(zhí)行

    創(chuàng)建的通道有兩種類型:

    • 連續(xù)數(shù)據(jù)流:DispatchIO.StreamType.stream,這個方式是對文件從頭到尾完整操作的。
    • 隨機片段數(shù)據(jù):DispatchIO.StreamType.random,這個方式是在文件的任意一個位置(偏移量)開始操作的。
    let filePath: NSString = "test.zip" // 創(chuàng)建一個可讀寫的文件描述符 let fileDescriptor = open(filePath.utf8String!, (O_RDWR | O_CREAT | O_APPEND), (S_IRWXU | S_IRWXG)) let queue = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.serialQueue") let cleanupHandler: (Int32) -> Void = { errorNumber in } let io = DispatchIO(type: .stream, fileDescriptor: fileDescriptor, queue: queue, cleanupHandler: cleanupHandler) 復制代碼

    文件路徑方式

    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:任務對象測試。

    /// 任務對象測試 @IBAction 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)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。

    丰满妇女强制高潮18xxxx | 国产小呦泬泬99精品 | 国产亚洲精品久久久久久国模美 | 精品久久8x国产免费观看 | 亚洲精品成人福利网站 | 亚洲精品一区二区三区婷婷月 | 日本www一道久久久免费榴莲 | 又色又爽又黄的美女裸体网站 | 成人免费视频视频在线观看 免费 | 久久久久99精品成人片 | 国产女主播喷水视频在线观看 | 成年女人永久免费看片 | 午夜性刺激在线视频免费 | 久久人妻内射无码一区三区 | 国产免费久久久久久无码 | 免费国产黄网站在线观看 | 精品国产一区二区三区四区在线看 | 亚洲成a人片在线观看无码3d | 国产九九九九九九九a片 | 领导边摸边吃奶边做爽在线观看 | 日本熟妇大屁股人妻 | 国产亚洲人成在线播放 | 欧美freesex黑人又粗又大 | 国产精品久久久一区二区三区 | 国产99久久精品一区二区 | 欧美丰满少妇xxxx性 | 图片区 小说区 区 亚洲五月 | 老子影院午夜伦不卡 | 好爽又高潮了毛片免费下载 | 亚洲精品国偷拍自产在线麻豆 | 牛和人交xxxx欧美 | 日韩精品a片一区二区三区妖精 | 国产超碰人人爽人人做人人添 | 色综合久久久久综合一本到桃花网 | 国产精华av午夜在线观看 | 日日噜噜噜噜夜夜爽亚洲精品 | 巨爆乳无码视频在线观看 | 性欧美疯狂xxxxbbbb | 图片区 小说区 区 亚洲五月 | 久久99精品久久久久久 | 亚洲 欧美 激情 小说 另类 | 成人精品视频一区二区 | 永久免费精品精品永久-夜色 | 国精品人妻无码一区二区三区蜜柚 | 蜜臀aⅴ国产精品久久久国产老师 | 黑人巨大精品欧美一区二区 | 亚洲欧美精品伊人久久 | 日韩精品无码免费一区二区三区 | 午夜精品一区二区三区在线观看 | 国产人妖乱国产精品人妖 | 亚洲色www成人永久网址 | 国产色视频一区二区三区 | 国产国产精品人在线视 | 成人女人看片免费视频放人 | 97资源共享在线视频 | 中文字幕亚洲情99在线 | 老熟妇仑乱视频一区二区 | 国产亚洲精品久久久闺蜜 | 国产人妻人伦精品1国产丝袜 | 精品人妻人人做人人爽 | www国产亚洲精品久久网站 | 初尝人妻少妇中文字幕 | 欧洲熟妇精品视频 | 一本色道久久综合狠狠躁 | 一二三四社区在线中文视频 | 亚洲欧美色中文字幕在线 | 欧美真人作爱免费视频 | 荡女精品导航 | 久久精品国产一区二区三区肥胖 | 中文字幕人妻无码一区二区三区 | 亚洲性无码av中文字幕 | 亚洲а∨天堂久久精品2021 | 中国女人内谢69xxxxxa片 | 亚洲狠狠色丁香婷婷综合 | 99精品无人区乱码1区2区3区 | 国产精品久久久av久久久 | 极品尤物被啪到呻吟喷水 | 国产在线无码精品电影网 | 欧美午夜特黄aaaaaa片 | 好屌草这里只有精品 | 欧美freesex黑人又粗又大 | 波多野结衣aⅴ在线 | 女人色极品影院 | 精品一区二区不卡无码av | 九九久久精品国产免费看小说 | 国产精品va在线播放 | 成人综合网亚洲伊人 | 乱人伦中文视频在线观看 | 老子影院午夜精品无码 | 女人和拘做爰正片视频 | 久久综合狠狠综合久久综合88 | 成人无码影片精品久久久 | 国产三级久久久精品麻豆三级 | 亚洲а∨天堂久久精品2021 | 久久亚洲精品成人无码 | 国产无套内射久久久国产 | 久久99精品久久久久久 | 欧美日韩一区二区三区自拍 | 久久人人爽人人爽人人片ⅴ | 亚洲成a人片在线观看日本 | 人人妻人人藻人人爽欧美一区 | 亚洲伊人久久精品影院 | 久激情内射婷内射蜜桃人妖 | 欧美三级不卡在线观看 | 国产成人无码a区在线观看视频app | 久久99精品久久久久久 | 亚洲熟女一区二区三区 | 无码帝国www无码专区色综合 | 永久免费观看美女裸体的网站 | 欧美精品在线观看 | 国产精品无码久久av | 一本久久a久久精品亚洲 | 任你躁在线精品免费 | 欧洲熟妇色 欧美 | 无码一区二区三区在线 | 日本乱偷人妻中文字幕 | 亚洲男人av香蕉爽爽爽爽 | 妺妺窝人体色www婷婷 | 在线亚洲高清揄拍自拍一品区 | 日本免费一区二区三区最新 | 午夜肉伦伦影院 | 日日摸日日碰夜夜爽av | 东京热一精品无码av | 少妇邻居内射在线 | 欧洲熟妇精品视频 | 国产 浪潮av性色四虎 | 99久久99久久免费精品蜜桃 | 亚洲中文字幕无码中字 | 一本无码人妻在中文字幕免费 | 日本爽爽爽爽爽爽在线观看免 | 国产激情艳情在线看视频 | 最近的中文字幕在线看视频 | 性色欲网站人妻丰满中文久久不卡 | 成人无码视频免费播放 | 亚洲国产精品无码一区二区三区 | 亚洲熟悉妇女xxx妇女av | 亚洲中文字幕成人无码 | 日韩精品久久久肉伦网站 | 亚洲精品一区国产 | 沈阳熟女露脸对白视频 | 亚洲自偷自偷在线制服 | 麻豆精产国品 | 人人妻人人藻人人爽欧美一区 | 欧美第一黄网免费网站 | 天堂а√在线中文在线 | 国产亚洲精品精品国产亚洲综合 | 丝袜人妻一区二区三区 | 欧美精品免费观看二区 | 中文字幕无码免费久久9一区9 | 精品aⅴ一区二区三区 | 帮老师解开蕾丝奶罩吸乳网站 | 午夜福利试看120秒体验区 | 国产人妻精品午夜福利免费 | 久久97精品久久久久久久不卡 | 国产香蕉尹人综合在线观看 | 色综合视频一区二区三区 | 亚洲成a人片在线观看日本 | 强开小婷嫩苞又嫩又紧视频 | 国产色xx群视频射精 | 国产又爽又猛又粗的视频a片 | 曰本女人与公拘交酡免费视频 | 国产精品香蕉在线观看 | 欧洲熟妇精品视频 | 熟女少妇人妻中文字幕 | 国产内射爽爽大片视频社区在线 | 少妇性荡欲午夜性开放视频剧场 | 中文字幕无码av激情不卡 | 麻豆md0077饥渴少妇 | 99精品久久毛片a片 | 亚拍精品一区二区三区探花 | 国产乱人无码伦av在线a | 漂亮人妻洗澡被公强 日日躁 | 国产麻豆精品一区二区三区v视界 | 99精品无人区乱码1区2区3区 | 日本高清一区免费中文视频 | 99视频精品全部免费免费观看 | 久久久无码中文字幕久... | 天堂а√在线中文在线 | 蜜桃av抽搐高潮一区二区 | 亚洲国产综合无码一区 | 国产精品久久久一区二区三区 | 亚洲伊人久久精品影院 | 亚洲色偷偷男人的天堂 | 综合激情五月综合激情五月激情1 | 亚洲欧美精品aaaaaa片 | 无码吃奶揉捏奶头高潮视频 | 狠狠色噜噜狠狠狠7777奇米 | 少妇高潮喷潮久久久影院 | 色五月丁香五月综合五月 | 欧美兽交xxxx×视频 | 婷婷丁香五月天综合东京热 | √天堂资源地址中文在线 | 成人精品一区二区三区中文字幕 | 午夜成人1000部免费视频 | 亚洲色欲色欲欲www在线 | 在线欧美精品一区二区三区 | 精品人人妻人人澡人人爽人人 | 日韩av无码一区二区三区不卡 | 天天做天天爱天天爽综合网 | 亚洲国产精品久久久久久 | 香蕉久久久久久av成人 | 少妇激情av一区二区 | 天堂亚洲免费视频 | 又粗又大又硬又长又爽 | 两性色午夜视频免费播放 | 久久久久久久人妻无码中文字幕爆 | 色婷婷av一区二区三区之红樱桃 | 亚洲色偷偷偷综合网 | 亚洲欧美日韩国产精品一区二区 | 小sao货水好多真紧h无码视频 | 无码播放一区二区三区 | 精品熟女少妇av免费观看 | 97人妻精品一区二区三区 | 国产成人精品必看 | 久久久久成人片免费观看蜜芽 | 欧美zoozzooz性欧美 | 精品国产一区av天美传媒 | 成人影院yy111111在线观看 | 亚洲国产欧美国产综合一区 | 国产明星裸体无码xxxx视频 | 四虎国产精品一区二区 | 亚洲国产精品毛片av不卡在线 | 国产莉萝无码av在线播放 | 荫蒂添的好舒服视频囗交 | 欧美猛少妇色xxxxx | 日日碰狠狠丁香久燥 | 婷婷丁香五月天综合东京热 | 久久久久成人精品免费播放动漫 | 亚洲 a v无 码免 费 成 人 a v | 中文字幕无码日韩欧毛 | 国产熟妇高潮叫床视频播放 | 成熟人妻av无码专区 | 国语精品一区二区三区 | 午夜性刺激在线视频免费 | 精品亚洲成av人在线观看 | 大屁股大乳丰满人妻 | 亚洲人成人无码网www国产 | 狂野欧美性猛xxxx乱大交 | 国产人妖乱国产精品人妖 | 色一情一乱一伦 | www国产亚洲精品久久久日本 | 亚洲国产欧美在线成人 | 中文字幕久久久久人妻 | 中文字幕无码免费久久99 | 亚洲国产欧美国产综合一区 | 国产乱人无码伦av在线a | 伊人久久大香线蕉午夜 | 亚洲aⅴ无码成人网站国产app | 亚洲国产欧美国产综合一区 | 欧美成人高清在线播放 | 国产特级毛片aaaaaaa高清 | 红桃av一区二区三区在线无码av | 久久精品人人做人人综合试看 | 国内精品一区二区三区不卡 | 日韩欧美中文字幕公布 | 久久久久久久久888 | 亚洲精品一区二区三区四区五区 | 国内精品人妻无码久久久影院 | 久久亚洲精品中文字幕无男同 | 欧美人与物videos另类 | 精品成在人线av无码免费看 | 麻花豆传媒剧国产免费mv在线 | 国产精品va在线播放 | 久久国产精品萌白酱免费 | 日本护士毛茸茸高潮 | 久久久精品欧美一区二区免费 | 国产精品无码一区二区三区不卡 | 真人与拘做受免费视频一 | 人妻少妇精品视频专区 | 国产一区二区三区影院 | 国产熟妇高潮叫床视频播放 | 国产无套粉嫩白浆在线 | 久久99国产综合精品 | 国产精品久久久久久久9999 | 性生交大片免费看女人按摩摩 | av在线亚洲欧洲日产一区二区 | 无码国产色欲xxxxx视频 | 亚无码乱人伦一区二区 | 欧美日本精品一区二区三区 | 人妻夜夜爽天天爽三区 | 国产精品人人妻人人爽 | 亚洲熟妇色xxxxx欧美老妇 | 精品久久久久久亚洲精品 | 高潮毛片无遮挡高清免费视频 | 2020久久香蕉国产线看观看 | 国产成人精品必看 | 国产在热线精品视频 | 亚洲色在线无码国产精品不卡 | 亚洲中文字幕av在天堂 | 欧美一区二区三区 | 成人无码精品1区2区3区免费看 | 成人无码精品1区2区3区免费看 | 最新版天堂资源中文官网 | 日日天干夜夜狠狠爱 | 成人亚洲精品久久久久软件 | 思思久久99热只有频精品66 | 国产在热线精品视频 | 色一情一乱一伦一视频免费看 | 午夜精品一区二区三区的区别 | 国产成人无码午夜视频在线观看 | 成人免费视频在线观看 | 精品国精品国产自在久国产87 | aⅴ亚洲 日韩 色 图网站 播放 | 成人精品一区二区三区中文字幕 | 窝窝午夜理论片影院 | 亚洲中文字幕无码一久久区 | 人人爽人人澡人人高潮 | 98国产精品综合一区二区三区 | 欧美日韩久久久精品a片 | 在线观看国产午夜福利片 | 亚洲大尺度无码无码专区 | 成人欧美一区二区三区黑人免费 | 少妇性l交大片欧洲热妇乱xxx | 精品厕所偷拍各类美女tp嘘嘘 | 无码人妻精品一区二区三区下载 | 在线视频网站www色 | 国产内射爽爽大片视频社区在线 | 色 综合 欧美 亚洲 国产 | 色一情一乱一伦一区二区三欧美 | 2020最新国产自产精品 | 亚洲色大成网站www国产 | 一二三四在线观看免费视频 | 麻豆成人精品国产免费 | 欧美刺激性大交 | 亚洲中文字幕在线观看 | 97精品人妻一区二区三区香蕉 | 国产成人综合在线女婷五月99播放 | 日本爽爽爽爽爽爽在线观看免 | 青青久在线视频免费观看 | 欧美人与物videos另类 | 在线亚洲高清揄拍自拍一品区 | 人人澡人人透人人爽 | 亚洲成a人一区二区三区 | 西西人体www44rt大胆高清 | 欧洲熟妇精品视频 | 国产免费久久久久久无码 | 丰满少妇高潮惨叫视频 | 亚洲国产精品一区二区第一页 | 国产国语老龄妇女a片 | 日韩av无码中文无码电影 | 99er热精品视频 | 欧美自拍另类欧美综合图片区 | 色爱情人网站 | 免费观看的无遮挡av | 四十如虎的丰满熟妇啪啪 | 精品无人国产偷自产在线 | 人人妻人人澡人人爽欧美精品 | 亚洲va中文字幕无码久久不卡 | 欧美性猛交xxxx富婆 | 天天拍夜夜添久久精品大 | 激情国产av做激情国产爱 | 欧美精品无码一区二区三区 | 国产精品二区一区二区aⅴ污介绍 | av人摸人人人澡人人超碰下载 | 精品国产一区二区三区av 性色 | 国语自产偷拍精品视频偷 | 伊人久久大香线焦av综合影院 | 成人欧美一区二区三区黑人免费 | 久久精品一区二区三区四区 | 亚洲无人区午夜福利码高清完整版 | 又色又爽又黄的美女裸体网站 | 在线视频网站www色 | 国产一精品一av一免费 | 国产精品国产自线拍免费软件 | 色 综合 欧美 亚洲 国产 | 一本无码人妻在中文字幕免费 | 久久人人爽人人爽人人片ⅴ | 日产精品99久久久久久 | 欧美性猛交内射兽交老熟妇 | 成人毛片一区二区 | 婷婷五月综合缴情在线视频 | 色五月五月丁香亚洲综合网 | 国产偷抇久久精品a片69 | 日日鲁鲁鲁夜夜爽爽狠狠 | 国产精品无码一区二区三区不卡 | 啦啦啦www在线观看免费视频 | 国产乡下妇女做爰 | 最新版天堂资源中文官网 | 中文字幕av日韩精品一区二区 | 男人的天堂av网站 | 国产精品无码永久免费888 | 性欧美大战久久久久久久 | 国产精品无码永久免费888 | 一本久久a久久精品亚洲 | 无套内谢老熟女 | 真人与拘做受免费视频 | 亚洲欧美日韩成人高清在线一区 | 波多野结衣aⅴ在线 | 夜夜高潮次次欢爽av女 | 亚洲精品一区二区三区在线观看 | 小sao货水好多真紧h无码视频 | 夫妻免费无码v看片 | 呦交小u女精品视频 | 国产在线精品一区二区三区直播 | 2019nv天堂香蕉在线观看 | 最新版天堂资源中文官网 | 亚洲国产av美女网站 | 永久免费观看国产裸体美女 | 最新国产麻豆aⅴ精品无码 | 丰满岳乱妇在线观看中字无码 | 男女作爱免费网站 | 久久99精品久久久久久 | 图片区 小说区 区 亚洲五月 | 国产福利视频一区二区 | 日本免费一区二区三区最新 | 成人三级无码视频在线观看 | 国产熟妇另类久久久久 | 国产免费无码一区二区视频 | 999久久久国产精品消防器材 | 99久久精品国产一区二区蜜芽 | 成人女人看片免费视频放人 | 欧美精品无码一区二区三区 | 国产av剧情md精品麻豆 | 蜜臀aⅴ国产精品久久久国产老师 | 无码国产色欲xxxxx视频 | 国产黑色丝袜在线播放 | 亚洲а∨天堂久久精品2021 | 亚洲啪av永久无码精品放毛片 | 波多野结衣乳巨码无在线观看 | 无码人妻av免费一区二区三区 | 国产猛烈高潮尖叫视频免费 | 国产片av国语在线观看 | 欧美人与动性行为视频 | 久9re热视频这里只有精品 | 牲欲强的熟妇农村老妇女 | 麻豆国产人妻欲求不满 | √天堂中文官网8在线 | 少妇高潮喷潮久久久影院 | 女人高潮内射99精品 | 99久久精品无码一区二区毛片 | 1000部夫妻午夜免费 | 18无码粉嫩小泬无套在线观看 | 色婷婷久久一区二区三区麻豆 | 国产sm调教视频在线观看 | 激情五月综合色婷婷一区二区 | 无码一区二区三区在线 | 免费人成网站视频在线观看 | 欧洲vodafone精品性 | 中文久久乱码一区二区 | 激情五月综合色婷婷一区二区 | 久久综合网欧美色妞网 | 久久亚洲中文字幕精品一区 | 久久久成人毛片无码 | 大色综合色综合网站 | 丝袜 中出 制服 人妻 美腿 | 色综合久久久无码中文字幕 | 亚洲爆乳精品无码一区二区三区 | 久久99精品国产.久久久久 | 无码一区二区三区在线观看 | 人妻aⅴ无码一区二区三区 | 乱中年女人伦av三区 | 女人被男人躁得好爽免费视频 | 国内精品人妻无码久久久影院 | 人妻少妇精品无码专区二区 | 天堂а√在线地址中文在线 | 欧美肥老太牲交大战 | 久久精品女人天堂av免费观看 | 国产猛烈高潮尖叫视频免费 | 国产深夜福利视频在线 | 婷婷五月综合激情中文字幕 | 久久国语露脸国产精品电影 | 99精品国产综合久久久久五月天 | 未满成年国产在线观看 | 精品国产青草久久久久福利 | 亚洲娇小与黑人巨大交 | 99久久婷婷国产综合精品青草免费 | 国产高潮视频在线观看 | 国产精华av午夜在线观看 | 日欧一片内射va在线影院 | 蜜桃视频韩日免费播放 | 亚洲人成网站在线播放942 | 天天拍夜夜添久久精品大 | 午夜精品久久久内射近拍高清 | 欧美乱妇无乱码大黄a片 | 免费观看又污又黄的网站 | 日日噜噜噜噜夜夜爽亚洲精品 | 丝袜足控一区二区三区 | 强辱丰满人妻hd中文字幕 | 欧美 日韩 亚洲 在线 | 人人妻人人澡人人爽人人精品浪潮 | 国产熟女一区二区三区四区五区 | 精品欧美一区二区三区久久久 | 国产三级久久久精品麻豆三级 | 又大又硬又爽免费视频 | 国精品人妻无码一区二区三区蜜柚 | 色妞www精品免费视频 | 东京无码熟妇人妻av在线网址 | 久久99精品久久久久婷婷 | 日韩精品成人一区二区三区 | 久久婷婷五月综合色国产香蕉 | 欧洲精品码一区二区三区免费看 | 中文字幕av日韩精品一区二区 | 亚洲男人av天堂午夜在 | 97久久精品无码一区二区 | 天堂无码人妻精品一区二区三区 | 狠狠噜狠狠狠狠丁香五月 | 国产偷自视频区视频 | 美女黄网站人色视频免费国产 | 波多野结衣高清一区二区三区 | 亚洲无人区午夜福利码高清完整版 | 国产精品无码一区二区三区不卡 | 久久天天躁狠狠躁夜夜免费观看 | 少妇邻居内射在线 | 国产成人精品一区二区在线小狼 | 搡女人真爽免费视频大全 | 国产亚洲精品精品国产亚洲综合 | 97夜夜澡人人双人人人喊 | 久青草影院在线观看国产 | 久久99精品久久久久久动态图 | 国产农村乱对白刺激视频 | 97人妻精品一区二区三区 | 97人妻精品一区二区三区 | 亚洲色在线无码国产精品不卡 | 老熟妇仑乱视频一区二区 | 无遮挡啪啪摇乳动态图 | 两性色午夜视频免费播放 | 最新国产乱人伦偷精品免费网站 | 亚洲国产一区二区三区在线观看 | 中文字幕日韩精品一区二区三区 | 性色av无码免费一区二区三区 | 国产激情无码一区二区app | 国产麻豆精品一区二区三区v视界 | 国产免费观看黄av片 | 亚洲一区二区三区在线观看网站 | 麻豆人妻少妇精品无码专区 | 日韩视频 中文字幕 视频一区 | 国产精品久久久久无码av色戒 | 亚洲熟妇色xxxxx亚洲 | 强伦人妻一区二区三区视频18 | 小泽玛莉亚一区二区视频在线 | 最近的中文字幕在线看视频 | 性欧美牲交在线视频 | 亚洲爆乳大丰满无码专区 | 秋霞成人午夜鲁丝一区二区三区 | 色综合天天综合狠狠爱 | 亚洲人交乣女bbw | 国产人妻精品一区二区三区不卡 | 牛和人交xxxx欧美 | 熟妇人妻中文av无码 | 国产一区二区三区影院 | 久久久久久久人妻无码中文字幕爆 | 日本一区二区三区免费高清 | 丝袜足控一区二区三区 | 免费国产成人高清在线观看网站 | 日韩人妻少妇一区二区三区 | 日本大香伊一区二区三区 | 亚洲色在线无码国产精品不卡 | 在线播放无码字幕亚洲 | 午夜时刻免费入口 | www国产亚洲精品久久网站 | 正在播放东北夫妻内射 | 老太婆性杂交欧美肥老太 | 99久久精品午夜一区二区 | 欧美性黑人极品hd | 99精品视频在线观看免费 | 日韩人妻无码一区二区三区久久99 | 亚洲成av人综合在线观看 | 欧美成人午夜精品久久久 | 欧美日韩在线亚洲综合国产人 | 5858s亚洲色大成网站www | 色欲综合久久中文字幕网 | 国产女主播喷水视频在线观看 | 日韩av无码一区二区三区不卡 | 伊在人天堂亚洲香蕉精品区 | 成人精品视频一区二区 | 夜夜夜高潮夜夜爽夜夜爰爰 | 久久午夜夜伦鲁鲁片无码免费 | 波多野结衣aⅴ在线 | 国产尤物精品视频 | 女人色极品影院 | 国产精品第一区揄拍无码 | 国产成人综合色在线观看网站 | www国产精品内射老师 | 中文字幕av日韩精品一区二区 | 天天躁日日躁狠狠躁免费麻豆 | 久久久国产一区二区三区 | 成人免费视频一区二区 | 中文无码成人免费视频在线观看 | 久久国产精品精品国产色婷婷 | 亚洲人亚洲人成电影网站色 | 久久www免费人成人片 | 欧美刺激性大交 | 98国产精品综合一区二区三区 | 又黄又爽又色的视频 | 一本精品99久久精品77 | 久久久久人妻一区精品色欧美 | 国产后入清纯学生妹 | 久久天天躁狠狠躁夜夜免费观看 | 兔费看少妇性l交大片免费 | 成人片黄网站色大片免费观看 | 日韩人妻少妇一区二区三区 | 精品一二三区久久aaa片 | 秋霞成人午夜鲁丝一区二区三区 | 夜夜躁日日躁狠狠久久av | 免费观看激色视频网站 | 日韩精品乱码av一区二区 | 无码午夜成人1000部免费视频 | 中文字幕无码av波多野吉衣 | 麻豆av传媒蜜桃天美传媒 | 秋霞特色aa大片 | 欧洲vodafone精品性 | 老司机亚洲精品影院 | 亚洲中文无码av永久不收费 | 老子影院午夜精品无码 | 精品无人国产偷自产在线 | 国产亚洲精品精品国产亚洲综合 | 中文字幕中文有码在线 | ass日本丰满熟妇pics | 天天躁日日躁狠狠躁免费麻豆 | 久热国产vs视频在线观看 | 欧洲极品少妇 | 欧美真人作爱免费视频 | 欧美国产日韩久久mv | 无码精品国产va在线观看dvd | 黑人玩弄人妻中文在线 | 人人超人人超碰超国产 | 国产一区二区不卡老阿姨 | 国产精品无码一区二区桃花视频 | 噜噜噜亚洲色成人网站 | 中国女人内谢69xxxx | 2019nv天堂香蕉在线观看 | 大屁股大乳丰满人妻 | 亚洲欧美日韩综合久久久 | 丰满少妇熟乱xxxxx视频 | 精品久久久久久人妻无码中文字幕 | 爆乳一区二区三区无码 | 精品少妇爆乳无码av无码专区 | 理论片87福利理论电影 | 国产亚洲精品久久久久久大师 | 亚洲精品久久久久久久久久久 | 在线观看免费人成视频 | 图片小说视频一区二区 | 亚洲综合另类小说色区 | 内射巨臀欧美在线视频 | 久久精品国产一区二区三区肥胖 | 亚洲国产精品久久久天堂 | 成人免费视频视频在线观看 免费 | 亚洲区欧美区综合区自拍区 | 大乳丰满人妻中文字幕日本 | 免费无码av一区二区 | 国产精品第一区揄拍无码 | 亚洲国产精品久久人人爱 | 性色欲网站人妻丰满中文久久不卡 | 人人妻人人藻人人爽欧美一区 | 蜜臀av在线观看 在线欧美精品一区二区三区 | 国产亚洲美女精品久久久2020 | 亚洲国产精品久久人人爱 | 少女韩国电视剧在线观看完整 | 亚洲人成网站免费播放 | 亚洲精品午夜国产va久久成人 | 狠狠色噜噜狠狠狠7777奇米 | 欧美野外疯狂做受xxxx高潮 | 亚洲 另类 在线 欧美 制服 | 亲嘴扒胸摸屁股激烈网站 | 夜夜高潮次次欢爽av女 | 精品无人区无码乱码毛片国产 | 国产熟妇另类久久久久 | 学生妹亚洲一区二区 | 国产九九九九九九九a片 | 国产精品美女久久久久av爽李琼 | 性生交片免费无码看人 | 国产精品a成v人在线播放 | 一本色道久久综合狠狠躁 | 亚洲综合精品香蕉久久网 | 中文无码成人免费视频在线观看 | 99久久无码一区人妻 | 永久黄网站色视频免费直播 | 97se亚洲精品一区 | 国产农村乱对白刺激视频 | 国内精品人妻无码久久久影院蜜桃 | 2020最新国产自产精品 | 男女性色大片免费网站 | 久久人人爽人人爽人人片ⅴ | 国产av人人夜夜澡人人爽麻豆 | 亚洲欧洲日本无在线码 | 国产成人综合在线女婷五月99播放 | 国产极品视觉盛宴 | 一个人看的视频www在线 | 亚洲精品一区二区三区大桥未久 | 日本一本二本三区免费 | 四虎国产精品免费久久 | 国产情侣作爱视频免费观看 | 国产精品久久久久久亚洲影视内衣 | 国产乱人偷精品人妻a片 | 伊人久久婷婷五月综合97色 | 成人无码视频免费播放 | 噜噜噜亚洲色成人网站 | 97夜夜澡人人双人人人喊 | 人妻夜夜爽天天爽三区 | 中文字幕av伊人av无码av | √8天堂资源地址中文在线 | 久久五月精品中文字幕 | 国产人成高清在线视频99最全资源 | 久久国语露脸国产精品电影 | 亚洲大尺度无码无码专区 | 中文字幕乱码人妻无码久久 | 成人无码视频免费播放 | 十八禁视频网站在线观看 | 欧洲美熟女乱又伦 | 少妇久久久久久人妻无码 | 亚洲成av人影院在线观看 | 好男人社区资源 | 欧美丰满熟妇xxxx性ppx人交 | 娇妻被黑人粗大高潮白浆 | 亚洲熟妇色xxxxx亚洲 | 狠狠噜狠狠狠狠丁香五月 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 性啪啪chinese东北女人 | 老熟妇仑乱视频一区二区 | 精品一二三区久久aaa片 | 国产精品办公室沙发 | 国产网红无码精品视频 | 精品亚洲成av人在线观看 | 欧美国产日产一区二区 | 在线视频网站www色 | 大地资源网第二页免费观看 | 国产精品久久久久久亚洲影视内衣 | 大屁股大乳丰满人妻 | 亚洲精品国产精品乱码视色 | 精品aⅴ一区二区三区 | 中文字幕无码免费久久99 | 精品国产青草久久久久福利 | 成人免费无码大片a毛片 | 精品国产福利一区二区 | 亚洲国产av美女网站 | 丝袜 中出 制服 人妻 美腿 | 国产精品高潮呻吟av久久 | 国产凸凹视频一区二区 | 久久这里只有精品视频9 | 97久久国产亚洲精品超碰热 | 亚洲日韩av片在线观看 | 亚洲中文字幕无码中文字在线 | 国精产品一区二区三区 | 中文无码精品a∨在线观看不卡 | 精品久久8x国产免费观看 | 娇妻被黑人粗大高潮白浆 | 蜜桃视频韩日免费播放 | 欧美国产日产一区二区 | 久久久中文字幕日本无吗 | 少妇久久久久久人妻无码 | 色偷偷人人澡人人爽人人模 | 六月丁香婷婷色狠狠久久 | 老司机亚洲精品影院 | 精品国产av色一区二区深夜久久 | 国产精品.xx视频.xxtv | 国产乱人伦av在线无码 | 国产在热线精品视频 | 亚洲gv猛男gv无码男同 | 丰满肥臀大屁股熟妇激情视频 | 久久久亚洲欧洲日产国码αv | 日韩精品无码免费一区二区三区 | 精品厕所偷拍各类美女tp嘘嘘 | 久久综合激激的五月天 | 亚洲国产欧美国产综合一区 | 国产av一区二区三区最新精品 | 日本大香伊一区二区三区 | 高清国产亚洲精品自在久久 | 久久久久av无码免费网 | 又大又黄又粗又爽的免费视频 | 亚洲国产精品毛片av不卡在线 | 久久无码中文字幕免费影院蜜桃 | 亚洲va中文字幕无码久久不卡 | 国产亚洲精品久久久久久大师 | 亚洲人成无码网www | 日韩人妻无码中文字幕视频 | 无人区乱码一区二区三区 | 乱码av麻豆丝袜熟女系列 | 国语精品一区二区三区 | 奇米影视7777久久精品人人爽 | 又粗又大又硬毛片免费看 | 青青草原综合久久大伊人精品 | 中国女人内谢69xxxxxa片 | 欧美日本日韩 | 日本精品人妻无码免费大全 | 国产成人精品视频ⅴa片软件竹菊 | 亚洲欧洲中文日韩av乱码 | 精品人妻人人做人人爽夜夜爽 | 男人的天堂2018无码 | 国产三级久久久精品麻豆三级 | 亚洲乱码中文字幕在线 | 国产麻豆精品精东影业av网站 | 97久久国产亚洲精品超碰热 | 动漫av一区二区在线观看 | 国产精品igao视频网 | 免费人成在线观看网站 | 日本一区二区更新不卡 | 国产激情艳情在线看视频 | 国产莉萝无码av在线播放 | 中文字幕av伊人av无码av | 国产又粗又硬又大爽黄老大爷视 | 国产精品久久久 | 久久久精品456亚洲影院 | 亚洲色欲久久久综合网东京热 | 小sao货水好多真紧h无码视频 | 国产av一区二区三区最新精品 | 一本久道久久综合婷婷五月 | 午夜熟女插插xx免费视频 | 国产日产欧产精品精品app | 亚洲国产精品一区二区美利坚 | 丰满岳乱妇在线观看中字无码 | 在线视频网站www色 | 老熟妇乱子伦牲交视频 | 熟女体下毛毛黑森林 | 精品久久8x国产免费观看 | 国产av久久久久精东av | 亚洲小说图区综合在线 | 亚洲成av人在线观看网址 | 成人av无码一区二区三区 | 无码吃奶揉捏奶头高潮视频 | 久久久精品456亚洲影院 | 久久久久久久久蜜桃 | 天堂一区人妻无码 | 国产人妻人伦精品 | 欧美zoozzooz性欧美 | 国产特级毛片aaaaaaa高清 | 成人一区二区免费视频 | 亚洲日韩中文字幕在线播放 | 国语自产偷拍精品视频偷 | 亚洲春色在线视频 | 亚洲最大成人网站 | 亚洲成av人综合在线观看 | 欧美国产日韩亚洲中文 | 熟女俱乐部五十路六十路av | 久久天天躁夜夜躁狠狠 | 精品国产一区二区三区四区 | 精品人人妻人人澡人人爽人人 | 人人妻人人澡人人爽欧美精品 | 欧美熟妇另类久久久久久多毛 | 欧美日韩久久久精品a片 | 国产人妻大战黑人第1集 | 欧美三级a做爰在线观看 | 中文字幕无码人妻少妇免费 | 国产精品自产拍在线观看 | 日本一卡2卡3卡四卡精品网站 | 精品久久久久久亚洲精品 | 荫蒂被男人添的好舒服爽免费视频 | 精品亚洲韩国一区二区三区 | 国产精品嫩草久久久久 | 中文无码精品a∨在线观看不卡 | 亚洲日韩乱码中文无码蜜桃臀网站 | aa片在线观看视频在线播放 | 国产99久久精品一区二区 | 波多野结衣高清一区二区三区 | аⅴ资源天堂资源库在线 | www一区二区www免费 | 亚洲成av人片在线观看无码不卡 | 三级4级全黄60分钟 | 高潮毛片无遮挡高清免费视频 | 亚洲中文无码av永久不收费 | 高潮毛片无遮挡高清免费 | 天堂无码人妻精品一区二区三区 | 真人与拘做受免费视频一 | 给我免费的视频在线观看 | 国产艳妇av在线观看果冻传媒 | 日本大乳高潮视频在线观看 | 久久精品国产一区二区三区肥胖 | 亚洲国产日韩a在线播放 | 国产精品免费大片 | 国产莉萝无码av在线播放 | 精品无人国产偷自产在线 | 欧美老人巨大xxxx做受 | 人人妻人人澡人人爽人人精品 | 亚洲综合在线一区二区三区 | 特级做a爰片毛片免费69 | 精品国产一区二区三区av 性色 | 成人三级无码视频在线观看 | 亚洲精品久久久久久久久久久 | 亚洲中文字幕在线观看 | 亚洲s码欧洲m码国产av | 无码国产乱人伦偷精品视频 | 国内丰满熟女出轨videos | 日本精品高清一区二区 | 久久久av男人的天堂 | av无码不卡在线观看免费 | 熟妇人妻中文av无码 | 丰满岳乱妇在线观看中字无码 | 老司机亚洲精品影院 | 亚洲自偷自拍另类第1页 | 久久 国产 尿 小便 嘘嘘 | 日韩精品久久久肉伦网站 | 日本精品少妇一区二区三区 | 一本久道久久综合狠狠爱 | 国产无遮挡又黄又爽又色 | 国产人妻人伦精品 | 国产精品无码一区二区桃花视频 | 中文字幕av无码一区二区三区电影 | www国产精品内射老师 | 午夜肉伦伦影院 | 天天躁日日躁狠狠躁免费麻豆 | 四十如虎的丰满熟妇啪啪 | 精品久久久无码中文字幕 | 国产欧美精品一区二区三区 | 亚洲综合精品香蕉久久网 | 性欧美疯狂xxxxbbbb | 欧美大屁股xxxxhd黑色 | 精品国产一区二区三区四区在线看 | 98国产精品综合一区二区三区 | 一本加勒比波多野结衣 | 国产三级精品三级男人的天堂 | 玩弄中年熟妇正在播放 | 无人区乱码一区二区三区 | 亚洲熟妇色xxxxx欧美老妇y | 国产猛烈高潮尖叫视频免费 | 国产偷自视频区视频 | 国产成人精品优优av | 精品久久综合1区2区3区激情 | 狠狠综合久久久久综合网 | 无码人妻久久一区二区三区不卡 | 精品无码一区二区三区的天堂 | 国产亚洲人成a在线v网站 | 欧美性猛交xxxx富婆 | 300部国产真实乱 | 精品亚洲成av人在线观看 | 亚洲gv猛男gv无码男同 | 一本久道高清无码视频 | 亚洲国产一区二区三区在线观看 | 国产精品久久久久久久影院 | 成年美女黄网站色大免费全看 | 日产精品99久久久久久 | 免费无码的av片在线观看 | 亚洲中文字幕va福利 | 熟女少妇人妻中文字幕 | 一二三四社区在线中文视频 | 亚洲 a v无 码免 费 成 人 a v | 人人妻人人澡人人爽欧美一区九九 | 国产精品久久久午夜夜伦鲁鲁 | 日本乱人伦片中文三区 | 久久亚洲国产成人精品性色 | 夫妻免费无码v看片 | 色综合久久久无码中文字幕 | 国内少妇偷人精品视频免费 | 中文字幕人妻丝袜二区 | 成年女人永久免费看片 | 日韩人妻系列无码专区 | 久久久久久久久蜜桃 | 亚洲中文无码av永久不收费 | 欧美人与牲动交xxxx | 国产精品-区区久久久狼 | 日本一本二本三区免费 | 伊人久久婷婷五月综合97色 | 午夜福利一区二区三区在线观看 | 麻豆国产97在线 | 欧洲 | 成人欧美一区二区三区黑人免费 | 久久精品成人欧美大片 | 国产精品.xx视频.xxtv | 久久 国产 尿 小便 嘘嘘 | 在线 国产 欧美 亚洲 天堂 | 成人精品一区二区三区中文字幕 | 亚洲乱码国产乱码精品精 | 亚洲狠狠色丁香婷婷综合 | 欧美熟妇另类久久久久久多毛 | 国产精品成人av在线观看 | 波多野结衣aⅴ在线 | 日本精品久久久久中文字幕 | 亚洲а∨天堂久久精品2021 | 最近的中文字幕在线看视频 | 大胆欧美熟妇xx | 国产口爆吞精在线视频 | av无码电影一区二区三区 | 玩弄少妇高潮ⅹxxxyw | 狠狠色噜噜狠狠狠狠7777米奇 | 国产特级毛片aaaaaa高潮流水 | 青青青爽视频在线观看 | 人妻少妇精品无码专区二区 | 精品无人区无码乱码毛片国产 | 国产亚洲tv在线观看 | 国产人妻精品午夜福利免费 | 亚洲天堂2017无码中文 | 国产福利视频一区二区 | 色一情一乱一伦一区二区三欧美 | 国产精品手机免费 | 国产综合久久久久鬼色 | 亚洲理论电影在线观看 | 青青草原综合久久大伊人精品 | 国产午夜福利100集发布 | 俺去俺来也www色官网 | 亚洲另类伦春色综合小说 | 国产一区二区三区四区五区加勒比 | 日本乱偷人妻中文字幕 | 国产一区二区三区影院 | 国产xxx69麻豆国语对白 | 亚洲欧洲日本综合aⅴ在线 | av无码久久久久不卡免费网站 | 亚洲成a人片在线观看日本 | 美女毛片一区二区三区四区 | 亚洲 a v无 码免 费 成 人 a v | 亚洲国产日韩a在线播放 | 又大又硬又黄的免费视频 | 无码人妻精品一区二区三区下载 | 国产精品久久久 | 天下第一社区视频www日本 | 黑人粗大猛烈进出高潮视频 | 成熟人妻av无码专区 | 午夜理论片yy44880影院 | 天堂无码人妻精品一区二区三区 | 欧美熟妇另类久久久久久多毛 | 又粗又大又硬又长又爽 | 久精品国产欧美亚洲色aⅴ大片 | aⅴ亚洲 日韩 色 图网站 播放 | 亚洲日韩av一区二区三区四区 | 亚洲春色在线视频 | 两性色午夜视频免费播放 | √8天堂资源地址中文在线 | 亚洲欧美日韩成人高清在线一区 | 四虎影视成人永久免费观看视频 | a在线亚洲男人的天堂 | 久久久久亚洲精品中文字幕 | 久久伊人色av天堂九九小黄鸭 | 久久国产精品_国产精品 | 国产成人无码av片在线观看不卡 | 一本久久a久久精品亚洲 | 欧美丰满老熟妇xxxxx性 | 中文字幕无码视频专区 | 99麻豆久久久国产精品免费 | 亚洲一区二区三区国产精华液 | 久久人人爽人人爽人人片ⅴ | 人妻中文无码久热丝袜 | 国产亚洲精品久久久久久久 | 日韩少妇内射免费播放 | 99久久精品日本一区二区免费 | 国产成人综合美国十次 | 青青草原综合久久大伊人精品 | 免费人成网站视频在线观看 | 久久精品国产精品国产精品污 | 精品国产一区二区三区四区在线看 | 欧美性生交xxxxx久久久 | 久久国产精品_国产精品 | 亚洲国产av精品一区二区蜜芽 | 日日鲁鲁鲁夜夜爽爽狠狠 | 日韩欧美群交p片內射中文 | 亚洲另类伦春色综合小说 | 欧美精品在线观看 | 日本乱人伦片中文三区 | 人妻少妇精品无码专区动漫 | 女人被男人躁得好爽免费视频 | 国产做国产爱免费视频 | av无码久久久久不卡免费网站 | 曰韩无码二三区中文字幕 | 亚洲国产精品一区二区第一页 | 无码人妻丰满熟妇区五十路百度 | 欧美熟妇另类久久久久久不卡 | 亚洲一区二区三区国产精华液 | 玩弄人妻少妇500系列视频 | 性欧美大战久久久久久久 | 日产精品高潮呻吟av久久 | 撕开奶罩揉吮奶头视频 | 中文字幕无码av激情不卡 | 国内少妇偷人精品视频 | 亚洲精品国产a久久久久久 | 亚洲欧洲日本综合aⅴ在线 | 丰满诱人的人妻3 | 无码av中文字幕免费放 | 国产成人精品久久亚洲高清不卡 | 黑人大群体交免费视频 | 久久99精品久久久久婷婷 | 亚洲国精产品一二二线 | 久久视频在线观看精品 | 天堂在线观看www | 国产在线无码精品电影网 | 国产黑色丝袜在线播放 | 亚洲阿v天堂在线 | 亚洲狠狠色丁香婷婷综合 | 色综合视频一区二区三区 | 色妞www精品免费视频 | 天天av天天av天天透 | 日日鲁鲁鲁夜夜爽爽狠狠 | 国产精品久久久久久无码 | 成人片黄网站色大片免费观看 | 亚洲小说图区综合在线 | 欧美激情内射喷水高潮 | 一本大道伊人av久久综合 | 国产人妻大战黑人第1集 | 精品一区二区三区波多野结衣 | 国产办公室秘书无码精品99 | 婷婷综合久久中文字幕蜜桃三电影 | 丰满少妇人妻久久久久久 | 成年女人永久免费看片 | 国产一区二区三区影院 | 国产av一区二区三区最新精品 | 99精品无人区乱码1区2区3区 | 久久综合激激的五月天 | 成人免费视频视频在线观看 免费 | 欧美日韩久久久精品a片 | 国产精品久久久久9999小说 | 亚洲狠狠色丁香婷婷综合 | 国产亚洲精品精品国产亚洲综合 | 夜夜夜高潮夜夜爽夜夜爰爰 | 国产特级毛片aaaaaaa高清 | √天堂中文官网8在线 | 亚洲日本va午夜在线电影 | 无码免费一区二区三区 | 亚洲国产精品无码一区二区三区 | 乱码午夜-极国产极内射 | 午夜精品一区二区三区的区别 | 大乳丰满人妻中文字幕日本 | 久久婷婷五月综合色国产香蕉 | 久久精品国产99精品亚洲 | 国产精品理论片在线观看 | 四虎影视成人永久免费观看视频 | 波多野结衣av在线观看 | 九九久久精品国产免费看小说 | 久久精品国产大片免费观看 | 中文字幕中文有码在线 | 成人免费视频视频在线观看 免费 | 在线天堂新版最新版在线8 | 丰满少妇熟乱xxxxx视频 | 国产成人无码区免费内射一片色欲 | 亚洲一区二区三区在线观看网站 | 亚洲精品午夜无码电影网 | 亚洲精品午夜无码电影网 | 无码人妻av免费一区二区三区 | 成人性做爰aaa片免费看不忠 | 蜜桃av蜜臀av色欲av麻 999久久久国产精品消防器材 | 久久精品中文字幕一区 | av人摸人人人澡人人超碰下载 | 精品国产精品久久一区免费式 | 国产精品二区一区二区aⅴ污介绍 | 国产亚洲美女精品久久久2020 | 国产成人精品久久亚洲高清不卡 | 色婷婷av一区二区三区之红樱桃 | 亚洲一区二区三区在线观看网站 | 初尝人妻少妇中文字幕 | 欧美丰满老熟妇xxxxx性 | 中文字幕无码热在线视频 | 在线亚洲高清揄拍自拍一品区 | 国产精品久久国产精品99 | 国产精品igao视频网 | 中文字幕人妻无码一区二区三区 | 欧美精品国产综合久久 | 熟妇激情内射com | 欧美丰满老熟妇xxxxx性 | 亚洲精品无码国产 | 亚洲日本va午夜在线电影 | 国产色精品久久人妻 | 福利一区二区三区视频在线观看 | 国产麻豆精品精东影业av网站 | 成人免费视频在线观看 | 波多野结衣高清一区二区三区 | 少妇厨房愉情理9仑片视频 | 国产亚洲精品久久久久久国模美 | 无码中文字幕色专区 | 国产精品怡红院永久免费 | 激情爆乳一区二区三区 | 免费无码肉片在线观看 | 人妻与老人中文字幕 | 蜜桃视频插满18在线观看 | 人人澡人人妻人人爽人人蜜桃 | 亚洲日韩av一区二区三区中文 | 高清不卡一区二区三区 | 18禁止看的免费污网站 | 日韩欧美中文字幕在线三区 | 日日摸天天摸爽爽狠狠97 | 日日摸天天摸爽爽狠狠97 | 男女爱爱好爽视频免费看 | 午夜无码区在线观看 | 国产人成高清在线视频99最全资源 | 亚洲熟妇色xxxxx亚洲 | 欧美日韩在线亚洲综合国产人 | 中文无码成人免费视频在线观看 | 成年女人永久免费看片 | 人人澡人人妻人人爽人人蜜桃 | 国产亚洲精品久久久久久久久动漫 | 日本熟妇大屁股人妻 | 欧美日韩视频无码一区二区三 | 成人免费视频视频在线观看 免费 | 久久精品国产99久久6动漫 | 成年美女黄网站色大免费全看 | 男人扒开女人内裤强吻桶进去 | 狠狠cao日日穞夜夜穞av | 久久国产精品萌白酱免费 | 天天爽夜夜爽夜夜爽 | 国产激情无码一区二区 | 色老头在线一区二区三区 | 国产精品va在线播放 | 国产亚洲视频中文字幕97精品 | 国产黄在线观看免费观看不卡 | 天天爽夜夜爽夜夜爽 | 久久久久se色偷偷亚洲精品av | 东京无码熟妇人妻av在线网址 | 2020最新国产自产精品 | 日本熟妇人妻xxxxx人hd | 99久久人妻精品免费一区 | 国色天香社区在线视频 | 乱人伦人妻中文字幕无码 | 国产成人久久精品流白浆 | 精品水蜜桃久久久久久久 | 少妇厨房愉情理9仑片视频 | 中文字幕无码视频专区 | 鲁一鲁av2019在线 | 77777熟女视频在线观看 а天堂中文在线官网 | 日韩无套无码精品 | 国产人妻精品一区二区三区 | 色婷婷欧美在线播放内射 | 欧美精品一区二区精品久久 | 国产莉萝无码av在线播放 | 成人aaa片一区国产精品 | 欧美35页视频在线观看 | √8天堂资源地址中文在线 | 日韩欧美中文字幕公布 | a国产一区二区免费入口 | 日日天干夜夜狠狠爱 | 国产午夜亚洲精品不卡 | 沈阳熟女露脸对白视频 | 丝袜足控一区二区三区 | 亚洲无人区一区二区三区 | 亚洲七七久久桃花影院 | 色综合久久久久综合一本到桃花网 | 国产亚av手机在线观看 | 四虎永久在线精品免费网址 | 成人试看120秒体验区 | 性欧美大战久久久久久久 | 欧美日韩人成综合在线播放 | 人妻少妇被猛烈进入中文字幕 | 国产欧美精品一区二区三区 | 妺妺窝人体色www婷婷 | 国产精品久久久久影院嫩草 | 免费看男女做好爽好硬视频 | 亚洲成a人一区二区三区 | 亚洲欧美精品伊人久久 | 乌克兰少妇xxxx做受 | 国产三级久久久精品麻豆三级 | 久久zyz资源站无码中文动漫 | 男人扒开女人内裤强吻桶进去 | 帮老师解开蕾丝奶罩吸乳网站 | 午夜精品久久久内射近拍高清 | 亚洲中文字幕久久无码 | 久久人人爽人人人人片 | 高潮毛片无遮挡高清免费视频 | 久久久久se色偷偷亚洲精品av | 丝袜人妻一区二区三区 | 99久久无码一区人妻 | 午夜福利不卡在线视频 | 精品国产一区av天美传媒 | 少妇人妻偷人精品无码视频 | 国产97色在线 | 免 | 亚洲精品国产精品乱码视色 | 欧美阿v高清资源不卡在线播放 | 久久精品成人欧美大片 | 久久熟妇人妻午夜寂寞影院 | 国产精品美女久久久久av爽李琼 | 午夜丰满少妇性开放视频 | 99精品久久毛片a片 | 国产性生大片免费观看性 | 久久久久久av无码免费看大片 | 伊人久久婷婷五月综合97色 | 午夜精品一区二区三区在线观看 | 日韩av无码一区二区三区不卡 | 欧美人妻一区二区三区 | 国产成人综合在线女婷五月99播放 | 国内精品一区二区三区不卡 | 国产精品免费大片 | 国产午夜福利100集发布 | 丰满妇女强制高潮18xxxx | 国产在线精品一区二区高清不卡 | 人妻有码中文字幕在线 | 大地资源网第二页免费观看 | 国产精品va在线播放 | 精品熟女少妇av免费观看 | 国产成人av免费观看 | 性做久久久久久久免费看 | 人人爽人人澡人人人妻 | 精品日本一区二区三区在线观看 | 日本一卡2卡3卡四卡精品网站 | 亚洲综合久久一区二区 | 中文字幕无码人妻少妇免费 | 精品国产一区av天美传媒 | 亚洲精品中文字幕久久久久 | yw尤物av无码国产在线观看 | 色婷婷av一区二区三区之红樱桃 | 日日夜夜撸啊撸 | 国产精品久久久久影院嫩草 | 偷窥日本少妇撒尿chinese | 扒开双腿吃奶呻吟做受视频 | 波多野结衣一区二区三区av免费 | 又紧又大又爽精品一区二区 | 激情内射亚州一区二区三区爱妻 | 亚洲成a人片在线观看无码 | 欧美老妇交乱视频在线观看 | 一本色道婷婷久久欧美 | 欧美老人巨大xxxx做受 | 全黄性性激高免费视频 | 国产香蕉尹人视频在线 | 久久精品国产一区二区三区肥胖 | 亚洲爆乳无码专区 | 久久久久免费精品国产 | 国产精品igao视频网 | 国产精品高潮呻吟av久久 | 午夜精品久久久内射近拍高清 | 国产精品丝袜黑色高跟鞋 | 午夜福利试看120秒体验区 | 国产成人一区二区三区别 | 高潮毛片无遮挡高清免费视频 | 国产电影无码午夜在线播放 | 少妇高潮一区二区三区99 | 极品嫩模高潮叫床 | 亚洲热妇无码av在线播放 | 老司机亚洲精品影院无码 | 99久久久无码国产aaa精品 | 无码乱肉视频免费大全合集 | 伊人色综合久久天天小片 | 亚洲色大成网站www国产 | 东京热男人av天堂 | 99er热精品视频 | 久久人人爽人人爽人人片av高清 | 学生妹亚洲一区二区 | 精品成人av一区二区三区 | 99久久久无码国产aaa精品 | 一本色道久久综合亚洲精品不卡 | 在线观看欧美一区二区三区 | 中文字幕色婷婷在线视频 | 欧美日韩色另类综合 | 久久久婷婷五月亚洲97号色 | 亚洲中文字幕va福利 | 国产性生大片免费观看性 | 久久久中文久久久无码 | 狠狠cao日日穞夜夜穞av | 无遮挡啪啪摇乳动态图 | 无码精品人妻一区二区三区av | 亚洲综合无码一区二区三区 | www成人国产高清内射 | 无码国内精品人妻少妇 | 中文精品无码中文字幕无码专区 | 久久亚洲国产成人精品性色 | 色婷婷欧美在线播放内射 | 国产亚洲精品久久久ai换 | 在线视频网站www色 | 国产明星裸体无码xxxx视频 | 欧美丰满少妇xxxx性 | 亚洲熟女一区二区三区 | 色欲人妻aaaaaaa无码 | 久久久久99精品成人片 | 亚洲精品国产精品乱码不卡 | 台湾无码一区二区 | 又大又硬又爽免费视频 | 内射爽无广熟女亚洲 | 久久www免费人成人片 | 亚洲色www成人永久网址 | 无码人妻丰满熟妇区毛片18 | 久久久久成人片免费观看蜜芽 | 男人扒开女人内裤强吻桶进去 | 久久久久久av无码免费看大片 | 色 综合 欧美 亚洲 国产 | 免费国产成人高清在线观看网站 | 人妻插b视频一区二区三区 | 麻豆人妻少妇精品无码专区 | 国产人妻精品午夜福利免费 | 精品国产一区av天美传媒 | 国产成人无码午夜视频在线观看 | 日日麻批免费40分钟无码 | 性欧美牲交xxxxx视频 | 2020久久香蕉国产线看观看 | 樱花草在线播放免费中文 | 极品嫩模高潮叫床 | 三级4级全黄60分钟 | 日本欧美一区二区三区乱码 | 国产无遮挡又黄又爽免费视频 | 水蜜桃亚洲一二三四在线 | 亚洲国产成人a精品不卡在线 | 国产猛烈高潮尖叫视频免费 | 亚洲一区av无码专区在线观看 | 在线亚洲高清揄拍自拍一品区 | 精品国产一区av天美传媒 | 精品人妻av区 | a片免费视频在线观看 | 真人与拘做受免费视频 | 欧洲美熟女乱又伦 | 国产香蕉尹人综合在线观看 | 亚洲天堂2017无码中文 | 成年美女黄网站色大免费视频 | 国产精品久久久久久久9999 | aa片在线观看视频在线播放 | 日韩无套无码精品 | 欧美日韩久久久精品a片 | 一本色道久久综合亚洲精品不卡 | 国产女主播喷水视频在线观看 | 无套内射视频囯产 | 成人性做爰aaa片免费看不忠 | 国产精品久久久久久无码 | 亚洲熟妇自偷自拍另类 | 欧美日本日韩 | 国产午夜亚洲精品不卡 | 好男人www社区 | 亚洲欧洲日本无在线码 | 日本一区二区三区免费高清 | 国产无遮挡又黄又爽免费视频 | 国产欧美精品一区二区三区 | 亚洲色大成网站www国产 | 玩弄少妇高潮ⅹxxxyw | 中文字幕无码热在线视频 | 人人妻人人澡人人爽人人精品浪潮 | 日本大香伊一区二区三区 | 少妇性l交大片欧洲热妇乱xxx | 亚洲男人av天堂午夜在 | 99久久人妻精品免费一区 | 亚洲中文字幕av在天堂 | 无码人妻久久一区二区三区不卡 | 成人欧美一区二区三区黑人 | 亚洲成av人影院在线观看 | 日韩成人一区二区三区在线观看 | 色一情一乱一伦一视频免费看 | 131美女爱做视频 | 欧美放荡的少妇 | 无码精品国产va在线观看dvd | 无码人妻精品一区二区三区下载 | 国内精品久久毛片一区二区 | 亚洲另类伦春色综合小说 | 精品偷自拍另类在线观看 | 美女黄网站人色视频免费国产 | 欧美国产日产一区二区 | 久久伊人色av天堂九九小黄鸭 | 全球成人中文在线 | 欧美日韩久久久精品a片 | 亚洲色欲色欲天天天www | 婷婷色婷婷开心五月四房播播 | 亚洲中文字幕无码一久久区 | 又湿又紧又大又爽a视频国产 | 无码精品人妻一区二区三区av | 国产免费无码一区二区视频 | 98国产精品综合一区二区三区 | 丰满护士巨好爽好大乳 | 久久国产精品萌白酱免费 | 色一情一乱一伦一视频免费看 | 精品欧洲av无码一区二区三区 | 久久精品国产精品国产精品污 | 国产av剧情md精品麻豆 | 国内精品人妻无码久久久影院 | 99久久久国产精品无码免费 | √天堂中文官网8在线 | 未满小14洗澡无码视频网站 | 日韩欧美中文字幕公布 | 欧美第一黄网免费网站 | 97精品国产97久久久久久免费 | 巨爆乳无码视频在线观看 | 国产亚洲日韩欧美另类第八页 | 兔费看少妇性l交大片免费 | 亚洲欧美精品aaaaaa片 | 久久无码专区国产精品s | 奇米影视7777久久精品 | 99久久99久久免费精品蜜桃 | 狂野欧美性猛交免费视频 | 国产亚洲精品久久久久久国模美 | 99久久精品午夜一区二区 | 99视频精品全部免费免费观看 | 黄网在线观看免费网站 | 奇米影视888欧美在线观看 | 高中生自慰www网站 | 麻豆国产人妻欲求不满 | 久久国产劲爆∧v内射 | 国产sm调教视频在线观看 | 任你躁在线精品免费 | 少妇高潮一区二区三区99 | 亚洲午夜福利在线观看 | 性色av无码免费一区二区三区 | 蜜桃视频韩日免费播放 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 国产97人人超碰caoprom | 成人精品一区二区三区中文字幕 | 少妇邻居内射在线 | 偷窥村妇洗澡毛毛多 | 午夜肉伦伦影院 | 一本色道婷婷久久欧美 | 欧美激情综合亚洲一二区 | 色综合久久中文娱乐网 | 国产激情一区二区三区 | 乱中年女人伦av三区 | 性生交大片免费看女人按摩摩 | 日本乱偷人妻中文字幕 | 熟妇人妻中文av无码 | 久久精品国产99久久6动漫 | 亚洲色大成网站www国产 | 国产成人久久精品流白浆 | 欧美真人作爱免费视频 | 国精产品一区二区三区 | 国产特级毛片aaaaaaa高清 | 日本大乳高潮视频在线观看 | 欧美日韩人成综合在线播放 | 无码帝国www无码专区色综合 | 国产凸凹视频一区二区 | 人人爽人人澡人人人妻 | 中文字幕人妻无码一夲道 | 老熟女乱子伦 | 2020久久香蕉国产线看观看 | 成人一在线视频日韩国产 | 国产av剧情md精品麻豆 | 人人爽人人澡人人高潮 | 全黄性性激高免费视频 | 亚洲中文字幕成人无码 | 在线观看国产一区二区三区 | 欧美国产日产一区二区 | 久久午夜无码鲁丝片秋霞 | 亚洲精品鲁一鲁一区二区三区 | 国内综合精品午夜久久资源 | 精品无码av一区二区三区 | 亚洲日本在线电影 | 女人高潮内射99精品 | 波多野结衣一区二区三区av免费 | 午夜不卡av免费 一本久久a久久精品vr综合 | 成人一在线视频日韩国产 | 国产成人无码av片在线观看不卡 | 高清不卡一区二区三区 | 久久国产劲爆∧v内射 | 无码人妻av免费一区二区三区 | 波多野结衣av在线观看 | 日韩亚洲欧美中文高清在线 | 亚洲精品一区三区三区在线观看 | 中文字幕av伊人av无码av | 亚洲 欧美 激情 小说 另类 | 色一情一乱一伦一视频免费看 | 色一情一乱一伦一区二区三欧美 | 中文字幕日韩精品一区二区三区 | 国产精品丝袜黑色高跟鞋 | 四虎永久在线精品免费网址 | 男女猛烈xx00免费视频试看 | 亚洲成av人综合在线观看 | 亚洲精品久久久久久久久久久 | 夜夜夜高潮夜夜爽夜夜爰爰 | 国产乱码精品一品二品 | 丰满妇女强制高潮18xxxx | 丰满妇女强制高潮18xxxx | 国产精品毛多多水多 | 日本饥渴人妻欲求不满 | 国产精品亚洲五月天高清 | av香港经典三级级 在线 | 精品一区二区不卡无码av | 国产办公室秘书无码精品99 | 国产色视频一区二区三区 | 精品国产麻豆免费人成网站 | 日本高清一区免费中文视频 | 爆乳一区二区三区无码 | 麻豆蜜桃av蜜臀av色欲av | 久久久无码中文字幕久... | 日韩精品无码一区二区中文字幕 | 精品久久8x国产免费观看 | 久久综合九色综合欧美狠狠 | 国模大胆一区二区三区 | 人人妻人人澡人人爽精品欧美 | 99久久婷婷国产综合精品青草免费 | 日本丰满护士爆乳xxxx | 亚洲日本一区二区三区在线 | 国产69精品久久久久app下载 | 日本一卡2卡3卡四卡精品网站 | 国产又爽又黄又刺激的视频 | 麻豆蜜桃av蜜臀av色欲av | 97久久精品无码一区二区 | 欧美成人免费全部网站 | 无码人妻精品一区二区三区不卡 | 国产香蕉尹人综合在线观看 | 日产精品高潮呻吟av久久 | 内射巨臀欧美在线视频 | 日韩少妇内射免费播放 | 99麻豆久久久国产精品免费 | 亚洲熟妇色xxxxx亚洲 | 国产内射爽爽大片视频社区在线 | 97久久超碰中文字幕 | 性做久久久久久久免费看 | 国产精品va在线观看无码 | 噜噜噜亚洲色成人网站 | 国产国语老龄妇女a片 | www一区二区www免费 | 日日摸日日碰夜夜爽av | 无码人妻精品一区二区三区不卡 | 久久综合狠狠综合久久综合88 | 成 人 免费观看网站 | 亚洲自偷精品视频自拍 | 毛片内射-百度 | 国产人妻久久精品二区三区老狼 | 又大又黄又粗又爽的免费视频 | 国产精品久久久久9999小说 | 色婷婷久久一区二区三区麻豆 | 最近免费中文字幕中文高清百度 | 国产情侣作爱视频免费观看 | 精品乱码久久久久久久 | 中文字幕人成乱码熟女app | 久久99精品国产麻豆 | 无套内谢的新婚少妇国语播放 | 无码人妻精品一区二区三区下载 | a在线亚洲男人的天堂 | 亚洲国产精品美女久久久久 | 日韩欧美中文字幕在线三区 | 日韩av无码一区二区三区不卡 | 精品一区二区不卡无码av | 久久久精品成人免费观看 | 亚洲乱码国产乱码精品精 | aⅴ在线视频男人的天堂 | 欧洲精品码一区二区三区免费看 | 麻豆人妻少妇精品无码专区 | 一本久久伊人热热精品中文字幕 | 丰满人妻被黑人猛烈进入 | 日本熟妇人妻xxxxx人hd | 中文字幕无码人妻少妇免费 | 对白脏话肉麻粗话av | 牲交欧美兽交欧美 | 久久国产自偷自偷免费一区调 | a片免费视频在线观看 | 亚洲中文字幕无码一久久区 | 精品人妻人人做人人爽夜夜爽 | 东京热男人av天堂 | 久久久中文久久久无码 | 欧美亚洲日韩国产人成在线播放 | 日本大香伊一区二区三区 | 性欧美熟妇videofreesex | 国产激情一区二区三区 | 国产后入清纯学生妹 | 国产精品沙发午睡系列 | 亚洲一区av无码专区在线观看 | 亚洲欧美日韩成人高清在线一区 | 全球成人中文在线 | 自拍偷自拍亚洲精品10p | 免费观看黄网站 | 99精品无人区乱码1区2区3区 | 永久免费观看美女裸体的网站 | 天天摸天天碰天天添 | 丝袜美腿亚洲一区二区 | 无码国产乱人伦偷精品视频 | 玩弄中年熟妇正在播放 | 中文字幕无码日韩专区 | 久久精品中文闷骚内射 | 日本一区二区更新不卡 | 国产免费观看黄av片 | 国产另类ts人妖一区二区 | 性欧美熟妇videofreesex | 又大又硬又爽免费视频 | 国产精品人人爽人人做我的可爱 | 成熟女人特级毛片www免费 | 亚洲熟女一区二区三区 | 国产成人精品久久亚洲高清不卡 | 99久久久无码国产aaa精品 | 成人无码视频免费播放 | 东京热一精品无码av | 樱花草在线播放免费中文 | 亚洲第一网站男人都懂 | 亚洲精品国偷拍自产在线麻豆 | 国产精品a成v人在线播放 | 最新国产乱人伦偷精品免费网站 | 亚洲一区av无码专区在线观看 | 久久午夜无码鲁丝片午夜精品 | 中文字幕日产无线码一区 | 亚洲天堂2017无码 | 精品国产成人一区二区三区 | 天海翼激烈高潮到腰振不止 | 国产人妻精品一区二区三区 | 亚洲成av人综合在线观看 | 少妇无码一区二区二三区 | 精品国产青草久久久久福利 | 中文字幕人妻无码一区二区三区 | 中文字幕乱妇无码av在线 | 欧美日韩在线亚洲综合国产人 | 无码人妻丰满熟妇区五十路百度 | 性生交大片免费看l | 欧美精品无码一区二区三区 | 欧美熟妇另类久久久久久多毛 | 亚洲人亚洲人成电影网站色 | 久久亚洲国产成人精品性色 | 亚洲小说春色综合另类 | 久久99精品久久久久久动态图 | 人人爽人人澡人人人妻 | 波多野结衣 黑人 | 国产亚洲视频中文字幕97精品 | 国产成人无码专区 | 色老头在线一区二区三区 | 丝袜人妻一区二区三区 | 97色伦图片97综合影院 | 亚洲色大成网站www国产 | 一本一道久久综合久久 | 人人妻人人藻人人爽欧美一区 | 亚洲人交乣女bbw | 久久伊人色av天堂九九小黄鸭 | 精品无码国产一区二区三区av | 日韩精品乱码av一区二区 | 亚洲日本va中文字幕 | 精品无码一区二区三区的天堂 | 疯狂三人交性欧美 | 色婷婷久久一区二区三区麻豆 | √天堂中文官网8在线 | 国产精品久免费的黄网站 | 亚洲国产一区二区三区在线观看 |