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是對于線程對象的抽象封裝,不會被直接使用,在日常的開發中,會使用它的兩個子類:NSInvocationOperation和NSBlockOperation。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(@"五秒鐘之后執行的代碼。");
});