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操作。

如上圖所示,當(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í)行。

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)性

如圖所示,當(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)
此處把添加Operation到Queue的操作,放到了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)
此處可以通過Operation的isCancelled屬性來判斷任務(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