iOS多線程的使用

iOS中,只有主線程跟Cocoa關聯,也即是在這個線程中,更新UI總是有效的,如果在其他線程中更新UI有時候會成功,但可能失敗。所以蘋果要求開發者在主線程中更新UI。但是如果我們吧所有的操作都放置在主線程中執行,當遇到比較耗時的操作的時候,勢必會阻塞線程,出現界面卡頓的情況。這時候采取將耗時的操作放入后臺線程中操作,且保持主線程只更新UI是我們推薦的做法。

在iOS中,要實現多線程,一共有四種方式。? 它們分別是:

pthreadsPOSIX線程(POSIX threads),簡稱Pthreads,是線程的POSIX標準。該標準定義了創建和操縱線程的一整套API。在類Unix操作系統(Unix、Linux、Mac OS X等)中,都使用Pthreads作為操作系統的線程。Windows操作系統也有其移植版pthreads-win32[1]這篇文章不介紹

NSThread需要管理線程的生命周期、同步、加鎖問題,這會導致一定的性能開銷

NSOperation & NSOperationQueue

GCDiOS4開始,蘋果發布了GCD,可以自動對線程進行管理。極大的方便了多線程的開發使用

一、pthread

pthread是一套基于C的API,它不接受cocoa框架的控制:當手動創建pthread的時候,cocoa框架并不知道。 蘋果不建議在cocoa中使用pthread,但是如果為了方便不得不使用,我們應該小心的使用。

下面這些方法可以創建pthread

OC

pthread_attr_t qosAttribute;

pthread_attr_init(&qosAttribute);

pthread_attr_set_qos_class_np(&qosAttribute, QOS_CLASS_UTILITY, 0);

pthread_create(&thread, &qosAttribute, f,NULL);

SWIFT

varthread= pthread_t()

var qosAttribute = pthread_attr_t()

pthread_attr_init(&qosAttribute)

pthread_attr_set_qos_class_np(&qosAttribute, QOS_CLASS_UTILITY, 0)

pthread_create(&thread, &qosAttribute, f,nil)

并且,可以使用下面的API對一個pthread進行修改。

蘋果的文檔中,有一篇文檔講述了GCD中使用pthread的禁忌:Compatibility with POSIX Threads。

6OBJECTIVE-C

pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND,0);

SWIFT

pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND, 0)

二、NSThread

對于NSThread,在使用的過程中,我們需要手動完成很多動作才能確保線程的順利運行。但與此同時,它給我們帶來更大的定制化空間。

1.創建NSThread。

對于NSThread的創建,蘋果給出了三種使用方式。

detachNewThreadSelector(_:toTarget:with:)?? detachNewThreadSelector ?會創建一個新的線程,并直接進入線程執行。

initWith(Target:selector:object:)????????? iOS10.0之前的創建方式,需要手動執行。

initWithBlock????????????????????????????????????? iOS10.0之后,可以創建一個執行block的線程。

2.NSThread線程通信。

如果我們想對已經存在的線程進行操作,可以使用

performSelector:onThread:withObject:waitUntilDone:

跳轉到目標線程執行,實現線程間跳轉,達到線程通信的目的。但是需要注意的是,這個方法不適合頻繁的進行通信,尤其是對于一些敏感的操作。

3.NSThread線程的狀態。

在一個線程中,可以通過相關的函數獲取到它的當前狀態。

+ isMainThread:判斷當前線程是不是主線程。

+ mainThread:獲取當前的主線程。

+ isMultiThreaded :判斷當前環境是不是多線程環境

+ threadDictionary :獲取包含項目中的線程的字典

@property(readonly, getter=isExecuting)BOOL executingNS_AVAILABLE(10_5, 2_0);???? 是否處于運行狀態

@property(readonly, getter=isFinished)BOOL finishedNS_AVAILABLE(10_5, 2_0);????????? 是否處于完成狀態

@property(readonly, getter=isCancelled)BOOL cancelledNS_AVAILABLE(10_5, 2_0);????? 是否處于取消狀態

4.NSThread線程的優先級。

可通過給NSThread設置優先級。以便讓開發者更靈活的控制程序的執行。

+ threadPriority? Returns the currentthread’s priority. 返回當前線程的優先級別

threadPriority??????? The receiver’s priority? 消息發送者的優先級,這個發送者是一個NSThread對象

+ setThreadPriority:???? Sets the currentthread’s priority. 設置線程的優先級

5.停止線程/終止線程

+ sleepUntilDate:?? Blocks the currentthreaduntil the time specified.? 直到某時刻執行

+ sleepForTimeInterval:???? Sleeps thethreadfora given time interval.?? 暫停線程

+ exit??? Terminates the currentthread.? 關閉線程,這里調用之前,為了確保程序的安全,我們應在明確線程的狀態是isFinished 和 isCancelled的時候執行。

- cancel??? Changes the cancelled state of the receiver to indicate that it should exit.? 主動進入取消狀態,如果當時線程沒有完成,會繼續執行完成。

6.使用NSThread

- (void)viewDidLoad {

[superviewDidLoad];

_testCount = 100;

_t1 =? [[NSThread alloc] initWithTarget:selfselector:@selector(test) object:nil];

_t1.name = @"線程一";

[_t1 start];

NSInvocationOperation

}

-(void)test{

for(inti = 0; i < 5 ; i++) {

[NSThreadsleepForTimeInterval:0.05];

NSLog(@"%ld,%@",(long)_testCount--,[[NSThreadcurrentThread] name]);

}

}

三、NSOperation & NSOperationQueue

1.NSOperation

NSOperation是對于線程對象的抽象封裝,不會被直接使用,在日常的開發中,會使用它的兩個子類:NSInvocationOperationNSBlockOperation。NSInvocationOperation類是NSOperation的具體子類,用于管理指定為調用的單個封裝任務的執行。 您可以使用此類來啟動包含在指定對象上調用選擇器的操作。 此類實現非并發操作。NSBlockOperation類也是NSOperation的具體子類,用于管理一個或多個block塊的并發執行。 您可以使用此對象一次執行多個block,而無需為每個塊創建單獨的操作對象。 當執行多個程序段時,只有當所有程序段執行完畢時,才會將操作本身完成。

NSInvocationOperation實現非并發操作。

3_invCationOp = [[NSInvocationOperationalloc] initWithTarget:selfselector:@selector(test2:) object:nil];

_invCationOp.name = @"invocation線程";

[_invCationOp start];

打印:

{number = 1, name = main},

{number = 1, name = main}

從打印結果可以看出,NSInvocationOperation實現的是非并法的操作,至于在哪個線程中操作,取決于start的當前調用時的線程。

如果我們需要創建一個并發的Queue,可以使用NSBlockOperation。如果我們像這樣創建:

- (void)blockOperation{

NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

}];

[blockOp addExecutionBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

}];

[blockOp addExecutionBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

}];

[blockOp start];

}

打印:

2017-05-23 15:11:00.289多線程[5377:589808] {number = 1, name = main},{number = 1, name = main}

2017-05-23 15:11:00.289多線程[5377:589808] {number = 1, name = main},{number = 1, name = main}

2017-05-23 15:11:00.289多線程[5377:589808] {number = 1, name = main},{number = 1, name = ?main}

顯然,這里都在主線程中執行,不能證明NSBlockOperation具有并發的能力,這是因為,每個NSBlockOperation對象的會優先在主線程中執行。如果主線程受到阻塞的時候才會開辟另一個線程去執行其他的操作。比如向下面這樣:

//NSBlockOperation

?- (void)blockOperation{

NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

? ? ?}];

? ? ?[blockOp addExecutionBlock:^{

? ? ? ? ?[NSThreadsleepForTimeInterval:2.0];

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

? ? ?}];

? ? ?[blockOp addExecutionBlock:^{

? ? ? ? ?[NSThreadsleepForTimeInterval:2.0];

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

? ? ?}];

? ? ?[blockOp start];

?}

打印:

2017-05-23 15:28:37.780多線程[5645:617976] {number = 1, name = main},{number = 1, name = main}

2017-05-23 15:28:39.848多線程[5645:618027] {number = 3, name = (null)},{number = 1, name = (null)}

2017-05-23 15:28:39.848多線程[5645:618028] {number = 4, name = (null)},{number = 1, name = (null)}=

這里就是異步執行了。NSBlockOperation在使用的過程中,會針對主線程當前的使用情況,選擇性的創建其他的線程。在提升流暢度的同時,還節約了資源。

2.NSOperationQueue

NSOperationQueue:手動管理異步執行。 ?如果我們想使用并發,并且要作到精確掌握并發的線程。可以使用NSOperationQueue。這是一個操作隊列,如果將NSOperation的具體子類對象添加進來的時候,開啟之后,所有的對象沒有先后,會異步執行各自的代碼。

- (void)operationQueue{

NSOperationQueue*queue = [[NSOperationQueuealloc] init];

NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{

NSLog(@"%ld,%@,%@",(long)_testCount--,[NSThreadcurrentThread],[NSThreadmainThread]);

}];

NSInvocationOperation*invCationOp = [[NSInvocationOperationalloc] initWithTarget:selfselector:@selector(test2:) object:nil];

invCationOp.name = @"invocation線程";

[queue addOperation:invCationOp];

[queue addOperation:blockOp];

}

打印:

2017-05-23 15:38:45.110多線程[5772:633972] 100,{number = 3, name = (null)},{number = 1, name = (null)}

2017-05-23 15:38:45.203多線程[5772:633975] 99,{number = 4, name = (null)},{number = 1, name = (null)}

在NSOperationQueue中,正常情況下,所有的operation的執行次序是隨機,如果我們想要某個operation被率先執行,可以將這個operation的優先級調高。對于優先級有以下的選擇:

[invCationOp setQueuePriority:NSOperationQueuePriorityVeryHigh];

對于優先級有以下的選擇:

typedefNS_ENUM(NSInteger,NSOperationQueuePriority) {

NSOperationQueuePriorityVeryLow= -8L,?? 最低

NSOperationQueuePriorityLow= -4L,?????? 次低

NSOperationQueuePriorityNormal= 0,????? 普通? 不做任何操作的operation的優先級是這個

NSOperationQueuePriorityHigh= 4,??????? 次高

NSOperationQueuePriorityVeryHigh= 8???? 最高

};

當然,如果有很多operation,使用的優先級不能滿足的時候,還可以設置 operation的依賴關系。 設置依賴之后,將會先執行依賴對象。

[invCationOp addDependency:blockOp];

值得注意的是:優先級和依賴關系不是沖突的。 優先級的選擇會在依賴關系下發生效果,也就是,在依賴關系成立的情況下,優先級的才會有效。

三、GCD? -? Grand Central Dispatch

Grand Central Dispatch早在Mac OS X 10.6雪豹上就已經存在了。后在iOS4.0的時候被引入。Grand Central Dispatch是OS X中的一個低級框架,用于管理整個操作系統中的并發和異步執行任務。本質上,隨著處理器核心的可用性,任務排隊等待執行。通過允許系統控制對任務的線程分配,GCD更有效地使用資源,這有助于系統和應用程序運行更快,高效和響應。

GCD的一個重要的對象是隊列:Dispatch Queue。跟Operationqueue類似,通過將Operation加入到隊列中,執行相應的單元。在GCD中,大量采用了block的形式創建類似的operation。

1.?Dispatch Queue? 創建

Dispatch Queue 分為兩類,主要是根據并行和串行來區分:

a. Serial Dispatch Queue: 線性執行的線程隊列,遵循FIFO(First In First Out)原則;?又叫private dispatch queues,同時只執行一個任務。Serial queue常用于同步訪問特定的資源或數據。當你創建多個Serial queue時,雖然各自是同步,但serial queue之間是并發執行。 main dipatch屬于這個類別。

b. Concurrent Dispatch Queue: 并發執行的線程隊列,并發執行的處理數取決于當前狀態。又叫global dispatch queue,可以并發的執行多個任務,但執行完成順序是隨機的。系統提供四個全局并發隊列,這四個隊列有這對應的優先級,用戶是不能夠創建全局隊列的,只能獲取。

我們可以自定義隊列,默認創建的隊列是串行的,但是也可以指定創建一個并行的隊列:

//串行隊列dispatch_queue_create("com.deafultqueue", 0)

//串行隊列dispatch_queue_create("com.serialqueue", DISPATCH_QUEUE_SERIAL)//并行隊列dispatch_queue_create("com.concurrentqueue", DISPATCH_QUEUE_CONCURRENT)

除了自定義隊列,系統其實也為有一些已經公開的隊列。這些隊列不需要我們顯示的創建,只能通過獲取的方式得到:

dispatch_get_main_queue() ? 獲取當前的APP主隊列,這個隊列在主線程中,通常我們調用它進行界面的刷新。

dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>) ? 獲取全局的Concurrent隊列,這里蘋果提供了四種不同的優先級,

#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

也即時有四個不同的并行隊列。

2.?Dispatch Queue ?執行

GCD的隊列有串行和并行兩種隊列,同時我們可以同步和異步兩種方式執行隊列,所以最多有四種不同的場景。

(1)串行同步。 凡涉及到同步的的都會阻塞線程。 UI線程—也即是我們的所說的主線程默認情況下其實就是執行同步的。這個時候如果有一些耗時間的操作,則會出現卡頓的現象。這種方式大部分情況用于能快速響應和后臺線程的耗時場景中。

//串行同步

dispatch_queue_t? serialQ = dispatch_queue_create("串行", DISPATCH_QUEUE_SERIAL);//創建一個串行隊列

NSLog(@"%@",[NSThreadcurrentThread].description);

dispatch_sync(serialQ, ^{

[NSThreadsleepForTimeInterval:3];

NSLog(@"%@ --? %@隊列",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_sync(serialQ, ^{

NSLog(@"%@ --? %@隊列",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

(2)串行異步。 這種情況下,GCD會開辟另一個新的線程,讓隊列中的內容在新的線程中按順序執行。

//串行異步

dispatch_async(serialQ, ^{

NSLog(@"%@ 1-- 隊列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_async(serialQ, ^{

NSLog(@"%@ 2-- 隊列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_async(serialQ, ^{

NSLog(@"%@ 3-- 隊列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_async(serialQ, ^{

NSLog(@"%@ 4-- 隊列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

(3)并行同步。 因為是同步執行,所以實際上這里的并行是沒有意義的。 依然在當前的線程中按順序執行,并阻塞。

dispatch_queue_t? conCurrentQ = dispatch_queue_create("并行", DISPATCH_QUEUE_CONCURRENT);//創建一個并行行隊列

//并行同步

dispatch_sync(conCurrentQ, ^{

[NSThreadsleepForTimeInterval:0.2];

NSLog(@"%@ 1-- 隊列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_sync(conCurrentQ, ^{

[NSThreadsleepForTimeInterval:0.2];

NSLog(@"%@ 2-- 隊列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_sync(conCurrentQ, ^{

[NSThreadsleepForTimeInterval:0.2];

NSLog(@"%@ 3-- 隊列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

(4)并行異步。 并行異步將極大的利用資源。首先會開辟新的線程,并且,當所有線程備占用的情況下,會繼續開辟(如果沒有限制的話)。所以這里還涉及線程的最大值的問題。

//并行異步

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 1-- 隊列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 2-- 隊列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 3-- 隊列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 4-- 隊列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

3.?Dispatch Queue ?暫停和繼續

我們可以使用dispatch_suspend函數暫停一個queue以阻止它執行block對象;使用dispatch_resume函數繼續dispatch queue。調用dispatch_suspend會增加queue的引用計數,調用dispatch_resume則減少queue的引用計數。當引用計數大于0時,queue就保持掛起狀態。因此你必須對應地調用suspend和resume函數。掛起和繼續是異步的,而且只在執行block之間(比如在執行一個新的block之前或之后)生效。掛起一個queue不會導致正在執行的block停止。

4.?Dispatch Queue ?的銷毀

每個隊列在執行完添加到其中的所有的block事件的時候,在ARC模式下,會被自動銷毀。 但是在手動管理內存的時候,我們需要調用

dispatch_release(queue);

來釋放。

5.隊列組 ?Dispatch Group (這些內容來自http://blog.csdn.net/q199109106q/article/details/8566300

多數情況下,我們可能會遇到這種問題: 對一個頁面中的多張圖片,每張圖片要單獨的進行網絡請求,我們沒有辦法保證每次的請求時間是一樣的,但是項目經理說必須要在獲取所有的圖片的情況下,才可以進行對頁面的刷新。這里有個很好例子可以解決這個問題。

// 根據url獲取UIImage

- (UIImage *)imageWithURLString:(NSString*)urlString {

NSURL*url = [NSURLURLWithString:urlString];

NSData*data = [NSDatadataWithContentsOfURL:url];

return[UIImage imageWithData:data];

}

- (void)downloadImages {

// 異步下載圖片

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

// 下載第一張圖片

NSString*url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";

UIImage *image1 = [selfimageWithURLString:url1];

// 下載第二張圖片

NSString*url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";

UIImage *image2 = [selfimageWithURLString:url2];

// 回到主線程顯示圖片

dispatch_async(dispatch_get_main_queue(), ^{

self.imageView1.image = image1;

self.imageView2.image = image2;

});

});

}

但是我們發現,事實上,圖片一和 二兩者在請求的過程中是完全獨立的, 但是這里明顯的,圖片一的下載將阻塞,直到下載完才會開始圖片二的下載。這種方式畢竟還是有瑕疵的啊哈。

Dispatch Group可以幫助解決這個問題。 它是Dispatch Queue的組合,被加入到group的queue會在組內其他的queue也執行完操作的時候,有group統一調用預設好的一個block。最重要的是,在group中的內容是可以異步執行的。也即是多個隊列在不同的線程執行。 如果圖片大小差不多的話,這種方式將節省我們不一半的時間。 ?我們來看看這個模型。

//dispaach group

dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 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, ^{

[NSThreadsleepForTimeInterval:5.0];

NSLog(@"第一個項目執行完成。");

});

dispatch_group_async(group, queue2, ^{

[NSThreadsleepForTimeInterval:10.0];

NSLog(@"第二個項目執行完成。");

});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{

NSLog(@"集體回調");

});

6.GCD的其他的用法

(1)控制一段代碼只執行一次。用在創建單例的時候再好不過了。

//控制代碼只執行一次數

for(inti = 1 ;i <= 10 ;i++){

staticdispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

NSLog(@"被執行 %d次",i);

});

}

打印結果:2017-05-24 18:15:17.704多線程[10542:898532]被執行1

(2) 只能控制執行一次是不是有點不夠完美 。dispatch_apply 可以讓你控制一段代碼執行任意多次。這里的執行是異步執行的,如果為了確保順序執行,應該對執行的內容進行加鎖。

//控制執行任意多次

dispatch_queue_t queueX = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

__blockintcount = 0;

NSLock*lock = [[NSLockalloc]init];

dispatch_apply(5, queueX, ^(size_t index) {

[lock lock];

NSLog(@"%d,%zu",count++,index);

[lock unlock];

});

(3)做一個block式的延時。 除了使用performSelector之外,我們還以使用dispatch_after進行延時,并且是以block的形式進行。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 *NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

NSLog(@"五秒鐘之后執行的代碼。");

});

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容