最淺顯易懂的iOS多線程技術 - GCD的教程
筆者勵志打造一篇淺顯易懂地介紹iOS中GCD的文章!
筆者見過很多其他講解GCD的博客,有些寫得非常詳細非常專業,幾乎涵蓋了GCD大大小小的全部知識,細致龐雜的內容容易讓人摸不清主次,筆者覺得這類文章 并不適合初學者學習 ,于是決定寫一篇針對一些只是聽過,但是對GCD還不了解的童鞋們。
本文排除了一些細枝末節,擾亂人頭緒的東西,著重講解了GCD中重要的知識點,并在最后展示了GCD中 經常使用的函數 并附上結果圖和講解,簡單明了。
進程與線程
在了解多線程之前,需要弄清進程和線程的概念和他們之間的區別。
進程:
系統中正在運行的一個程序,進程之間是相互獨立的,每個進程都有屬于自己的內存空間。比如手機中的 微信 應用和 印象筆記 應用,他們都是iOS系統中獨立的進程,有著自己的內存空間。
線程:
進程內部執行任務所需要的執行路徑。進程若想執行任務,則必須得在線程下執行。也就是說進程至少有一個線程才能執行任務。但是,我們使用軟件的時候,很少有只讓它做一件事的時候:
舉個 印象筆記 的:chestnut: : 當你正在編輯一則筆記的時候點擊了同步按鈕,那么編輯任務(線程)和同步任務(線程)一定是不能按照順序執行的。因為同步任務的完成時間是不可控的,如果在同步的過程中無法進行別的任務(線程)那就太糟糕了!
因此,我們需要讓一些任務可以同時進行。既然任務是在線程上執行的,那么多任務的執行就意味著需要多線程的開啟和使用。
來一張圖直觀地展示一下內存,進程和線程的關系:
內存,進程和線程
多線程概述
多線程的實現原理:雖然在同一時刻,CPU只能處理1條線程,但是CPU可以快速地在多條線程之間調度(切換),造成了多線程并發執行的假象。
1. 多線程的優點
- 能適當提高程序的執行效率。
- 能適當提高資源利用率(CPU、內存利用率)。
2. 多線程的缺點
- 創建線程是需要成本的:iOS下主要成本包括:在棧空間的子線程512KB、主線程1MB,創建線程大約需要90毫秒的創建時間。
- 線程越多,CPU在調度線程上的開銷就越大。
- 線程越多,程序設計就越復雜:因為要考慮到線程之間的通信,多線程的數據共享。
多線程在iOS開發中的應用
1. iOS的主線程
一個iOS程序運行后,默認會開啟1條線程,稱為“主線程”或“UI線程”
主線程的作用:
- 顯示\刷新UI界面
- 處理UI事件(比如點擊事件、滾動事件、拖拽事件等)
主線程的使用 注意事項 :
不能把比較耗時的操作放到主線程中,,嚴重影響UI的流暢度,給用戶一種程序“卡頓”的體驗。
因此,要將耗時的操作放在子線程中異步執行。這樣一來,及時開始執行了耗時的操作,也不會影響主線程中UI交互的體驗。
2. iOS的子線程
子線程是異步執行的,不影響主線程。在iOS開發中,我們需要將耗時的任務(網絡請求,復雜的運算)放在子線程進行,不讓其影響UI的交互體驗。
3. 多線程安全
當多個線程訪問同一塊資源時,很容易引發數據錯亂和數據安全問題。就好比好幾個人在同時修改同一個表格,造成數據的錯亂。
3.1 資源搶奪的解決方案
我們需要給數據添加 互斥鎖 。也就是說,當某線程訪問一個數據之前就要給數據加鎖,讓其不被其他的線程所修改。就好比一個人修改表格的時候給表格設置了密碼,那么其他人就無法訪問文件了。當他修改文件之后,再講密碼撤銷,第二個人就可以訪問該文件了。
注意:
這里的線程都為子線程,如果給數據加了鎖,就等于將這些異步的子線程變成同步的了,這也叫做線程同步技術。
3.2 互斥鎖使用:
@synchronized(鎖對象) { // 需要鎖定的代碼 };
3.3 互斥鎖的優缺點
優點:能有效防止因多線程搶奪資源造成的數據安全問題
缺點:需要消耗大量的CPU資源
互斥鎖的使用前提:多條線程搶奪同一塊資源的時候使用。
3.4互斥鎖在iOS開發中的使用
OC在定義屬性時有 nonatomic 和 atomic 兩種選擇
- atomic:原子屬性,為setter方法加鎖(默認就是atomic)
- nonatomic:非原子屬性,不會為setter方法加鎖
3.5 nonatomic和atomic對比
atomic:線程安全,需要消耗大量的資源
nonatomic:非線程安全,適合內存小的移動設備
建議:
所有屬性都聲明為nonatomic,盡量避免多線程搶奪同一塊資源,將加鎖、資源搶奪的業務邏輯交給服務器端處理,減小移動客戶端的壓力。
多線程在iOS中的應用:GCD
GCD,全稱為 Grand Central Dispatch ,是iOS用來管理線程的技術。 純C語言,提供了非常多強大的函數。
1. GCD的優勢
GCD會自動利用更多的CPU內核(比如雙核、四核)。
GCD會自動管理線程的生命周期(創建線程、調度任務、銷毀線程)。
程序員只需要告訴GCD想要執行什么任務,不需要編寫任何線程管理代碼。
2. 為什么要用GCD?
為了要提高軟件性能,應該異步執行耗時任務(加載圖片),以防止影響主線程任務的執行(UI相應)。
舉個:chestnut: :
從網絡加載一張圖片,如果將此任務放到主線程,那么在下載完成的時間里,軟件是無法相應用戶的任何操作的。特別地,如果當前是在可以滾動的頁面,就會造成無法滾動這種體驗非常糟的情況。
所以:應該將網絡加載放在異步執行,執行成功后,再回到主線程顯示加載后的圖片(詳細做法馬上就會講到)。
3. GCD的使用步驟
- 由開發者定制將要執行的任務。
- 將任務添加到隊列中,GCD會自動將隊列中的任務取出,放到對應的線程中執行。
注意:
任務的取出遵循隊列的FIFO原則:先進先出,后進后出。
4. 什么是隊列?
隊列是用來存放任務的,由GCD將這些任務從隊列中取出并放到相應的線程中執行。
GCD的隊列可以分為2大類型:
1. 并發隊列(Concurrent Dispatch Queue)
可以讓多個任務并發(同時)執行(自動開啟多個線程同時執行任務),并發功能只有在異步(dispatch_async)函數下才有效
2. 串行隊列(Serial Dispatch Queue)
讓任務一個接著一個地執行(一個任務執行完畢后,再執行下一個任務)。
那么隊列和線程又有什么區別?
簡單來說,隊列就是用來存放任務的“暫存區”,而線程是執行任務的路徑,GCD將這些存在于隊列的任務取出來放到相應的線程上去執行,而隊列的性質決定了在其中的任務在哪種線程上執行。
下面由一張圖來直觀地展示任務,隊列和線程的關系:
任務,隊列和線程
在這里,我們可以看到,放入串行隊列的任務會一個一個地執行。而放入并行隊列的任務,會在多個線程并發地執行。
5. 隊列的創建
5.1 串行隊列的創建:
GCD中獲得串行有2種途徑:
1.使用 dispatch_queue_create 函數創建串行隊列
// 創建串行隊列(隊列類型傳遞NULL或者DISPATCH_QUEUE_SERIAL) dispatch_queue_t queue = dispatch_queue_create("serial_queue", NULL);
2.使用主隊列(跟主線程相關聯的隊列)
主隊列是GCD自帶的一種特殊的串行隊列:放在主隊列中的任務,都會放到主線程中執行。
可以使用dispatch_get_main_queue()獲得系統提供的主隊列:
dispatch_queue_t queue = dispatch_get_main_queue();
5.2 并發隊列的創建:
1.使用 dispatch_queue_create 函數創建并發隊列。
dispatch_queue_t queue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
2.使用 dispatch_get_global_queue 獲得全局并發隊列。
GCD默認已經提供了全局的并發隊列,供整個應用使用,可以無需手動創建。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
6. GCD的幾種重要的應用
6.1 子線程與主線程的通信
需求點:我們有時需要在子線程處理一個耗時比較長的任務,而且此任務完成后,要在主線程執行另一個任務。
例子:從網絡加載圖片(在子線程),加載完成就更新UIView(在主線程)。
為了實現這個需求,我們需要首先拿到全局并發隊列(或自己開啟一個子線程)來執行耗時的操作,然后在其完成block中拿到全局串行隊列來執行UI刷新的任務。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //加載圖片 NSData *dataFromURL = [NSData dataWithContentsOfURL:imageURL]; UIImage *imageFromData = [UIImage imageWithData:dataFromURL]; dispatch_async(dispatch_get_main_queue(), ^{ //加載完成更新view UIImageView *imageView = [[UIImageView alloc] initWithImage:imageFromData]; }); });
以筆者的拙見,除了復雜的算法,網絡請求以外,大多數 dataWithContentsOf。。。 函數可能也會比較耗時,所以以后遇到與NSData交互的操作時,盡量將其放在子線程執行。
6.2 dispatch_once
需求點:用于在程序啟動到終止,只執行一次的代碼。此代碼被執行后,相當于自身全部被加上了注釋,不會再執行了。
為了實現這個需求,我們需要使用 dispatch_once 讓代碼在運行一次后即刻被“雪藏”。
//使用dispatch_once函數能保證某段代碼在程序運行過程中只被執行1次 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 只執行1次的代碼,這里默認是線程安全的:不會有其他線程可以訪問到這里 });
6.3 dispatch_group
需求點:執行多個耗時的異步任務,但是只能等到這些任務都執行完畢后,才能在主線程執行某個任務。
為了實現這個需求,我們需要讓將這些異步執行的操作放在 dispatch_group_async 函數中執行,最后再調用 dispatch_group_notify 來執行最后執行的任務。
dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行1個耗時的異步操作 }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行1個耗時的異步操作 }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 等前面的異步操作都執行完畢后,回到主線程... });
讓我們看一下示例代碼和運行結果:
示例代碼:
為了使對比明顯,筆者多開了幾條線程,這樣更容易看清問題。
dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行1個耗時的異步操作 for (NSInteger index = 0; index < 10000; index ++) { } NSLog(@"完成了任務1"); }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行1個耗時的異步操作 for (NSInteger index = 0; index < 20000; index ++) { } NSLog(@"完成了任務2"); }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行1個耗時的異步操作 for (NSInteger index = 0; index < 200000; index ++) { } NSLog(@"完成了任務3"); }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行1個耗時的異步操作 for (NSInteger index = 0; index < 400000; index ++) { } NSLog(@"完成了任務4"); }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行1個耗時的異步操作 for (NSInteger index = 0; index < 1000000; index ++) { } NSLog(@"完成了任務5"); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 等前面的異步操作都執行完畢后,回到主線程... NSLog(@"都完成了"); });
運行結果:
dispatch_group 的使用運行結果
從三次運行的結果來看:
-
異步執行的任務1-5的最終完成時間是與其自身完成任務所需要的時間并無絕對關聯。因為任務5是最耗時的,它在第一次運行結果里并不是最后才完成的。任務1是最不耗時的,但是它在第二次運行結果里也不是最先完成的。
-
異步執行的任務1-5無論完成順序如何,只有當他們都完成后才會調用主線程的打印“都完成了”。
6.4 dispatch_barrier
需求點:雖然我們有時要執行幾個不同的異步任務,但是我們還是要將其分成兩組:當第一組異步任務都執行完成后才執行第二組的異步任務。這里的組可以包含一個任務,也可以包含多個任務。
為了實現這個需求,我們需要使用 dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block); 在兩組任務之間形成“柵欄”,使其“下方”的異步任務在其“上方”的異步任務都完成之前是無法執行的。
dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ NSLog(@"----任務 1-----"); }); dispatch_async(queue, ^{ NSLog(@"----任務 2-----"); }); dispatch_barrier_async(queue, ^{ NSLog(@"----barrier-----"); }); dispatch_async(queue, ^{ NSLog(@"----任務 3-----"); }); dispatch_async(queue, ^{ NSLog(@"----任務 4-----"); });
示例代碼:
dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ for (NSInteger index = 0; index < 10000; index ++) { } NSLog(@"完成了任務1"); }); dispatch_async(queue, ^{ for (NSInteger index = 0; index < 20000; index ++) { } NSLog(@"完成了任務2"); }); dispatch_async(queue, ^{ for (NSInteger index = 0; index < 200000; index ++) { } NSLog(@"完成了任務3"); }); dispatch_barrier_async(queue, ^{ NSLog(@"--------我是分割線--------"); }); dispatch_async(queue, ^{ for (NSInteger index = 0; index < 400000; index ++) { } NSLog(@"完成了任務4"); }); dispatch_async(queue, ^{ for (NSInteger index = 0; index < 1000000; index ++) { } NSLog(@"完成了任務5"); }); dispatch_async(queue, ^{ for (NSInteger index = 0; index < 1000; index ++) { } NSLog(@"完成了任務6"); });
運行結果:
dispatch_barrier 的使用運行結果
從這三次運行結果來看:
- 無論任務1-3內部的執行順序如何,只有當三者都完成了才會執行任務4-6。
- 1-3內部的執行順序和4-6內部的完成順序都是不可控的,同上一個知識點類似。
本文介紹了需要了解GCD所需的最重要的知識,因為怕打斷讀者思路,并沒有涵蓋所有細節。
來自:http://www.jianshu.com/p/6e74f5438f2c