iOS 多線程概覽

1、為什么所有的UI操作都在主線程中

不僅是iOS系統(tǒng),包括Android等,所有的UI渲染、操作都在主線程中來完成。那為什么不采用多線程的方式呢?
使用多線程渲染UI更快,操作更流暢。但是系統(tǒng)設(shè)計者和開發(fā)者來說,需要解決線程問題的成本就更高了,也就是說成本遠大于收益了。所以工程師們把所有的UI渲染和操作全都放在了主線程中。參考為什么必須在主線程操作UI

2、為什么要使用多線程

即然所有的UI操作都是單線程的,那么為何還需要多線程呢?在App開發(fā)中,所遇到不僅有UI操作,還有一些其他的費時操作,如網(wǎng)絡(luò)請求、文件讀取操作、AR模型下載等。此時就需要開發(fā)者把費時的操作放到子線程中去,完成后再返回住線程執(zhí)行一些UI操作。


11.png

如上圖所示,當(dāng)進入這個Controller時開始下載圖片,由于下載的圖片較多需要幾秒甚至十幾秒,在下載時我同步滑動底部的UISlider,此時UISlider并沒有滑動,直到所有的圖片全部下載完成后才滑動。

試想如果我們的使用的每個App在費時操作時都需要等待很長時間用戶才能操作,這樣勢必會給用戶很不好的體驗。此時的解決方案是創(chuàng)建一個新的線程來下載圖片,這樣既不影響用戶的UI操作也不影響圖片下載。

3、多線程的實現(xiàn)方式

Apple為開發(fā)者提供的三種多線程實現(xiàn)方式:

(1)、 Thread

  • 輕量級
  • 需要開發(fā)者手動管理線程生命周期和線程同步

(2)、 GCD(Grand Central Dispatch)

  • 相對Thread而言,不需要管理線程生命周期,操作更簡單
  • 本身維護了一個線程池,會自動根據(jù)當(dāng)前手機系統(tǒng)的情況來動態(tài)管理線程,不需要開發(fā)者來管理線程池和線程并發(fā)情況
  • 底層源碼是開源的,點擊Apple Open Spurce查看源碼

(3)、 Cocoa Operation

  • 面向?qū)ο蟮腁PI
  • 可以取消、依賴、任務(wù)優(yōu)先級、可以子類化

4、多線程常用隊列

多線程可以根據(jù)任務(wù)執(zhí)行的隊列方式分為三種隊列:

  • 主隊列: 在主線程中執(zhí)行的任務(wù)
  • 串行隊列(Serial Queue): 任務(wù)按照先后順序執(zhí)行,同一時刻只會執(zhí)行一個任務(wù)
  • 并行隊列(Concurrent Queue): 多個任務(wù)同時執(zhí)行,完成的順序不一定

參考 Apple Developer About Dispatch Queues

5、Serial Queue

  • (1)、串行隊列處理并發(fā)任務(wù)

前面說過,對于一些耗時操作,一般將其放到一個子線程中執(zhí)行,待完成后再返回主線程中刷新UI。核心代碼如下:

// 圖片下載管理類`DownloaderManager`
public class DownloaderManager: NSObject {
    public class func downloadImageWithURL(_ url: String) -> UIImage? {
        guard let data = try? Data(contentsOf: URL(string: url)!) else { return nil }
        return UIImage(data: data)
    }
}

// 創(chuàng)建一個隊列
let serialQueue = DispatchQueue(label: "TSN.RPChat.io")
for (index, imgurl) in DownloaderManager.imageArray.enumerated() {
   // 把下載任務(wù)加載到隊列中
   serialQueue.async { 
      let image = DownloaderManager.downloadImageWithURL(imgurl)
      DispatchQueue.main.async {
         girlsImg.image = image
      }
   }
}

此時再次運行代碼,發(fā)現(xiàn)在下載圖片的同時也能滑動UISlider,同時發(fā)現(xiàn)圖片的加載順序是按照從上到下的順序加載的。默認情況下,系統(tǒng)會創(chuàng)建一個串行隊列,也就是下載完成第一張圖片之后再去下載第二張。此時對我來說,我的目的是下載并把圖片全部顯示出來,我并不關(guān)心圖片的下載和加載順序。這樣我就需要在創(chuàng)建隊列時設(shè)置這個隊列為一個并行隊列。

串行隊列

6、Concurrent Queue

作為隊列,concurrent queue中的任務(wù)雖然是按照進入隊列的順序啟動,但不用等待之前的任務(wù)完成,iOS會根據(jù)當(dāng)前系統(tǒng)情況啟動多個線程并行執(zhí)行隊列中的任務(wù)。

在創(chuàng)建隊列時設(shè)置attributes屬性為concurrent就創(chuàng)建了一個并行隊列。

let concurrentQueue = DispatchQueue(label: "TSN.RPChat.io", attributes: .concurrent)

此時運行工程,可以看到圖片并不是按照先后順序加載的,說明同一個concurrent queue中的所有任務(wù)在并行執(zhí)行。

Concurrent Queue

7、面向?qū)ο蟮腃ocoa Operation

上面創(chuàng)建的隊列我使用的GCD(Grand Central Dispatch)方式,盡管GCD對線程管理進行了封裝并加入了面向?qū)ο蠊芾砟J?。但是如果我要對一個隊列中的任務(wù)做更多的操作,如(查看狀態(tài)、取消任務(wù),控制任務(wù)的執(zhí)行順序等)仍然不太方便??紤]到這些問題,蘋果為開發(fā)這提供了一個面向?qū)ο蠓绞降亩嗳蝿?wù)執(zhí)行機制Operation。Operation是基于GCD的對象封裝。

(1)、Operation概覽

Operation的一些使用狀態(tài):

  • isReady是否可執(zhí)行,一般用于異步的情況下
  • isExexuting標(biāo)記Operation是否正在執(zhí)行中
  • isFinished標(biāo)記Operation是否已經(jīng)執(zhí)行完成了,一般用于異步
  • isCancelled標(biāo)記Operation是否已經(jīng)cancel

更多狀態(tài)請參考Apple Developer: Maintaining Operation Object States

(2)、OperationQueue

  • OperationQueue可以加入多個Operation
let ope1 = Operation()
let ope2 = Operation()
       
let que = OperationQueue()
que.addOperation(ope1)
que.addOperation(ope2)
  • maxConcurrentOperationCount可設(shè)置最大并發(fā)數(shù)當(dāng)前,默認情況下,系統(tǒng)會根據(jù)當(dāng)前情況動態(tài)確定最大并發(fā)數(shù)
que.maxConcurrentOperationCount = 5

此處需要注意的是最大并發(fā)數(shù)并不是線程數(shù),最大并發(fā)數(shù)表示的是當(dāng)前隊列最多可同時執(zhí)行的的任務(wù)(或線程)數(shù)量。

  • 可取消所有Operation,但當(dāng)前正在執(zhí)行的Operation不會取消
  • 所有的Operation執(zhí)行完畢后退出銷毀

(3)、BlockOperation

let queblock = BlockOperation.init(block: { [weak self] in
            
})
let que = OperationQueue.init()
que.maxConcurrentOperationCount = 3
que.addOperation(queblock)

(4)、completionBlock

當(dāng)執(zhí)行完一個任務(wù)時的回調(diào).
我們可以通過創(chuàng)建Operation的方法,首先創(chuàng)建一個Operation對象,然后將其添加到隊列中,這樣做就可以通過設(shè)置completionBlock,在任務(wù)完成時得到通知。

(5)、默認優(yōu)先級

蘋果為Operation提供了優(yōu)先級,Operation通過qualityOfService屬性來控制其優(yōu)先級,來看源碼:

public enum QualityOfService : Int {
    
    case userInteractive = 33

    case userInitiated = 25

    case utility = 17

    case background = 9

    case `default` = -1
}
  • userInteractive: 最高優(yōu)先級,用于用戶交互事件
  • userInitiated:次高優(yōu)先級,用于用戶需要馬上執(zhí)行的事件
  • utility:普通優(yōu)先級,用于普通任務(wù)
  • background:最低優(yōu)先級,用于不重要的任務(wù)
  • default:默認優(yōu)先級,主線程和沒有設(shè)置優(yōu)先級的線程都默認為這個優(yōu)先級
operation.qualityOfService = .default

通過queuePriority屬性來控制在OperationQueue中的優(yōu)先級:

public enum QueuePriority : Int {

        case veryLow = -8

        case low = -4

        case normal = 0

        case high = 4

        case veryHigh = 8
    }
operation.queuePriority = .high

(6)、使用OperationQueue下載圖片

// 創(chuàng)建一個隊列
let queue = OperationQueue()

// 創(chuàng)建一個Operation
queue.addOperation {
et image = DownloaderManager.downloadImageWithURL(imgurl)
OperationQueue.main.addOperation {
     girlsImg.image = image 
   }
}

這里需要注意的是:更新UI的代碼要放到主線程中完成。使用OperationQueue.main獲取主線程隊列,然后添加addOperation把更新UI的任務(wù)放到主線程。
如果需要在下載完成時做一些相關(guān)操作,可以使用completionBlock,

let operation = BlockOperation(block: {
     // 要執(zhí)行的任務(wù),如下載圖片等
     let image = DownloaderManager.downloadImageWithURL(imgurl)
     // 下載完成后,返回主線程渲染圖片
     OperationQueue.main.addOperation {
         girlsImg.image = image
     }
})
operation.completionBlock = {
     // 執(zhí)行完成后
}
// 設(shè)置最大并發(fā)數(shù) 不設(shè)置時系統(tǒng)回根據(jù)當(dāng)前情況動態(tài)設(shè)置最大并發(fā)數(shù) 設(shè)置為1時為串行隊列
queue.maxConcurrentOperationCount = 5
// 將Operation添加到queue隊列中
queue.addOperation(operation)

(7)、設(shè)置任務(wù)之間的關(guān)聯(lián)性

設(shè)置任務(wù)之間的關(guān)聯(lián)性

如圖所示,當(dāng)點擊download按鈕的時候開始下載圖片,但是客戶要求按照432的順序加載,但是圖片1不影響,此處需要用到addDependency方法,讓圖片按照432的順序下載,圖片1并行下載,核心代碼如下:

let queue = OperationQueue()

let operation1 = BlockOperation(block: {
       let image = DownloaderManager.downloadImageWithURL(imgArray[0])
       OperationQueue.main.addOperation {
           self.girlsImg1.image = image
    }
})
operation1.completionBlock = {
    print("-------operation1")
 }
        
let operation2 = BlockOperation(block: {
        let image = DownloaderManager.downloadImageWithURL(imgArray[1])
        OperationQueue.main.addOperation {
           self.girlsImg2.image = image
     }
})
operation2.completionBlock = {
     print("-------operation2")
}
        
let operation3 = BlockOperation(block: {
    let image = DownloaderManager.downloadImageWithURL(imgArray[2])
        OperationQueue.main.addOperation {
          self.girlsImg3.image = image
    }
})
operation3.completionBlock = {
   print("-------operation3")
}
        
let operation4 = BlockOperation(block: {
    let image = DownloaderManager.downloadImageWithURL(imgArray[7])
    OperationQueue.main.addOperation {
         self.girlsImg4.image = image
    }
})
operation4.completionBlock = {
    print("-------operation4")
}

operation3.addDependency(operation4)
operation2.addDependency(operation3)
        
queue.addOperation(operation1)
queue.addOperation(operation4)
queue.addOperation(operation3)
queue.addOperation(operation2)

此處把添加OperationQueue的操作,放到了addDependency之后,確保執(zhí)行前有正確的依賴關(guān)系。多次運行代碼可以看到,圖片的下載順序依然是4->3->2的順序。

-------operation1
-------operation4
-------operation3
-------operation2

(8)、取消執(zhí)行的任務(wù)

除了設(shè)置一個隊列中任務(wù)關(guān)聯(lián)性之外,還可以控制取消隊列中的任務(wù),但是取消的結(jié)果會根據(jù)任務(wù)的狀態(tài)而不同:

  • 已經(jīng)完成的任務(wù),取消不影響其結(jié)果
  • 當(dāng)一個任務(wù)被取消時所有與其關(guān)聯(lián)的任務(wù)也會被取消
  • 任務(wù)被取消后,completionBlock依舊會執(zhí)行

取消圖片下載

如圖所示,當(dāng)我點擊download按鈕后快速點擊cancel按鈕,可以看到圖片一的下載任務(wù)被cancel了。

let cancelItem = UIBarButtonItem()
cancelItem.title = "cancel"
cancelItem.rx.tap.subscribe(onNext: {
   self.queue.cancelAllOperations()
}).disposed(by: disposeBag)

此處可以通過OperationisCancelled屬性來判斷任務(wù)是否被cancel。當(dāng)isCancelled返回true表示該任務(wù)被cancel了。

-------operation4,false
-------operation3,false
-------operation2,false
-------operation1,true

本文主要簡單介紹了Operation的一些簡單應(yīng)用,正確的理解和應(yīng)用這些多線程技術(shù)是構(gòu)建復(fù)雜App的基礎(chǔ),關(guān)于更多多線程的應(yīng)用可參考官方多線程的文檔:

Apple Developer Threading Programming Guide

Apple Developer About Dispatch Queues

本文demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容