iOS中常見的實現多線程并發的有三種方式,NSThread,NSOperation和GCD。Operation Queues實現并發的主要方式是通過NSOperation&NSOperationQueue實現,主要分以下三步,本文的主要結構也大致是如下結構。
- 實例化NSOperation子類,綁定執行操作
- 創建NSOperationQueue,將NSOperation實例添加進來
- 系統自動將NSOperationQueue隊列中檢測取出和執行NSOperation操作
NSOperation
NSOperation實例我們稱之為操作對象,操作對象可以將需要執行的代碼和相關數據集成并封裝。操作對象通常不直接執行,而是它加入到操作隊列中按順序調用。同時操作對象也可以直接調用start方法來執行任務,但為了能并行執行任務,標準做法是在start內創建線程。
NSOperation本身是抽象基類,因此必須使用它的子類,使用NSOperation子類的方式有2種:
- 使用系統提供的兩個具體子類: NSInvocationOperation和NSBlockOperation
- 自定義子類繼承NSOperation,實現內部相應的方法
NSInvocationOperation
通過object & selector 非常方便地創建一個NSInvocationOperation,我們已經有了一個現成的方法,而方法
NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(run) object:nil];
NSBlockOperation
使用子類NSBlockOperation,通過使用NSBlockOperation來執行一個或多個block,只有當一個NSBlockOperation所關聯的所有block都執行完畢時候,這個NSBlockOperation才算完成,有點類似于dispatch_group概念。從上面打印結果看到在多個線程執行任務。addExecutionBlock:可以為NSBlockOperation添加額外的操作。如果當前NSOperation的任務只有一個的話,那肯定不會開辟一個新的線程,只能同步執行。只有NSOperation的任務數>1的時候,這些額外的操作才有可能在其他線程并發執行。
- (void)runInvocationOp{
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
for (int i = 0; i < 5; i++) {
[op addExecutionBlock:^{
NSLog(@"%d------%@", i,[NSThread currentThread]);
}];
}
[op start];
}
執行結果如下
2017-05-10 18:40:04.642 JQMultiThread[19102:1441939] ------<NSThread: 0x60800006d740>{number = 1, name = main}
2017-05-10 18:40:04.643 JQMultiThread[19102:1441939] 1------<NSThread: 0x60800006d740>{number = 1, name = main}
2017-05-10 18:40:04.643 JQMultiThread[19102:1441939] 2------<NSThread: 0x60800006d740>{number = 1, name = main}
2017-05-10 18:40:04.644 JQMultiThread[19102:1441939] 4------<NSThread: 0x60800006d740>{number = 1, name = main}
2017-05-10 18:40:04.644 JQMultiThread[19102:1442083] 3------<NSThread: 0x60800007b6c0>{number = 3, name = (null)}
2017-05-10 18:40:04.644 JQMultiThread[19102:1442071] 0------<NSThread: 0x60800007b700>{number = 4, name = (null)}
自定義NSOperation
通常來說,我們都是通過將operation添加到operation queue來執行operation的,但這并不是必須的,可以通過start
方法執行一個operation,但是這種方式是無法保證異步執行的。當系統定義的兩個子類NSInvocationOperation和NSBlockOperation不能很好的滿足我們的需求時,我們可以自動移自己的NSOperation類,我們可以定義非并發和并發兩種不同類型的NSOperation子類,定義非并發要比并發簡單得多。
系統預定義的兩個子類 NSInvocationOperation 和 NSBlockOperation 不能很好的滿足我們的需求時,我們可以自定義自己的 NSOperation 子類,添加我們想要的功能。我們可以自定義非并發和并發兩種不同類型的 NSOperation 子類,而自定義一個前者要比后者簡單得多。
- 定義繼承自NSOperation的子類,通過實現內部相應的方法來創建任務。
非并發的NSOperation子類
最低限度來說,實現非并發NSOperation子類需要實現兩個初始化方法
- 自定義初始化方法
- main方法
引用官方文檔的例子如下
- (id)initWithURL:(NSURL *)url scanCount:(NSInteger)scanCount
{
self = [super init];
if (self)
{
self.loadURL = url;
ourScanCount = scanCount;
}
return self;
}
// -------------------------------------------------------------------------------
// main:
//
// Examine the given file (from the NSURL "loadURL") to see it its an image file.
// If an image file examine further and report its file attributes.
//
// We could use NSFileManager, but to be on the safe side we will use the
// File Manager APIs to get the file attributes.
// -------------------------------------------------------------------------------
-(void)main {
if (![self isCancelled])
{
// test to see if it's an image file
if ([self isImageFile:loadURL])
{
// in this example, we just get the file's info (mod date, file size) and report it to the table view
//
NSNumber *fileSize;
[self.loadURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:nil];
NSDate *fileCreationDate;
[self.loadURL getResourceValue:&fileCreationDate forKey:NSURLCreationDateKey error:nil];
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
[formatter setTimeStyle:NSDateFormatterNoStyle];
[formatter setDateStyle:NSDateFormatterShortStyle];
NSString *modDateStr = [formatter stringFromDate:fileCreationDate];
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:
[self.loadURL lastPathComponent], kNameKey,
[self.loadURL absoluteString], kPathKey,
modDateStr, kModifiedKey,
[NSString stringWithFormat:@"%ld", [fileSize integerValue]], kSizeKey,
[NSNumber numberWithInteger:ourScanCount], kScanCountKey, // pass back to check if user cancelled/started a new scan
nil];
if (![self isCancelled])
{
// for the purposes of this sample, we're just going to post the information
// out there and let whoever might be interested receive it (in our case its MyWindowController).
//
[[NSNotificationCenter defaultCenter] postNotificationName:kLoadImageDidFinish object:nil userInfo:info];
}
}
}
}
并發的NSOperation子類
在默認情況下,operation 是同步執行的,也就是說在調用它的 start 方法的線程中執行它們的任務。而在 operation 和 operation queue 結合使用時,operation queue 可以為非并發的 operation 提供線程,因此,大部分的 operation 仍然可以異步執行。但是,如果你想要手動地執行一個 operation ,又想這個 operation 能夠異步執行的話,你需要做一些額外的配置來讓你的 operation 支持并發執行。下面列舉了一些你可能需要重寫的方法:
start
:(Required) ,所有并發執行的 operation 都必須要重寫這個方法,替換掉 NSOperation 類中的默認實現。start 方法是一個 operation 的起點,我們可以在這里配置任務執行的線程或者一些其它的執行環境。另外,需要特別注意的是,在我們重寫的 start 方法中一定不要調用父類的實現;
main
:(Optional) ,通常這個方法就是專門用來實現與該 operation 相關聯的任務的。盡管我們可以直接在 start 方法中執行我們的任務,但是用 main 方法來實現我們的任務可以使設置代碼和任務代碼得到分離,從而使 operation 的結構更清晰;
isExecuting
和 isFinished
:(Required) ,并發執行的 operation 需要負責配置它們的執行環境,并且對外報告執行環境的狀態。因此,一個并發執行的 operation 必須要維護一些狀態信息,用來記錄它的任務是否正在執行,是否已經完成執行等。此外,當這兩個方法所代表的值發生變化時,我們需要生成相應的 KVO 通知,以便外界能夠觀察到這些狀態的變化;
isConcurrent
:(Required) ,這個方法用來標識一個 operation 是否是并發的 operation ,需要重寫這個方法并返回 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;
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
@end
- (void)start {
// Always check for cancellation before launching the task.
if ([self isCancelled])
{
// Must move the operation to the finished state if it is canceled.
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:@"isExecuting"];
//start方法重寫之后,蘋果不會再主動執行main方法
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main {
@try {
// Do the main work of the operation here.
[self completeOperation];
}
@catch(...) {
// Do not rethrow exceptions.
}
}
- (void)completeOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
NSOperation生命周期與狀態
NSOperation被創建后有幾個生命周期,Pending,Ready,Executing,Finished,Cancel狀態,前三個狀態都可以直接執行cancel的操作。
operation開始執行之后,會一直執行任務直到完成,或者顯式地取消操作。取消可能發生在任何時候,甚至在operation執行之前。盡管NSOperation提供了一個方法,讓應用取消一個操作,但是識別出取消事件則是我們自己的事情。若operation直接終止, 可能無法回收所有已分配的內存或資源。因此operation對象需要檢測取消事件,并優雅地退出執行。NSOperation對象需要定期地調用isCancelled方法檢測操作是否已經被取消,如果返回YES(表示已取消),則立即退出執行。不管是自定義NSOperation子類,還是使用系統提供的兩個具體子類,都需要支持取消。isCancelled方法本身非常輕量,可以頻繁地調用而不產生大的性能損失
以下地方可能需要調用isCancelled:
- 在執行任何實際的工作之前
- 在循環的每次迭代過程中,如果每個迭代相對較長可能需要調用多次
- 代碼中相對比較容易中止操作的任何地方
需要注意的是,為了讓我們自定義的operation能夠取消操作,我們需要在代碼中定期檢查isCancelled方法的返回值,一旦檢查到這個方法返回YES,就需要立刻停止執行接下來任務。
如果是進行特定任務比如數據請求或者數據下載,我們可以采用系統重寫cancel的方法取消操作,但可能會出現一種情況,就是在檢查的過程中,這個操作完成了,一旦進入finished狀態后,就cancel不掉了,因為沒有這個路徑,直接從finished到cancel。AFNetworking和SDWebImage的cancel操作都是直接重寫,然后去取消下載或者請求的操作。
AFNetworking 中AFURLConnectionOperation的cancel實現
- (void)cancel {
[self.lock lock];
if (![self isFinished] && ![self isCancelled]) {
[super cancel];
if ([self isExecuting]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
}
[self.lock unlock];
}
SDWebImage中SDWebImageDownloaderOperation的cancel實現
- (void)cancel {
@synchronized (self) {
if (self.thread) {
[self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
}
else {
[self cancelInternal];
}
}
}
- (void)cancelInternalAndStop {
if (self.isFinished) return;
[self cancelInternal];
CFRunLoopStop(CFRunLoopGetCurrent());
}
- (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
if (self.cancelBlock) self.cancelBlock();
if (self.connection) {
[self.connection cancel];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
// As we cancelled the connection, its callback won't be called and thus won't
// maintain the isFinished and isExecuting flags.
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
[self reset];
}
NSOperation 屬性的KVO維護
通過觀察以下Key paths
,可以非常容易地被觀察到NSOperation的狀態:
- isCancelled
- isConcurrent
- isExecuting
- isFinished
- isReady
- dependencies
- queuePriority
- completionBlock
如果你重寫了start方法,或者對除了main函數外的NSOperation進行了重大定制,你必須確保自定義對象保持符合這些關鍵路徑的KVO,當覆蓋start
方法的時候,最應該關注的key patshs變化是isExecuting和isFinished,因為這兩個key paths
最容易受到重寫start方法的影響
NSOperationQueue
一個NSOperation對象可以通過調用start方法來執行任務,默認是同步執行的。也可以將NSOperation添加到一個NSOperationQueue(操作隊列)中去執行,而且是異步執行的。具體的執行不用我們自己去管理,都由操作系統去處理。NSOperationQueue和GCD中的并發隊列、串行隊列略有不同的是:NSOperationQueue一共有兩種隊列:主隊列(maxConcurrentOperationCount為1)、其他隊列。其中其他隊列同時包含了串行、并發功能。
主隊列:凡是加到主隊列中的operation,都會放到主線程執行。
NSOperationQueue *queue = [NSOperationQueue mainQueue];
其他隊列:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
將任務加到隊列中去
把任務加入到隊列中主要是以下方法
- (void)addOperation:
,添加一個 operation 到 operation queue 中。
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:
,添加一組 operation 到 operation queue 中。
- (void)addOperationWithBlock:
,直接添加一個 block 到 operation queue 中,而不用創建一個 NSBlockOperation 對象。
- (void)runAddOperation
{
NSOperationQueue *_queue = [[NSOperationQueue alloc] init];
NSBlockOperation *_blockOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block operation");
}];
NSBlockOperation *_copyOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"Copy Operation");
}];
NSInvocationOperation *_invocationOp =[[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(runInvocationOp)
object:nil];
[_queue addOperationWithBlock:^{
NSLog(@"add block");
}];
[_queue addOperation:_invocationOp];
[_queue addOperations:@[_blockOp, _copyOp] waitUntilFinished:NO];
}
- (void)runInvocationOp
{
NSLog(@"send Log");
}
設置最大并發數(maxConcurrentOperationCount)
NSOperationQueue類是設計用來執行操作的并行執行,但你可以強行把maxConcurrentOperationCount設置成1用來執行串行操作。但這個隊列的執行順序并不跟GCD中的串行隊列那樣完全遵循FIFO,而是由好幾個因素決定,比如operation 的 isReady 狀態,優先級等。如果不設置maxConcurrentOperationCount的屬性,那么它的默認值就是
NSOperationQueueDefaultMaxConcurrentOperationCount,就是系統可設置最大操作并行數。
設置操作的任務依賴
通過配置依賴關系,我們可以讓不同的operation串行之行,一個operation只有在它依賴的所有operation都執行完成之后才能開始執行。配置operation的依賴關系要涉及到NSOperation類中的addDependency方法。注意三點
* 依賴是單向的操作,[A addDependency:B],表示A依賴B,但B并不會依賴A,不然會形成循環依賴
* 依賴也并不局限在同一個queue中
* 一定不能形成循環依賴,否則會形成死鎖
Operation的優先級
當加入到一個隊列中,執行順序取決于operation的isReady狀態以及它們對應的優先級。isReady狀態取決于操作之間的相互依賴。但是操作優先級水平則是完全由operation的優先級屬性決定。所有新建的操作都有一個默認的normal優先級,但是你可以通過setQueuePriority方法來人為增加或者降低操作優先級。
Operation Queue vs Grand Central Dispatch(GCD)
蘋果的文檔中已經對兩者的差別和使用場景給出了官方的解釋
GCD is a low-level API that gives you the flexibility to structure your code in a variety of different ways. In contrast, NSOperation provides you with a default structure that you can use for your asynchronous code. If you're looking for an existing, well-defined structure that's perfectly tailored for Cocoa applications, use NSOperation. If you're looking to create your own structure that exactly matches your problem space, use GCD.
GCD
是基于C語言開發的底層API,可以靈活地以各種不同的方式構建代碼。相比之下NSOperation給你提供了可異步處理代碼的默認結構。如果你正在尋找一種完美適用于Cocoa應用程序的結構請使用NSOperation。如果你希望創建與您的問題完全匹配的自己的結構,可以使用GCD。即可以歸結為兩點,GCD更底層,而NSOperation則為我們帶來了一些面向對象的封裝,也帶來了方便的靈活性。我自己對兩者的歸結點如下:
-
NSOperation
: 建立在 GCD 的基礎之上的,面向對象的解決方案,當operation需要添加相互依賴,或者取消一個正在執行的operation,暫停或者恢復operation queue時候,NSOperation無疑更加靈活。 -
GCD
:基于C層級的API,輕量級,FIFO執行并發的方式,使用GCD時候我們并不關心任務的調度情況,而讓系統幫我們自動處理。
Simple and Reliable Threading with NSOperation
Concurrency Programming Guide
iOS 并發編程之 Operation Queues