iOS爲開發者提供了豐富的多線程編程機制,從最直接最簡單的調用NSObject
的performSelector
系列API,到NSThread
、Operation Queues
以及Dispatch Queues
,當然iOS也支持更原始的pthread
。本文主要討論使用這些多線程編程機制時候的一些注意事項,具體如何使用這些機制,可以移步到Raywenderlich的教程以及官方文檔:
- Grand Central Dispatch In-Depth: Part 1/2
- Grand Central Dispatch In-Depth: Part 2/2
- How To Use NSOperations and NSOperationQueues
- Concurrency Programming Guide
前兩篇教程通過逐步改善一個圖片集加載的例子,來說明GCD(Grant Central Dispatch)在數據讀寫同步、數據加載以及線程同步上的應用;類似的,第三篇教程通過一個加載table內容的例子,說明了NSOperation以及NSOperationQueue的應用。
NSOperation和NSOperationQueue
相比其他iOS多線程機制,NSOperation
給開發者提供了更多獨特的特性,這些特性包括:
NSOperation
將任務封裝在對象中,完全支持OOP開發方式,有利於保持代碼結構的一致性。NSOperation
對象特性使得可以給任務線程設置相應的屬性,比如NSOperation
爲開發者定義了三個屬性isExecuting
,NSOperation
,isCanceled
,當然開發者在子類化NSOperation
時,可以根據需求定義額外的屬性;之後開發者可以對這些屬性進行KVO。調用
NSOperation
的cancel
方法可以重置任務線程的isCanceled
屬性,任務根據這個狀態作出相應的操作,比如退出或者暫停等;調用NSOperationQueue
的cancelAllOperations
方法可以重置該隊列上所有任務的isCanceled
屬性;任務隊列支持暫停操作,調用
NSOperationQueue
的setSuspended:(BOOL)
可以暫停或者重啓隊列任務之間支持互相依賴,通過調用
NSOperation
的addDependency:(NSOperation *)
方法爲自身任務添加依賴任務線程,調用removeDependency:(NSOperation *)
可以將依賴隊列移除可以給任務隊列中的任務設置運行優先級,從而控制任務隊列中任務執行順序
可以給任務設置一個
completionBlock
,該block在任務運行結束後執行。
下面總結幾點在使用隊列和任務時的幾個要點:
1)NSOperation
是一個抽象類,開發者需要繼承該類並至少要重載main
方法,
@interface CustomizedOperation : NSOperation
@property(strong) id myData;
-(id)initWithData:(id)data;
@end
@implementation CustomizedOperation
- (id)initWithData:(id)data {
if (self = [super init]){
_myData = data;
}
return self;
}
- (void)main {
@autoreleasepool {
while (true) {
// TODOs:
}
}
}
@end
2) 前面提到,可以調用cancel
或者cancelAllOperations
方法,但是這兩個方法除了重置任務的isCanceled
屬性之外,不會做其他操作,要想達到真正取消任務的目的,需要開發者自己不斷檢查給屬性的狀態,並作出相應操作,如:
@interface CustomOperation: NSOperation
@end
@implementation CustomOperation
- (void)main {
// 耗時操作
@autoreleasepool {
for (int i = 0 ; i < 1000000 ; i++) {
// 檢查任務狀態
if (self.isCancelled)
break;
// step 1
[self doSomething];
// 檢查任務狀態
if (self.isCancelled)
break;
// step 2
[self doSomethingelse];
}
}
}
@end
3) 任務默認都是同步執行的,如果想要任務異步執行,開發者需要自己維護任務的狀態,需要重寫start
, isExecuting
, isFinished
, isConcurrent
方法。其中重載start
方法時不需要調用父類方法,isConcurrent
方法只需要直接返回YES
即可。
@interface MyOperation : NSOperation {
BOOL _executing;
BOOL _finished;
}
- (void)completeOperation;
@end
@implementation MyOperation
- (id)init {
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
}
return self;
}
- (void)start {
// 啓動任務前檢查任務是否被取消
if ([self isCancelled])
{
// 如果任務被取消,則重置其他屬性狀態.
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
_executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main {
// 這裏執行主要任務
// 檢查任務狀態
if (self.isCancelled)
break;
// step 1
[self doSomething];
// 檢查任務狀態
if (self.isCancelled)
break;
// step 2
[self doSomethingelse];
[self completeOperation];
}
- (void)completeOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return _executing;
}
- (BOOL)isFinished {
return _finished;
}
GCD(Grant Central Dispatch)
1)dispatch queue的創建時候需要傳遞一個串行或者並行標誌,GCD庫提供了這兩個標誌,但是很多開發者卻不使用,下面是正確的姿勢:
dispatch_queue_t serialQueue = dispatch_queue_create("com.johnkui.serial", **DISPATCH_QUEUE_SERIAL**);
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.johnkui.concurrent", **DISPATCH_QUEUE_CONCURRENT**);
2)我們知道使用dispatch_group_async
或者dispatch_group_enter/dispatch_group_leave
配合dispatch_group_notify
或者dispatch_group_wait
可以實現同步操作,但是如果使用這些API不當的話,結果卻會出人意料。下面是使用dispatch_group_async
的正確姿勢:
dispatch_queue_t queueA = dispatch_queue_create("com.johnkui.concurrentA", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queueB = dispatch_queue_create("com.johnkui.concurrentB", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
// 給任務組添加任務
dispatch_group_async(group, queueA, ^{
// Some asynchronous work
});
// 給任務組添加其他任務
dispatch_group_async(group, queueB, ^{
// Some other asynchronous work
});
// wait on the group to block the current thread.
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_group_async
配合dispatch_group_wait
使用時,要保證dispatch_group_async
調用的block必須運行相應的隊列中,在上例中分爲爲queueA和queueB中,如果block裏的任務實際運行在其他queue中,則dispatch_group_wait
將立即返回而監測不到兩個任務的完成狀態,因爲group只監控和該group關聯的queue上的任務的運行狀態。如果block裏面的任務確實想運行在其他queue上,則可以使用dispatch_group_enter/dispatch_group_leave
,下面是正確的姿勢:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queueA = dispatch_queue_create("com.johnkui.concurrentA", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queueB = dispatch_queue_create("com.johnkui.concurrentB", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_enter(group);
dispatch_async(queueA, ^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 執行耗時任務
dispatch_group_leave(group);
});
});
dispatch_group_enter(group);
dispatch_async(queueB, ^{
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Hello"
message:@"任務完成了!"
delegate:nil
cancelButtonTitle:@"Ok"
otherButtonTitles: nil];
[alertView show];
dispatch_group_leave(group);
});
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"group is finished...");
});
當block裏的任務運行完需要更新UI時,一般都會這麼操作:
dispatch_async(dispatch_get_main_queue(), ^{
//更新UI
});
這個時候一定不要使用同步的dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
,否則整個主線程將出現死鎖,而是使用異步的dispatch_group_notify
;