在開發中常常會用到多線程來處理一些比較耗時的任務,好比下載的時候存儲數據、當進入一個新頁面的時候將網絡請求放在後臺,數據下來以後再到主線程來將數據展現出來等操做,以此來知足用戶大老爺的體驗,讓他們開開心心的用咱們開發出來的應用而不是用的時候一臉懵逼的等待響應T T。日常在開發的過程當中,咱們只需將耗時應用放在後臺的子線程、任務結束以後回到主線程來刷新頁面就行了。基本下面的幾行代碼是咱們最經常使用到的:php
dispatch_async(dispatch_get_global_queue(0, 0), ^{ //後臺處理代碼 etc.... dispatch_async(dispatch_get_main_queue(), ^{ //回到主線程刷新頁面 。。。 }); });
ios
GCD的這幾行代碼跟萬金油同樣解決了咱們常常遇到的下載東西的時候界面切換卡頓的問題,屢試不爽,致使若是咱們遇到了其餘的一些需求,好比:爲了最大限度的減小對應用效率的影響,將cell中須要顯示的幾張小圖片下載下來以後渲染成一張大圖片來顯示(畢竟GPU渲染一張圖片要比渲染幾張小圖片要快的多,若是這種cell比較多的話,那麼效果就更明顯了),遇到這樣的問題怎麼辦呢?首先下載過程確定是要放在後臺的,那麼怎麼判斷圖片都下載好了呢?確定都會想到「添加線程依賴呀,用group管理呀」什麼的..可是一寫老是會遇到各類問題,最主要的緣由就是那幾行萬金油讓咱們變得愈來愈懶了,愈來愈不想去研究後臺處理的其餘東西了。這一段時間比較閒,就仔細的研究了下多線程的東西,包括不少帖子以及官方文檔,一看不得了啊,總結了一堆使用方法,少年看你骨骼驚奇,是一塊打LOL的。。呸。。寫程序好材料,不如寫下來我本身查閱的同時給大家看看咯。。。編程
多線程主要會用到有三大類,NSThread,NSOperation,GCD,且聽我細細道來數組
NSThread 比其餘兩個輕量級,使用簡單。須要本身管理線程的生命週期、線程同步、加鎖、睡眠以及喚醒等。線程同步對數據的加鎖會有必定的系統開銷網絡
NSOperation 不須要關心線程管理,數據同步的事情,能夠把精力放在本身須要執行的操做上多線程
Grand Central Dispatch(GCD)是由蘋果開發的一個多核編程的解決方案。iOS4.0+才能使用,是替代NSThread, NSOperation的高效和強大的技術,GCD是基於C語言的。併發
建立方法async
- (instancetype)init; - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument
ide
若是要使用init方法來建立的話,是比較麻煩的,通常用在比較特殊的狀況,好比繼承NSTread定義本身須要使用的類,通常使用第二種方法建立,使用方法以下:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(fun) object:nil]; [thread start];
當咱們建立好thread以後,須要使用start方法來啓動這個線程,這樣就很簡單的把方法fun放到子線程中去執行了。須要注意的是,官方API中有這句話:
The objects target and argument are retained during the execution of the detached thread. They are released when the thread finally exits.
當線程執行的時候,會對target對象和傳入的參數的引用計數加1,調試的時候要注意下。
取消方法
咱們能夠經過線程的狀態屬性判斷當前線程的狀態,其有三種屬性,好比:
@property (readonly, getter=isExecuting) BOOL executing @property (readonly, getter=isFinished) BOOL finished @property (readonly, getter=isCancelled) BOOL cancelled
咱們能夠經過這三種屬性來判斷當前線程的狀態。那麼,咱們該如何取消一個線程呢,根據官方的API來看的話,確實是有一個cancel方法,可是實際上當咱們調用這個方法的時候線程並不會中止執行,根據官方文檔的描述,當咱們調用cancel方法的時候,僅僅是將線程的狀態標記爲了cacel,並不會取消當前線程的執行,若是咱們要手動取消的話,建議採用下面的代碼
if ([NSThread currentThread].cancelled) { [NSThread exit]; }
將線程標記爲取消以後,NSThread將會發送一個通知,若是寫了響應通知的方法的話,以下所示:
-(void)listen:(NSNotification*)notification{ NSLog(@"exit===%@",notification.object); }
那麼將會得到被結束線程的信息
exit===<NSThread: 0x7fed50f9e820>{number = 2, name = myThread}//myThread是我給線程起的名字
一樣的,當線程剛剛啓動的時候,也會發送一個NSWillBecomeMultiThreadedNotification通知,這個通知是主線程將會產生子線程,感興趣的看官能夠攔截看一下哦。
NSThread的其餘功能
-(void)sleepUntilDate:(NSDate *)aDate 讓當前線程休眠,經過阻塞當前的runloop來實現,使得當前進程不能夠響應任何方法
+(void)sleepForTimeInterval:(NSTimeInterval)ti 功能、原理同上,只不過傳入的參數類型不一樣
@property(copy) NSString *name 設置當前線程的名字
使用NSOperation來實現多線程處理任務的話,通常使用的是它的兩個子類,NSInvocationOperation和NSBlockOperation
- 建立方法
NSInvocationOperation建立方法 - (instancetype)initWithTarget:(id)target selector:(SEL)sel object:(id)arg - (instancetype)initWithInvocation:(NSInvocation *)inv NSBlockOperation建立方法 + (instancetype)blockOperationWithBlock:(void (^)(void))block
當咱們建立好operation後,調用start方法就能夠啓動這個operation,以下所示(以NSInvocationOperation爲例,NSBlockOperation使用方法相似):
NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(fun) object:nil]; [invocation start];
可是須要注意的是,咱們這樣建立出來的任務,並非在子線程裏運行的,就是說若是是在主線程裏啓動這個opreation的話,那麼這個任務就是在主線程隊列運行的;若是是在子線程開啓的,則是在子線程運行的,因此咱們須要建立一個NSOperationQueue,而後將這個兩個操做添加進去,這樣就保證了操做是在咱們自定義的隊列裏運行的,以下所示:
NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(fun) object:nil]; NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [queue addOperation:invocation];
當將operation添加到隊列之後,operation會自行啓動,不能夠再調用start方法了再次開啓了,這樣會致使程序的異常,由於一個Operation在同一時間最多隻能在一個線程執行。
當咱們建立好了一個blocKOperation以後,能夠調用
- (void)addExecutionBlock:(void (^)(void))block
來向blocKOperation添加任務,可是隻能在blocKOperation沒啓動以前添加,不能夠在啓動以後或者線程執行完了以後添加。並且添加的任務會開啓一個新的線程去執行,不過仍然是在該隊列裏。
咱們還能夠調用
@property(readonly, copy) NSArray <void (^executionBlocks)(void)> *
這個屬性會返回當前添加到當前blocKOperation裏的block組成的數組。
其餘方法
NSOperationQueue常用的方法:
+(NSOperationQueue *)currentQueue 這是個類方法,放回當前線程的隊列,若是當前線程是主線程的話,那麼返回的就是主線程隊列
+(NSOperationQueue *)mainQueue 這也是個類方法,直接獲取主線程
-(void)addOperations:(NSArray
The NSOperation class is key-value coding (KVC) and key-value observing (KVO) compliant for several of its properties. As needed, you can observe these properties to control other parts of your application. To observe the properties, use the following key paths: isCancelled - read-only isAsynchronous - read-only isExecuting - read-only isFinished - read-only isReady - read-only dependencies - read-only queuePriority - readable and writable completionBlock - readable and writable
-(void)addDependency:(NSOperation *)operation 添加依賴操做
(void)removeDependency:(NSOperation *)operation 移除依賴操做
@property(readonly, copy) NSArray
[operation_one addDependency:operation_two]; [operation_two addDependency:operation_three];
而後將這三個operation加入隊列的話,那麼這三個操做將按順序執行。
4.@property(copy) void (^completionBlock)(void) 爲operation設置一個comletionBlock,當operation執行完以後,就會調用這個Block.
最後一個就是GCD的,GCD是基於C的一組API,使用起來很方便,並且上面兩個線程能夠完成的任務,使用GCD基本上均可以完成。GCD的使用方法有不少,常用的方法以下所示。
建立隊列
dispatch_queue_t myQueue = dispatch_queue_create("myQueueName", DISPATCH_QUEUE_SERIAL);
使用dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)方法咱們能夠建立一個自定義的隊列,該方法有兩個參數:
label 數須要傳入一個C語言字符串,做爲該隊列的名字;
attr 代表該隊列是串行隊列仍是併發隊列,其有兩個枚舉值:
DISPATCH_QUEUE_SERIAL 串行隊列 DISPATCH_QUEUE_CONCURRENT 併發隊列
串行隊列裏的任務老是會按添加順序執行,遵循FIFO規則;
而向併發隊列裏添加的任務根據添加方式會選擇併發執行仍是順序執行,後面咱們會細說。
2. 向獲取到的隊列裏添加方法
dispatch_queue_t myQueue = dispatch_queue_create("myQueueName", DISPATCH_QUEUE_SERIAL); //建立一個隊列 dispatch_async(myQueue, 0), ^{ //須要執行的任務 });
上面代碼的執行效果是,向本身建立的myQueue添加執行任務,dispatch_async該方法會馬上返回。不會阻塞當前線程。
須要注意的是,若是dispatch_asyn添加任務的目標隊列是串行隊列的話,那麼該隊列裏的方法會按順序執行;若是目標隊列是併發隊列的話,那麼將會在併發隊列裏開啓多個線程來執行添加的任務(通常狀況下,一個任務對應一個線程),CPU會切片式的執行這些線程任務。
還有一種添加方法:
dispatch_queue_t myQueue = dispatch_queue_create("myQueueName", DISPATCH_QUEUE_SERIAL); //建立一個隊列 dispatch_sync(myQueue, 0), ^{ //須要執行的任務 });
dispatch_sync方法會阻塞當前的線程,直到block塊裏的代碼執行完畢纔會返回。因此不要在主線程裏調用dispatch_sync方法,會致使主線程阻死。
無論dispatch_sync添加任務的隊列是串行隊列仍是併發隊列,添加的任務都是遵循FIFO規則執行的。
3 . 獲取線程
dispatch_queue_t queue = dispatch_get_main_queue(); //獲取主隊列 dispatch_queue_t queue = dispatch_get_global_queue(long identifier, unsigned long flags) //獲取全局併發隊列
dispatch_get_global_queue方法有兩個參數:
identifier 獲取到的全局併發隊列優先級,該參數有四個值:
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 高優先級 #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 默認優先級 #define DISPATCH_QUEUE_PRIORITY_LOW (-2) 低優先級 #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 後臺優先級
當向更高優先級的隊列裏添加任務的時候,若是同時還有其餘的優先級隊列任務的在執行的話,高優先級隊列裏的任務則有更多的執行機會(由於CPU是切片式的在執行任務,高優先級隊列裏的任務將會得到更多的執行),從高優先級-默認優先級-低優先級,通過個人測試,每一個優先級有30%左右的執行頻率差距(該數據僅供參考)。
4.建立及使用隊列組
dispatch_group_t group = dispatch_group_create(); //建立一個組
下面幾行代碼做爲一個簡單的示例介紹組的用法和重要的功能。
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue1, ^{ for(int i = 0; i < 100; i ++){ NSLog(@"111 --- %d",i); } }); dispatch_group_async(group, queue2, ^{ for(int i = 0; i < 100; i ++){ NSLog(@"222 --- %d",i); } }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"完事兒了"); });
上面的代碼中,首先獲取了兩個不一樣優先級的隊列,而後建立了一個組。接着將隊列和要執行的任務添加到組裏面,最後添加一個監測組內隊列執行的block塊,當組裏面全部的任務執行完以後,會調用這個block塊。下面詳細說下添加隊列到組和監測組運行的這兩個方法:
添加隊列到組
dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, ^(void)block)
該方法有三個參數:
- group: 接收添加隊列的組
- queue:將要添加的隊列
- block:在添加的隊列裏執行的Block塊
監測組運行
dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, ^(void)block)
該方法有三個參數:
- group: 要監測的組
- queue: 後面block執行的隊列
- block: 當組裏的任務執行完以後回調的Block塊
使用方法和功能都一目瞭然,最好仍是本身練習一下才會有更深的理解。
GCD的內容不少,若是不深刻了解的話沒法體會他的博大精深,上面列出來的幾個方法只不過是最經常使用的幾個方法,於GCD而言只不過是冰山一角,更多深刻的研究我會在之後寫出來。附贈一張GCD方法圖,小彩蛋:
另外,若是長時間的維持一個子線程(好比須要在子線程裏建立一個tmer),那麼就須要建立一個runloop來維持該線程,不然線程裏的任務執行完以後就會被系統回收掉。下面是引用一蘋果開發者文檔的一個數據表格,主要包括了線程的建立時間以及內存分配大小。
Item |
Approximate cost |
Notes |
---|---|---|
Kernel data structures |
Approximately 1 KB |
This memory is used to store the thread data structures and attributes, much of which is allocated as wired memory and therefore cannot be paged to disk. |
Stack space |
512 KB (secondary threads) |
8 MB (OS X main thread)1 MB (iOS main thread)2The minimum allowed stack size for secondary threads is 16 KB and the stack size must be a multiple of 4 KB. The space for this memory is set aside in your process space at thread creation time, but the actual pages associated with that memory are not created until they are needed. |
Creation time |
Approximately 90 microseconds |
This value reflects the time between the initial call to create the thread and the time at which the thread’s entry point routine began executing. The figures were determined by analyzing the mean and median values generated during thread creation on an Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running OS X v10.5. |
根據這個表格咱們能夠看出來,iOS建立一個子線程分配的棧空間是512KB,最小分配空間是16KB,而主線程的分配空間是1M,根據個人測試,建立子線程的分配空間確實是512KB,不過主線程的棧空間查看的話仍然是512KB,你們能夠本身查看下,並且只有經過NSThrad建立的線程才能夠配置堆棧的大小。另外分配空間是在建立線程的時候完成的,可是實際的頁面相關的內存只有到實際運行的時候在建立。可是下面的建立時間是基於酷睿雙核處理器和1GB運行內存的MAC OS X v10.5.測試出來的,僅供參考,並非手機建立線程的時間。
這篇博文總結了在開發中常常用到多線程的一些方法,不過多線程涉及的內容不少,很重要的就是多線程訪問統一資源形成資源競爭以及線程間互相等待形成線程死鎖問題,這些東西的篇幅都是很大的,我會在之後的博文中慢慢討論。