iOS Operation 的整理

operation

(上圖是一些 Operation 經常使用的功能的UML圖)swift

1. 爲何使用 Operation & OperationQueue

GCD 是在封裝了C,進行多線程開發。而 Operation & OperationQueue 則是封裝了 GCD 進行多線程開發。在平時用GCD就能解決不少多線程任務開發的問題, 那爲何還要使用 Operation & OperationQueue 呢?安全

使用 Operation & OperationQueue 可以:bash

  • 設置 Operation 之間的依賴;
  • 經過 KVO 觀察 Operation 的執行狀態;
  • 更加方便的設置 Operation 優先級
  • OperationQueue 能夠更方便的取消全部在隊列裏面的 Operation
  • 使用 OperationQueue 更方便的執行 Operation 的暫停和恢復執行(使用 isSuspend)

2. Operation & OperationQueue 介紹

Operation 一個操做,能夠當作一個任務。相似於 GCD 裏面的 DispatchItem。Operation 是一個抽象類,裏面定義了任務安全執行的邏輯。 在實際使用的時候通常使用系統建立的子類 BlockOperation (Objective-C 還可使用 NSInvocationOperation)。或者建立 Operation 的自定義子類。多線程

OperationQueue 操做的隊列,管理操做的執行,相似於 GCD 裏面的 DispatchQueue。在 GCD 中,任務的執行順序是先進先出 FIFO。OperationQueue 執行 Operation 的時候依據 Operation 的優先級和是否在準備就緒的狀態。當一個 Operation 加入 OperationQueue 之後,只有等到 Operation 執行完畢(任務取消也是執行完畢)才能從 OperationQueue 裏面移除。OperationQueue 不能直接把 Operation 移除。併發

3. Operation

增長一個輔助方法輸出當前內容和執行的線程app

func printOperation<T>(_ message: T) {
    print(" \(message), thread = \(Thread.current)")
}
複製代碼

3.1 建立 Operation 子類 BlockOperation

let operation = BlockOperation {
            printOperation("Op init block task 1 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Op init block task 1 end")
        }
        operation.addExecutionBlock {
            printOperation("Op task 2 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Op task 2 end")
        }
        
        operation.addExecutionBlock {
            printOperation("Op task 3 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Op task 3 end")
        }
複製代碼

初始化一個 BlockOperation, 隨後能夠加入多個任務異步

  • 須要注意的是不要在 Operation 執行中或者執行完畢加入,不然會報錯。
  • 多個任務添加的效果是任務會併發執行:若是本身調用 start(), BlockOperation { // task } 會在當前線程執行;addExecutionBlock { // task } 開啓新線程執行

3.2 建立 Operation 自定義子類

能夠定義本身的同步執行子類,和異步執行子類。同步執行子類的建立須要配置的比較少,通常狀況只須要建立一個本身的初始化方法和覆蓋 main() 方法就行了。異步執行的則比較複雜,須要本身設置多個狀態屬性。async

class CustomOperation: Operation {
    
    private let address: String
    
    init(address: String) {
        self.address = address
    }

    override func main() {
        if isCancelled {
            return
        }
        printOperation("CustomOperation task 1 begin")
        Thread.sleep(forTimeInterval: 3)
        printOperation("CustomOperation task 1 end")
    }
}
複製代碼
  • 須要注意的是的是覆蓋的 main() 方法須要判斷任務是否取消。
  • isCancelled 的判斷處通常是任務剛開始以及一些耗時任務的開始執行與完畢的添加

具體可參考:Apple Operation Documentationide

3.2 定製 Opearation 行爲

3.2.1 添加依賴

let operationTask2 = BlockOperation {
            printOperation("Operation task 2 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 2 end")
        }
        let operationTask3 = BlockOperation {
            printOperation("Operation task 3 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 3 end")
        }
        
        let operationTask4 = BlockOperation {
            printOperation("Operation task 4 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 4 end")
        }

        // 2 -> 4 - > 3
        operationTask3.addDependency(operationTask4)
        operationTask4.addDependency(operationTask2)
複製代碼

在上面的例子中,operationTask4 的開始執行依賴 operationTask2,operationTask3 的開始執行依賴 operationTask4。ui

  • 添加的依賴 Operation 能夠在不一樣的 OperationQueue;
  • 當 Operation 添加到 OperationQueue 之後就不要再設置依賴了,誰也不知道會發生什麼;
  • Operation 的依賴添加註意不要產生循環。

3.2.2 修改 Operation 的優先級

let operationTask2 = BlockOperation {
            printOperation("Operation task 2 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 2 end")
        }
        let operationTask3 = BlockOperation {
            printOperation("Operation task 3 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 3 end")
        }
        
        let operationTask4 = BlockOperation {
            printOperation("Operation task 4 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 4 end")
        }

        operationTask2.queuePriority = .veryHigh
        operationTask3.queuePriority = .normal
        operationTask4.queuePriority = .low
複製代碼

在上面設置了 Operation 的隊列優先級屬性

  • 設置 queuePriority 只會大機率先執行高優先級的,不必定高優先級的就必定先執行;
  • Operation 的實際執行是判斷它是否準備就緒能夠執行任務了,若是它還有依賴沒有執行完畢,那麼它確定不會執行的。若是多個 Operation 都是能夠執行狀態纔會大機率先執行高優先級的 Operation。

3.2.3 設置 Operation completionBlock

operation.completionBlock = {
            DispatchQueue.main.async {
                // Task done
            }
        }
複製代碼
  • 因爲操做執行完畢的線程是不肯定的,要是有一些內容須要更新。最好是回到主線程更新
  • Operation 不論是正常執行完畢,仍是任務被取消了,都會調用 completionBlock

3.3 執行 Operation

和 GCD 裏面的 DispatchWorkItem 同樣,任務能夠本身執行,也能夠提交給隊列執行。

3.3.1 本身執行

本身執行調用 start() 方法。下面看一些例子:

BlockOperation 的單個任務

private func singleBlockOperation() {
        printOperation("Test begin")
        
        let operation = BlockOperation {
            printOperation("Operation init block task 1 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation init block task 1 end")
        }
                
        printOperation("Operation start")
        operation.start()
        printOperation("Test end")
    }
複製代碼

輸出:

Test begin, thread = <NSThread: 0x6000039ead80>{number = 1, name = main}
 Operation start, thread = <NSThread: 0x6000039ead80>{number = 1, name = main}
 Operation init block task 1 begin, thread = <NSThread: 0x6000039ead80>{number = 1, name = main}
 Operation init block task 1 end, thread = <NSThread: 0x6000039ead80>{number = 1, name = main}
 Test end, thread = <NSThread: 0x6000039ead80>{number = 1, name = main}
複製代碼

能夠看到 start() 默認是一個同步執行方法。它會等待任務在當前線程執行完畢 (例子此時在主線程)。

  • 其實也能夠設置爲異步執行的,不過須要添加一些額外的操做,通常異步執行使用的狀況是添加到 OperationQueue。由 OperationQueue 處理更多相關信息。

BlockOperation 的多個任務

狀況一:

private func singleBlockMutipleTaskOperation() {
        printOperation("Test begin")
        
        let operation = BlockOperation {
            printOperation("Operation init block task 1 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation init block task 1 end")
        }
             
        operation.addExecutionBlock {
            printOperation("Operation task 2 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 2 end")
        }
        
        operation.addExecutionBlock {
            printOperation("Operation task 3 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 3 end")
        }

        printOperation("Operation start")
        operation.start()
        printOperation("Test end")
    }
複製代碼

輸出:

Test begin, thread = <NSThread: 0x6000006b6d00>{number = 1, name = main}
 Operation start, thread = <NSThread: 0x6000006b6d00>{number = 1, name = main}
 Operation init block task 1 begin, thread = <NSThread: 0x6000006b6d00>{number = 1, name = main}
 Operation task 2 begin, thread = <NSThread: 0x600000635d00>{number = 6, name = (null)}
 Operation task 3 begin, thread = <NSThread: 0x600000620e80>{number = 7, name = (null)}
 Operation task 3 end, thread = <NSThread: 0x600000620e80>{number = 7, name = (null)}
 Operation init block task 1 end, thread = <NSThread: 0x6000006b6d00>{number = 1, name = main}
 Operation task 2 end, thread = <NSThread: 0x600000635d00>{number = 6, name = (null)}
 Test end, thread = <NSThread: 0x6000006b6d00>{number = 1, name = main}
複製代碼

此時 BlockOperation 第一個任務 BlockOperation { // task }在當前線程(當前線程是主線程)執行,額外的任務 addExecutionBlock { // task } 在其它線程執行。

狀況二:

private func singleBlockMutipleTaskOperationAddTasks() {
        printOperation("Test begin")
        
        let operation = BlockOperation()
             
        operation.addExecutionBlock {
            printOperation("Operation task 2 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 2 end")
        }
        
        operation.addExecutionBlock {
            printOperation("Operation task 3 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 3 end")
        }

        printOperation("Operation start")
        operation.start()
        printOperation("Test end")
    }
複製代碼

輸出:

Test begin, thread = <NSThread: 0x6000009c95c0>{number = 1, name = main}
 Operation start, thread = <NSThread: 0x6000009c95c0>{number = 1, name = main}
 Operation task 2 begin, thread = <NSThread: 0x6000009c95c0>{number = 1, name = main}
 Operation task 3 begin, thread = <NSThread: 0x600000959f40>{number = 6, name = (null)}
 Operation task 3 end, thread = <NSThread: 0x600000959f40>{number = 6, name = (null)}
 Operation task 2 end, thread = <NSThread: 0x6000009c95c0>{number = 1, name = main}
 Test end, thread = <NSThread: 0x6000009c95c0>{number = 1, name = main}
複製代碼

和狀況一的結果相似: 第一個任務 addExecutionBlock { // task1 } 在當前線程(當前線程是主線程)執行,額外的任務 addExecutionBlock { // tasks } 在其它線程執行。

3.3.2 添加到 OperationQueue 執行

  1. 通常狀況:
private func queueAddOperations() {
        printOperation("Test begin")
        let queue = OperationQueue()
        
        queue.addOperation {
            printOperation("Operation task 1 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 1 end")
        }
        let operationTask2 = BlockOperation {
            printOperation("Operation task 2 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 2 end")
        }
        let operationTask3 = BlockOperation {
            printOperation("Operation task 3 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 3 end")
        }

        queue.addOperation(operationTask2)
        queue.addOperation(operationTask3)
        
        printOperation("Test end")
    }
複製代碼

輸出:

Test begin, thread = <NSThread: 0x600002bc6cc0>{number = 1, name = main}
 Test end, thread = <NSThread: 0x600002bc6cc0>{number = 1, name = main}
 Operation task 1 begin, thread = <NSThread: 0x600002b51980>{number = 7, name = (null)}
 Operation task 2 begin, thread = <NSThread: 0x600002bb3400>{number = 8, name = (null)}
 Operation task 3 begin, thread = <NSThread: 0x600002bc3280>{number = 9, name = (null)}
 Operation task 2 end, thread = <NSThread: 0x600002bb3400>{number = 8, name = (null)}
 Operation task 3 end, thread = <NSThread: 0x600002bc3280>{number = 9, name = (null)}
 Operation task 1 end, thread = <NSThread: 0x600002b51980>{number = 7, name = (null)}
複製代碼

異步並不是執行而且全部任務開啓新線程。

  1. 指定 OperationQueue 像串行隊列同樣運行:

OperationQueue 裏面有一個設置當前隊列最大操做併發執行的屬性 maxConcurrentOperationCount

private func queueActAsSerial() {
        printOperation("Test begin")
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1 // 設置最大運行數目
        
        queue.addOperation {
            printOperation("Operation task 1 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 1 end")
        }
        
        let operationTask2 = BlockOperation {
            printOperation("Operation task 2 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 2 end")
        }
        let operationTask3 = BlockOperation {
            printOperation("Operation task 3 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 3 end")
        }
        
        queue.addOperation(operationTask2)
        queue.addOperation(operationTask3)
        
        printOperation("Test end")
    }
複製代碼

輸出:

Test begin, thread = <NSThread: 0x600002b66d00>{number = 1, name = main}
 Test end, thread = <NSThread: 0x600002b66d00>{number = 1, name = main}
 Operation task 1 begin, thread = <NSThread: 0x600002b14080>{number = 3, name = (null)}
 Operation task 1 end, thread = <NSThread: 0x600002b14080>{number = 3, name = (null)}
 Operation task 2 begin, thread = <NSThread: 0x600002bf7a80>{number = 6, name = (null)}
 Operation task 2 end, thread = <NSThread: 0x600002bf7a80>{number = 6, name = (null)}
 Operation task 3 begin, thread = <NSThread: 0x600002bf7a80>{number = 6, name = (null)}
 Operation task 3 end, thread = <NSThread: 0x600002bf7a80>{number = 6, name = (null)}
複製代碼

能夠當作此時就像是串行隊列同樣,只能等上一個任務完成才能執行下一個任務。這個最大併發數目在執行的時候最好是用默認值。它會由系統給出一個最優的值。在實際的設置中若是設置爲 maxConcurrentOperationCount = 1000 系統也是會根據實際狀況取最優值的。

  1. 添加依賴和 completionBlock
private func operationDependenciesAndCompletionBlock() {
        printOperation("Test begin")
        let queue = OperationQueue()
        
        queue.addOperation {
            printOperation("Operation task 1 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 1 end")
        }
        
        let operationTask2 = BlockOperation {
            printOperation("Operation task 2 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 2 end")
        }
        let operationTask3 = BlockOperation {
            printOperation("Operation task 3 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 3 end")
        }
        
        let operationTask4 = BlockOperation {
            printOperation("Operation task 4 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 4 end")
        }
        
        // 2 -> 4 - > 3
        operationTask3.addDependency(operationTask4)
        operationTask4.addDependency(operationTask2)
        
        operationTask4.completionBlock = {
            DispatchQueue.main.async {
                printOperation("operationTask4 completion")
            }
        }
        
        queue.addOperation(operationTask2)
        queue.addOperation(operationTask3)
        queue.addOperation(operationTask4)
        
        printOperation("Test end")
    }
複製代碼

輸出:

Test begin, thread = <NSThread: 0x600000bbebc0>{number = 1, name = main}
 Test end, thread = <NSThread: 0x600000bbebc0>{number = 1, name = main}
 Operation task 1 begin, thread = <NSThread: 0x600000b34000>{number = 6, name = (null)}
 Operation task 2 begin, thread = <NSThread: 0x600000b2c440>{number = 7, name = (null)}
 Operation task 1 end, thread = <NSThread: 0x600000b34000>{number = 6, name = (null)}
 Operation task 2 end, thread = <NSThread: 0x600000b2c440>{number = 7, name = (null)}
 Operation task 4 begin, thread = <NSThread: 0x600000b2c440>{number = 7, name = (null)}
 Operation task 4 end, thread = <NSThread: 0x600000b2c440>{number = 7, name = (null)}
 Operation task 3 begin, thread = <NSThread: 0x600000b34000>{number = 6, name = (null)}
 operationTask4 completion, thread = <NSThread: 0x600000bbebc0>{number = 1, name = main}
 Operation task 3 end, thread = <NSThread: 0x600000b34000>{number = 6, name = (null)}
複製代碼

能夠看出:

  • 依賴關係爲: operationTask2 執行完畢之後執行 operationTask4。operationTask4 執行完畢之後執行 operationTask3。
  • operationTask4 執行完畢會調用它的 completionBlock
  • 添加進 OperationQueue 的 Operation 只有在準備好了纔會執行。好比若是有依賴項就必須得執行完畢纔會變成可執行狀態。
  • GCD 入隊是 FIFO, OperationQueue 則不必定。
  1. 等待執行完成

queue.waitUntilAllOperationsAreFinished() 這個會阻塞當前線程,直到隊列裏面全部 Operation 執行完畢。

  • 最好不要在主線程調用這個方法,否則會影響用戶體驗;
  • 調用該方法的時候還能夠繼續向隊列裏面添加 Operation。
  1. Operation 的取消
private func queueCancel() {
        printOperation("Test begin")
        let queue = OperationQueue()
        
        queue.addOperation {
            printOperation("Operation task 1 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 1 end")
        }
        
        let operationTask2 = BlockOperation {
            printOperation("Operation task 2 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 2 end")
        }
        let operationTask3 = BlockOperation {
            printOperation("Operation task 3 begin")
            Thread.sleep(forTimeInterval: 3)
            printOperation("Operation task 3 end")
        }
                
        operationTask2.completionBlock = {
            DispatchQueue.main.async {
                printOperation("operationTask2 completion. isFinished = \(operationTask2.isFinished), isCancelled = \(operationTask2.isCancelled)")
            }
        }
        queue.addOperation(operationTask2)
        queue.addOperation(operationTask3)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            printOperation("xxxx cancelAllOperations")
            queue.cancelAllOperations()
        }
        
        printOperation("Test end")
    }
複製代碼

輸出:

Test begin, thread = <NSThread: 0x600002d1ecc0>{number = 1, name = main}
 Operation task 1 begin, thread = <NSThread: 0x600002d86b00>{number = 6, name = (null)}
 Operation task 2 begin, thread = <NSThread: 0x600002d8ba40>{number = 7, name = (null)}
 Operation task 3 begin, thread = <NSThread: 0x600002d68680>{number = 5, name = (null)}
 Test end, thread = <NSThread: 0x600002d1ecc0>{number = 1, name = main}
 xxxx cancelAllOperations, thread = <NSThread: 0x600002d1ecc0>{number = 1, name = main}
 Operation task 2 end, thread = <NSThread: 0x600002d8ba40>{number = 7, name = (null)}
 Operation task 1 end, thread = <NSThread: 0x600002d86b00>{number = 6, name = (null)}
 Operation task 3 end, thread = <NSThread: 0x600002d68680>{number = 5, name = (null)}
 operationTask2 completion. isFinished = true, isCancelled = true, thread = <NSThread: 0x600002d1ecc0>{number = 1, name = main}
複製代碼
  • 調用 queue.cancelAllOperations() 會遍歷調用全部的 operation.cancel()。取消操做不會自動將它們從隊列中刪除,也不會中止當前正在執行的操做
  • 即便調用 cancel() 之後,仍是會調用 operation.completionBlock { // xxxx }。
  1. 暫停和恢復 OperationQueue

經過設置 queue.isSuspended = true // or false

  • 還沒執行的 operarion 就不暫停不許備執行了;
  • 正在執行的 operarion 繼續執行;
  • 後加入的 operarion 暫停不許備執行。
  1. 一個資源競爭的使用例子

有時候一些關鍵資源須要保證線程安全,在 GCD 中可使用 DispatchSemaphore 使用,下面添加一個使用 NSLock 的例子

private var tickets = 12; // 12 張票
    private var lock: NSLock!

    private func queueWithThreadSafe() {
        lock = NSLock()
        
        let queue0 = OperationQueue()
        let queue1 = OperationQueue()

        // 添加畫不一樣隊列的不一樣操做
        queue0.addOperation {
            self.tikcetsSell()
        }
        
        queue0.addOperation {
            self.tikcetsSell()
        }

        queue0.addOperation {
            self.tikcetsSell()
        }
        
        queue1.addOperation {
            self.tikcetsSell()
        }
    }
    
    // 多個線程賣票,須要保證線程安全
    private func tikcetsSell() {
        while true {
            lock?.lock() // 加鎖 ..... 在須要保存線程安全的代碼前加鎖
            if tickets > 0 {
                Thread.sleep(forTimeInterval: 0.3)
                tickets -= 1
                printOperation("Tickets count = \(tickets)")
            }
            lock?.unlock() // 去鎖 ..... 在須要保存線程安全的代碼後面去鎖

            if tickets <= 0 {
                printOperation("Tickets sold out")
                break
            }
        }
    }

複製代碼

輸出:

Tickets count = 11, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets count = 10, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets count = 9, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets count = 8, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets count = 7, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets count = 6, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets count = 5, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets count = 4, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets count = 3, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets count = 2, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets count = 1, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets count = 0, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets sold out, thread = <NSThread: 0x60000145be80>{number = 7, name = (null)}
 Tickets sold out, thread = <NSThread: 0x6000014ba400>{number = 8, name = (null)}
 Tickets sold out, thread = <NSThread: 0x6000014b7540>{number = 9, name = (null)}
 Tickets sold out, thread = <NSThread: 0x6000014a8440>{number = 10, name = (null)}
複製代碼

可使用 NSLock 在 OperationQueue 裏面實現線程安全。

4. 最後

這裏有一個自定義 Operaion 的實際例子:Raywenderlich: Operation and OperationQueue Tutorial in Swift

相關文章
相關標籤/搜索