多線程
多線程技術(shù)是iOS開發(fā)里十分常見的,下面會(huì)介紹GCD的常用多線程技術(shù)。首先簡(jiǎn)單了解一下幾個(gè)概念:
- 同步,異步:任務(wù)執(zhí)行時(shí)是否開多線程,同步為單線程,異步為多線程
- 串行,并發(fā):任務(wù)執(zhí)行的方式,串行為順序執(zhí)行,并發(fā)為同時(shí)執(zhí)行
- 隊(duì)列:任務(wù)存放的地方,線程從隊(duì)列里取出任務(wù)執(zhí)行。分為串行隊(duì)列和并發(fā)隊(duì)列
- 線程:執(zhí)行任務(wù)的實(shí)際運(yùn)作單位,主線程從主隊(duì)列里取任務(wù),更新UI必須在主線程完成
GCD:
GCD是操作最簡(jiǎn)單,也是我們最常用的多線程技術(shù)。在OC里是一套C語言API,在Swift里對(duì)其封裝進(jìn)一步簡(jiǎn)化,本文會(huì)使用Swift語法。
一。主隊(duì)列,全局隊(duì)列
先來看看這個(gè)GCD的最常用公式:
DispatchQueue.global().async {
//long time operation
DispatchQueue.main.async {
//update UI
}
}
它分為以下幾個(gè)步驟:
- 通過
DispatchQueue.global()獲取全局隊(duì)列 - 在全局隊(duì)列中
async并發(fā)執(zhí)行耗時(shí)操作,如下載 - 通過
DispatchQueue.main獲取全局隊(duì)列 - 在全局隊(duì)列中
async并發(fā)執(zhí)行更新UI的動(dòng)作
那么為什么要如此使用呢?
- 首先,更新UI的操作必須放在主線程中,否則會(huì)報(bào)錯(cuò)。這里
DispatchQueue.main是獲取主隊(duì)列,它是一個(gè)串行隊(duì)列,而主線程要執(zhí)行的任務(wù)就是到這個(gè)隊(duì)列里去取的。 -
DispatchQueue.global()是獲取系統(tǒng)提供的全局隊(duì)列,該隊(duì)列是并發(fā)隊(duì)列。加入該隊(duì)列的任務(wù)會(huì)開辟多條線程共同執(zhí)行,就好像一個(gè)頁面會(huì)下載顯示很多張圖片,用全局隊(duì)列的話圖片會(huì)一張張顯示出來,而如果用主隊(duì)列的話,所有圖片下載完成才統(tǒng)一顯示。 - 這里不管是主隊(duì)列還是全局隊(duì)列都使用
async,而沒有使用sync是因?yàn)?code>sync同步操作,相當(dāng)于將所有操作都由主線程來執(zhí)行,這會(huì)阻塞主線程,也就是UI上的卡頓。
這里寫了一個(gè)小實(shí)驗(yàn)比較上面的各種情況。(因?yàn)閳D片地址在公司服務(wù)器上,這里不公布代碼,只看結(jié)果)。
點(diǎn)擊一個(gè)button,會(huì)Push到下一個(gè)頁面,而下一個(gè)頁面中會(huì)有一個(gè)CollectionView,共包含10個(gè)CollectionViewCell,每一個(gè)cell中都會(huì)下載并顯示一張圖片。
-
第一張圖用的是之前的標(biāo)準(zhǔn)公式,點(diǎn)擊按鈕后立即Push到下一個(gè)頁面,并且圖片也相繼顯示出來。
GCDDemo1.gif
-
第二張圖是將
DispatchQueue.global()換成了DispatchQueue.main()??梢钥吹?,點(diǎn)擊按鈕后立即Push到下一個(gè)頁面,所有圖片下載完成后再一起顯示,這是因?yàn)橹麝?duì)列是串行隊(duì)列,所有的更新UI任務(wù)都排在了下載任務(wù)之后,因此要等所有的圖片下載完成后,UI才會(huì)開始顯示。[圖片上傳中...(GCDDemo3.gif-83a055-1564041778608-0)]
-
第三張圖是將
DispatchQueue.global().async換成了DispatchQueue.global().sync??梢钥吹剑c(diǎn)擊按鈕后卡頓了一會(huì)兒,再進(jìn)入下一個(gè)頁面。這是因?yàn)?code>sync是同步操作,會(huì)阻塞主線程。GCDDemo3.gif
由以上三張圖可看出,常用公式是最合適的選擇
二。創(chuàng)建隊(duì)列
let queue = DispatchQueue(label: "com.demo.queue", qos: .background, attributes: .concurrent)
- qos是指隊(duì)列的優(yōu)先級(jí),從background到userinteractive,優(yōu)先級(jí)從低到高
- concurrent指定了該隊(duì)列為并發(fā)隊(duì)列,若不穿,則默認(rèn)為串行隊(duì)列
自建的隊(duì)列最終會(huì)被歸入到主隊(duì)列和全局隊(duì)列中去,那么為什么還要?jiǎng)?chuàng)建它們呢,也是便于對(duì)一系列任務(wù)的管理。
三。派發(fā)組DispatchGroup
開發(fā)中也許會(huì)有這一種情況,我們要同時(shí)請(qǐng)求好幾個(gè)接口后,再刷新UI。這種對(duì)多個(gè)異步請(qǐng)求的管理,用DispatchGroup最合適。
用法如下
let group = DispatchGroup()
group.enter()
//task1
group.leave()
group.enter()
//task2
group.leave()
group.enter()
//task3
group.leave()
group.notify(queue: DispatchQueue.main) {
//update UI
print("更新UI")
}
更新UI的操作會(huì)在task1,task2,task3這3個(gè)異步任務(wù)完成后再執(zhí)行。
四。信號(hào)量semaphore
簡(jiǎn)單的說,在異步操作中,任務(wù)完成的順序是不確定的。semaphore可以使得我們將異步操作按順序同步完成。
這里有一片博客,對(duì)其進(jìn)行了詳細(xì)的介紹
五。屏障barrier
想象有這樣一個(gè)操作。從數(shù)據(jù)庫里執(zhí)行兩次讀的任務(wù)read1和read2,并發(fā)執(zhí)行并沒有什么問題??扇绻趓ead1和read2中間加入一個(gè)write1,要求read1讀取的是write之前的數(shù)據(jù),read2讀取的是write之后的數(shù)據(jù),那么應(yīng)該如何處理呢?
首先,如果write不能用普通的并發(fā)操作。因?yàn)椴l(fā)隊(duì)列的特性是無法確保read1,read2以及write的執(zhí)行順序,這可能會(huì)發(fā)生read2讀取的是write之前的數(shù)據(jù)。
這里我們需要用到barrier。顧名思義,它是作為一個(gè)屏障,隔開了read1和read2,在這個(gè)屏障內(nèi)的進(jìn)行write,也會(huì)保證,read1->write->read2這樣的一個(gè)執(zhí)行順序
DispatchQueue.global().async {
//read1
}
DispatchQueue.global().async(flags: .barrier) {
//write
}
DispatchQueue.global().async {
//read2
}
六。延遲執(zhí)行
這里用GCD封裝一個(gè)延遲執(zhí)行
func delay(_ timeInterval: TimeInterval, closure: @escaping ()->()) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + timeInterval, execute: closure)
}
調(diào)用起來非常簡(jiǎn)單和明了
delay(3) {
//task
}
值的一體的是,該延遲執(zhí)行指的并不是在3秒后執(zhí)行task,而指的是,在3秒后,將task加入主隊(duì)列。所以當(dāng)主隊(duì)列有大量處理或者本身已經(jīng)延遲的情況下,真正執(zhí)行的時(shí)間會(huì)比3秒更長(zhǎng)。不過一般來說,在主隊(duì)列非阻塞的情況下,該方法還是算非常簡(jiǎn)潔有效的。
七。創(chuàng)建單例
得益于GCD中的dispatch_once,這個(gè)函數(shù)會(huì)讓程序只執(zhí)行一次,我們?cè)贠C中用它來實(shí)現(xiàn)單例。
static dispatch_once_t pred;
dispatch_once(pred, ^{
//init
});
不過為了防止單例類通過alloc或者new的方法實(shí)例化,實(shí)際上要寫的代碼還有很多,相比較下來Swift的單例實(shí)現(xiàn)就要簡(jiǎn)單很多。
class Singleton {
static let shared = Singleton()
private init() {}
}
八。死鎖
死鎖一般是指同步任務(wù)的相互等待,造成程序崩潰。
例如,我們?cè)赩iewController的ViewDidLoad里加入這一句,程序執(zhí)行到這里立刻就會(huì)崩潰。
DispatchQueue.main.sync {}
因?yàn)閂iewDidLoad是從主線程執(zhí)行,而DispatchQueue.main.sync {}也是在主線程里同步執(zhí)行。ViewDidLoad需要等待DispatchQueue.main.sync {}完成,但ViewDidLoad又是先加入主隊(duì)列的,因此DispatchQueue.main.sync {}要執(zhí)行必須先等ViewDidLoad執(zhí)行完畢。因此他們相互等待,造成死鎖。


