1. 程式人生 > >iOS多執行緒——你要知道的NSOperation都在這裡

iOS多執行緒——你要知道的NSOperation都在這裡

你要知道的iOS多執行緒NSThread、GCD、NSOperation、RunLoop都在這裡

本系列文章主要講解iOS中多執行緒的使用,包括:NSThread、GCD、NSOperation以及RunLoop的使用方法詳解,本系列文章不涉及基礎的執行緒/程序、同步/非同步、阻塞/非阻塞、序列/並行,這些基礎概念,有不明白的讀者還請自行查閱。本系列文章將分以下幾篇文章進行講解,讀者可按需查閱。

NSOperation&&NSOperationQueue的使用姿勢全解

經過前面的學習,講解了最基礎的NSThread使用方法,封裝更完善的GCDGCD提供了極其便捷的方法來編寫多執行緒程式,可以自動實現多核的真正平行計算,自動管理執行緒的生命週期,好處不言而喻,但可定製性就有點不足了,Foundation

框架提供了NSOperationNSOperationQueue這一面向物件的多執行緒類,這兩個類與GCD提供的功能類似,NSOperation提供任務的封裝,NSOperationQueue顧名思義,提供執行佇列,可以自動實現多核平行計算,自動管理執行緒的生命週期,如果是併發的情況,其底層也使用執行緒池模型來管理,基本上可以說這兩個類提供的功能覆蓋了GCD,並且提供了更多可定製的開發方式,開發者可以按需選擇。

使用NSOperationNSOperationQueue來編寫多執行緒程式非常簡單,只需要建立一個任務物件,建立一個執行佇列或者和獲取主執行緒一樣獲取一個主任務佇列,然後將任務提交給佇列即可實現併發,如過你想要序列只需要將佇列的併發數設定為一即可。接下來將分別介紹兩個類的使用。

NSOperation “任務的封裝”

GCD類似,GCD向佇列提交任務,NSOperation就是對任務進行的封裝,封裝好的任務交給不同的NSOperationQueue即可進行序列佇列的執行或併發佇列的執行。這裡的任務就是NSOperation類的一個方法,main方法或start方法(兩個方法有區別,後文會講),但NSOperation類的這兩個方法是空方法,沒有幹任何事情,因此,我們需要自定義繼承NSOperation類並重寫相關方法,OC也提供了兩個子類供我們使用NSBlockOperationNSInvocationOperation

接下來看一下NSOperation

類中比較重要的屬性和方法:

/*
對於併發Operation需要重寫該方法
也可以不把operation加入到佇列中,手動觸發執行,與呼叫普通方法一樣
*/
- (void)start;

/*
非併發Operation需要重寫該方法
*/
- (void)main;

//只讀屬性任務是否取消,如果自定義子類,需要重寫該屬性
@property (readonly, getter=isCancelled) BOOL cancelled;

/*
設定cancelled屬性為YES
僅僅標記cancelled屬性,不退出任務,和NSThread的cancel一個機制
自定義子類時需要使用該屬性判斷是否在外部觸發了取消任務的操作,手動退出任務
*/
- (void)cancel;

//只讀屬性,任務是否正在執行,如果自定義子類,需要重寫該屬性
@property (readonly, getter=isExecuting) BOOL executing;

/*
只讀屬性,任務是否結束,如果自定義子類,需要重寫該方法
對於加入到佇列的任務來說,當finished設定為YES後,佇列會將任務移除出佇列
*/
@property (readonly, getter=isFinished) BOOL finished;

//是否為併發任務,該屬性已經被標識即將棄用,應該使用下面的asynchronous屬性
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below

/*
只讀屬性,判斷任務是否為併發任務,預設返回NO
如果需要自定義併發任務子類,需要重寫getter方法返回YES
*/
@property (readonly, getter=isAsynchronous) BOOL asynchronous;

/*
只讀屬性,任務是否準備就緒
對於加入佇列的任務,當ready為YES,標識該任務即將開始執行
如果任務有依賴的任務沒有執行完成ready為NO
*/
@property (readonly, getter=isReady) BOOL ready;

/*
新增一個NSOperation為當前任務的依賴
如果一個任務有依賴,需要等待依賴的任務執行完成才能開始執行
*/
- (void)addDependency:(NSOperation *)op;

//刪除一個依賴
- (void)removeDependency:(NSOperation *)op;

//任務在佇列裡的優先順序
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

//任務在佇列裡的優先順序
@property NSOperationQueuePriority queuePriority;

/*
任務完成後的回撥方法
當finished屬性設定為YES時才會執行該回調
*/
@property (nullable, copy) void (^completionBlock)(void);

上述內容中有一些屬性和方法是在自定義NSOperation子類中必須要重寫的,自定義子類能夠提供更高的可定製性,因此,編寫自定義子類更復雜,自定義子類在後面會講,如果我們只需要實現GCD那樣的功能,提交一個併發的任務,OC為我們提供了兩個子類NSBlockOperationNSInvocationOperation,這兩個子類已經幫我們完成了各種屬性的設定操作,我們只需要編寫一個任務的block或者一個方法即可像使用GCD一樣方便的編寫多執行緒程式。

接下來舉兩個建立任務的栗子:

//建立一個NSBlockOperation物件,傳入一個block
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 100; i++)
    {
        NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
    }
}];

/*
建立一個NSInvocationOperation物件,指定執行的物件和方法
該方法可以接收一個引數即object
*/
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task:) object:@"Hello, World!"];

可以發現,建立任務真的很簡單,就像GCD中建立任務一樣簡潔,任務建立完成就可以建立隊列了。

NSOperationQueue

NSOperationQueue就是任務的執行佇列,看一下該類中有哪些比較重要的屬性和方法:

//向佇列中新增一個任務
- (void)addOperation:(NSOperation *)op;

/*
向佇列中新增一組任務
是否等待任務完成,如果YES,則阻塞當前執行緒直到所有任務完成
如果為False,不阻塞當前執行緒
*/
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;

//向佇列中新增一個任務,任務以block的形式傳入,使用更方便
- (void)addOperationWithBlock:(void (^)(void))block;

//獲取佇列中的所有任務
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;

//獲取佇列中的任務數量
@property (readonly) NSUInteger operationCount;

/*
佇列支援的最大任務併發數
如果為1,則只支援同時處理一個任務,即序列佇列,主佇列就是序列佇列使用主執行緒執行任務
如果為大於1的數,則支援同時處理多個任務,即併發佇列,底層使用執行緒池管理多個執行緒來執行任務
*/
@property NSInteger maxConcurrentOperationCount;

//佇列是否掛起
@property (getter=isSuspended) BOOL suspended;

//佇列的名稱
@property (nullable, copy) NSString *name;

/*
取消佇列中的所有任務
即所有任務都執行cancel方法,所有任務的cancelled屬性都置為YES
*/
- (void)cancelAllOperations;

//阻塞當前執行緒直到所有任務完成
- (void)waitUntilAllOperationsAreFinished;

//類屬性,獲取當前佇列
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue;

//類屬性,獲取主佇列,任務新增到主佇列就會使用主執行緒執行,主佇列的任務併發數為1,即序列佇列
@property (class, readonly, strong) NSOperationQueue *mainQueue;

上述屬性中比較重要的就是maxConcurrentOperationCount,該屬性直接決定了佇列是序列的還是併發的,接下來看一個栗子:

- (void)task:(id)obj
{
    for (int i = 0; i < 100; i++)
    {
        NSLog(@"Task2 %@ %d %@", [NSThread currentThread], i, obj);
    }
}

- (void)viewWillAppear:(BOOL)animated
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:2];

    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    }];

    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task:) object:@"Hello, World!"];

    [queue addOperation:operation];
    [queue addOperation:invocationOperation];
}

上面這個栗子就很簡單了,首先建立了一個佇列,最大任務併發數設定為2,接下來建立了兩個任務並新增進了佇列,摘取幾個輸出如下:

Task2 <NSThread: 0x1c427e440>{number = 3, name = (null)} 0 Hello, World!
Task1 <NSThread: 0x1c0462fc0>{number = 4, name = (null)} 0

從輸出中可以發現,兩個任務使用了兩個不同的執行緒來執行,如果將最大任務併發數量設定為1這裡就會使用同一個執行緒序列執行,任務2必須得等任務1執行完成才能開始執行,就不再做實驗了。使用Foundation提供的NSBlockOperationNSInvocationOperation很方便,這兩個子類已經幫我們完成了各個重要屬性的設定操作,當block或傳入的方法任務在執行時會設定executing屬性值為YES,執行完成後將executing設定為NO並將finished設定為YES,但是,如果在block中使用另一個執行緒或是GCD非同步執行任務,block或方法會立即返回,此時就會將finished設定為YES,但是其實任務並沒有完成,所以這種情況下不能使用該屬性,當需要更高定製性時需要使用自定義NSOperation子類。

這個栗子很簡單,效果就和我們使用GCD編寫的多執行緒程式一樣,接下來再舉個新增依賴的栗子:

- (void)task:(id)obj
{
    for (int i = 0; i < 100; i++)
    {
        NSLog(@"Task5 %@ %d %@", [NSThread currentThread], i, obj);
    }
}

- (void)viewWillAppear:(BOOL)animated
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:4];

    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    }];

    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    }];

    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    }];

    NSBlockOperation *operation4 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
        }
    }];

    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task:) object:@"Hello, World!"];

    [operation2 addDependency:operation1];
    [operation3 addDependency:operation1];
    [operation4 addDependency:operation3];

    [queue addOperation:operation1];
    [queue addOperation:operation2];
    [queue addOperation:operation3];
    [queue addOperation:operation4];
    [queue addOperation:invocationOperation];
}

上述栗子添加了五個任務,任務依賴關係如下圖所示:

任務依賴關係圖

如圖所示,任務2依賴任務1,任務3依賴任務1,任務4依賴任務3,而任務5是獨立的,所以任務2需要等待任務1完成後才可以開始執行,任務3也是同樣,而任務4需要等待任務3完成後才可以開始執行,所以任務34是序列執行的,任務5是獨立的沒有任何依賴,所以任務5與其他任務並行執行,輸出結果就不給出了,我們還可以根據業務的不同設定不同的更復雜的依賴。

自定義NSOperation子類

經過前文的講解,關於NSOperationNSOperationQueue的基礎使用已經有了一個初步的掌握,如果我們去閱讀AFNetworkingSDWebImage的原始碼時可以發現,這些庫中大量用了NSOperationNSOperationQueue,當然也用了GCD,比如SDWebImage下載圖片的任務是自定義的NSOperation子類SDWebImageDownloaderOperation,之所以選擇使用自定義子類,正是因為自定義子類可以提供更多定製化的方法,而不僅僅侷限於一個block或一個方法,接下來將講解具體的自定義實現方法。

在官方文件中指出經自定義NSOperation子類有兩種形式,併發和非併發,非併發形式只需要繼承NSOperation類後實現main方法即可,而併發形式就比較複雜了,接下來會分別介紹兩種形式。

非併發的NSOperation自定義子類

官方文件中有說明,非併發的自定義子類只需要實現main方法即可,栗子如下:

@interface TestOperation: NSOperation

@property (nonatomic, copy) id obj;

- (instancetype)initWithObject:(id)obj;

@end

@implementation TestOperation

- (instancetype)initWithObject:(id)obj
{
    if (self = [super init])
    {
        self.obj = obj;
    }
    return self;
}

- (void)main
{
    for (int i = 0; i < 100; i++)
    {
        NSLog(@"Task %@ %d %@", self.obj, i, [NSThread currentThread]);
    }
    NSLog(@"Task Complete!");
}

@end

- (void)viewWillAppear:(BOOL)animated
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:4];

    TestOperation *operation = [[TestOperation alloc] initWithObject:@"Hello, World!"];
    [operation main];
    //[operation start];
    //[queue addOperation:operation];

}

上述栗子也很簡單,就是自定義子類繼承了NSOperation並且實現了main方法,在官方文件中指出,非併發任務,直接呼叫main方法即可,呼叫之後就和呼叫普通物件的方法一樣,使用當前執行緒來執行main方法,在本慄中即主執行緒,這個栗子沒有什麼特別奇特的地方,但其實也可以將其加入到佇列中,但這樣存在一個問題,由於我們沒有實現finished屬性,所以獲取finished屬性時只會返回NO,任務加入到佇列後不會被佇列刪除,一直會儲存,而且任務執行完成後的回撥塊也不會執行,所以最好不要只實現一個main方法就交給佇列去執行,即使我們沒有實現start方法,這裡呼叫start方法以後依舊會執行main方法。這個非併發版本不建議寫,好像也沒有什麼場景需要這樣寫,反而更加複雜,如果不小心加入到佇列中還會產生未知的錯誤。

併發的NSOperation自定義子類

關於併發的NSOperation自定義子類就比較複雜了,但可以提供更高的可定製性,這也是為什麼SDWebImage使用自定義子類來實現下載任務。

按照官方文件的要求,實現併發的自定義子類需要重寫以下幾個方法或屬性:

  • start方法: 任務加入到佇列後,佇列會管理任務並在執行緒被排程後適時呼叫start方法,start方法就是我們編寫的任務,需要注意的是,不論怎樣都不允許呼叫父類的start方法

  • isExecuting: 任務是否正在執行,需要手動呼叫KVO方法來進行通知,這樣,其他類如果監聽了任務的該屬性就可以獲取到通知

  • isFinished: 任務是否結束,需要手動呼叫KVO方法來進行通知,佇列也需要監聽該屬性的值,用於判斷任務是否結束,由於我們編寫的任務很可能是非同步的,所以start方法返回也不一定代表任務就結束了,任務結束需要開發者手動修改該屬性的值,佇列就可以正常的移除任務

  • isAsynchronous: 是否併發執行,之前需要使用isConcurrent,但isConcurrent被廢棄了,該屬性標識是否併發

直接看栗子吧:

@interface MyOperation: NSOperation

@property (nonatomic, assign, getter=isExecuting) BOOL executing;
@property (nonatomic, assign, getter=isFinished) BOOL finished;

@end

@implementation MyOperation

@synthesize executing = _executing;
@synthesize finished = _finished;

- (void)start
{
    //在任務開始前設定executing為YES,在此之前可能會進行一些初始化操作
    self.executing = YES;
    for (int i = 0; i < 500; i++)
    {
        /*
        需要在適當的位置判斷外部是否呼叫了cancel方法
        如果被cancel了需要正確的結束任務
        */
        if (self.isCancelled)
        {
            //任務被取消正確結束前手動設定狀態
            self.executing = NO;
            self.finished = YES;
            return;
        }
        //輸出任務的各個狀態以及佇列的任務數
        NSLog(@"Task %d %@ Cancel:%d Executing:%d Finished:%d QueueOperationCount:%ld", i, [NSThread currentThread], self.cancelled, self.executing, self.finished, [[NSOperationQueue currentQueue] operationCount]);
        [NSThread sleepForTimeInterval:0.1];
    }
    NSLog(@"Task Complete.");
    //任務執行完成後手動設定狀態
    self.executing = NO;
    self.finished = YES;
}

- (void)setExecuting:(BOOL)executing
{
    //呼叫KVO通知
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    //呼叫KVO通知
    [self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isExecuting
{
    return _executing;
}

- (void)setFinished:(BOOL)finished
{
    //呼叫KVO通知
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    //呼叫KVO通知
    [self didChangeValueForKey:@"isFinished"];
}

- (BOOL)isFinished
{
    return _finished;
}

- (BOOL)isAsynchronous
{
    return YES;
}

@end

- (void)cancelButtonClicked
{
    [self.myOperation cancel];
}

- (void)btnClicked
{
    NSLog(@"MyOperation Cancel:%d Executing:%d Finished:%d QueueOperationCount:%ld", self.myOperation.isCancelled, self.myOperation.isExecuting, self.myOperation.isFinished, self.queue.operationCount);    
}

- (void)viewWillAppear:(BOOL)animated
{
    self.queue = [[NSOperationQueue alloc] init];
    [self.queue setMaxConcurrentOperationCount:1];

    self.myOperation = [[MyOperation alloc] init];
    [self.queue addOperation:self.myOperation];

}

上面的栗子也比較簡單,各個狀態需要根據業務邏輯來設定,需要注意的是,一定要正確的設定各個狀態,並且在設定狀態時需要手動觸發KVO進行通知,因為可能有其他物件在監聽任務的某一個狀態,比如finished屬性,佇列就會監聽任務的屬性,start方法內部很可能會有非同步方法的執行,所以start方法返回並不代表任務結束,佇列不能根據start方法是否返回來判斷任務是否結束,所以需要開發者手動修改相關屬性並觸發KVO通知。

上述栗子的輸出如下:

//任務的輸出內容
Task 95 <NSThread: 0x1c027c780>{number = 3, name = (null)} Cancel:0 Executing:1 Finished:0 QueueOperationCount:1

//任務正在執行的時候,點選按鈕的輸出
MyOperation Cancel:0 Executing:1 Finished:0 QueueOperationCount:1

//當任務執行完成後,點選按鈕的輸出
MyOperation Cancel:0 Executing:0 Finished:1 QueueOperationCount:0

從輸出中可以看到任務和執行佇列的相關屬性的變化。

接下來舉一個下載檔案的栗子,使用自定義的NSOperation子類,提供了取消下載的功能,具體程式碼如下:

//FileDownloadOperation.h檔案程式碼
#ifndef FileDownloadOperation_h
#define FileDownloadOperation_h

#import <Foundation/Foundation.h>

@class FileDownloadOperation;

//定義一個協議,用於反饋下載狀態
@protocol FileDownloadDelegate <NSObject>

@optional
- (void)fileDownloadOperation:(FileDownloadOperation *)downloadOperation downloadProgress:(double)progress;
- (void)fileDownloadOperation:(FileDownloadOperation *)downloadOperation didFinishWithData:(NSData *)data;
- (void)fileDownloadOperation:(FileDownloadOperation *)downloadOperation didFailWithError:(NSError *)error;

@end

@interface FileDownloadOperation: NSOperation
//定義代理物件
@property (nonatomic, weak) id<FileDownloadDelegate> delegate;
//初始化建構函式,檔案URL
- (instancetype)initWithURL:(NSURL*)url;

@end

#endif /* FileDownloadOperation_h */

FileDownloadOperation.m檔案原始碼如下:

#import "FileDownloadOperation.h"

@interface FileDownloadOperation() <NSURLConnectionDelegate>

//定義executing屬性
@property (nonatomic, assign, getter=isExecuting) BOOL executing;
//定義finished屬性
@property (nonatomic, assign, getter=isFinished) BOOL finished;

//要下載的檔案的URL
@property (nonatomic, strong) NSURL *fileURL;
//使用NSURLConnection進行網路資料的獲取
@property (nonatomic, strong) NSURLConnection *connection;
//定義一個可變的NSMutableData物件,用於新增獲取的資料
@property (nonatomic, strong) NSMutableData *fileMutableData;
//記錄要下載檔案的總長度
@property (nonatomic, assign) NSUInteger fileTotalLength;
//記錄已經下載了的檔案的長度
@property (nonatomic, assign) NSUInteger downloadedLength;

@end

@implementation FileDownloadOperation

@synthesize delegate = _delegate;

@synthesize executing = _executing;
@synthesize finished = _finished;

@synthesize fileURL = _fileURL;
@synthesize connection = _connection;
@synthesize fileMutableData = _fileMutableData;
@synthesize fileTotalLength = _fileTotalLength;
@synthesize downloadedLength = _downloadedLength;

//executing屬性的setter
- (void)setExecuting:(BOOL)executing
{
    //設定executing屬性需要手動觸發KVO方法進行通知
    [self willChangeValueForKey:@"executing"];
    _executing = executing;
    [self didChangeValueForKey:@"executing"];
}
//executing屬性的getter
- (BOOL)isExecuting
{
    return _executing;
}
//finished屬性的setter
- (void)setFinished:(BOOL)finished
{
    //同上,需要手動觸發KVO方法進行通知
    [self willChangeValueForKey:@"finished"];
    _finished = finished;
    [self didChangeValueForKey:@"finished"];
}
//finished屬性的getter
- (BOOL)isFinished
{
    return _finished;
}
//返回YES標識為併發Operation
- (BOOL)isAsynchronous
{
    return YES;
}
//內部函式,用於結束任務
- (void)finishTask
{
    //中斷網路連線
    [self.connection cancel];
    //設定finished屬性為YES,將任務從佇列中移除
    //會呼叫setter方法,並觸發KVO方法進行通知
    self.finished = YES;
    //設定executing屬性為NO
    self.executing = NO;
}
//初始化建構函式
- (instancetype)initWithURL:(NSURL *)url
{
    if (self = [super init])
    {
        self.fileURL = url;

        self.fileMutableData = [[NSMutableData alloc] init];
        self.fileTotalLength = 0;
        self.downloadedLength = 0;
    }
    return self;
}
//重寫start方法
- (void)start
{
    //任務開始執行前檢查是否被取消,取消就結束任務
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //構造NSURLConnection物件,並設定不立即開始,手動開始
    self.connection = [[NSURLConnection alloc] initWithRequest:[[NSURLRequest alloc] initWithURL:self.fileURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:25] delegate:self startImmediately:NO];
    //判斷是否連線,沒有連線就結束任務
    if (self.connection == nil)
    {
        [self finishTask];
        return;
    }
    //成功連線到伺服器後檢查是否取消任務,取消任務就結束
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //設定任務開始執行
    self.executing = YES;
    //獲取當前RunLoop
    NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    //將任務交由RunLoop規劃
    [self.connection scheduleInRunLoop:currentRunLoop forMode:NSRunLoopCommonModes];
    //開始從服務端獲取資料
    [self.connection start];
    //判斷執行任務的是否為主執行緒
    if (currentRunLoop != [NSRunLoop mainRunLoop])
    {
        //不為主執行緒啟動RunLoop
        CFRunLoopRun();
    }
}

//MARK - NSURLConnectionDelegate 方法

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    //獲取並設定將要下載檔案的長度大小
    self.fileTotalLength = response.expectedContentLength;
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    //網路獲取失敗,呼叫代理方法
    if ([self.delegate respondsToSelector:@selector(fileDownloadOperation:didFailWithError:)])
    {
        //需要將代理方法放到主執行緒中執行,防止代理方法需要修改UI
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate fileDownloadOperation:self didFailWithError:error];
        });
    }
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    //收到資料包後判斷任務是否取消,取消則結束任務
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //新增獲取的資料
    [self.fileMutableData appendData:data];
    //修改已下載檔案長度
    self.downloadedLength += [data length];
    //呼叫回撥函式
    if ([self.delegate respondsToSelector:@selector(fileDownloadOperation:downloadProgress:)])
    {
        //計算下載比例
        double progress = self.downloadedLength * 1.0 / self.fileTotalLength;
        //同上,放在主執行緒中呼叫,防止主執行緒有修改UI的操作
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate fileDownloadOperation:self downloadProgress:progress];
        });
    }
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    //網路下載完成前檢查是否取消任務,取消就結束任務
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //呼叫回撥函式
    if ([self.delegate respondsToSelector:@selector(fileDownloadOperation:didFinishWithData:)])
    {
        //同理,放在主執行緒中呼叫
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate fileDownloadOperation:self didFinishWithData:self.fileMutableData];
        });
    }
    //下載完成,任務結束
    [self finishTask];
}

@end

上述程式碼的註釋很詳盡,就不再贅述了,只提供了取消下載的功能,還可以新增暫停和斷點下載的功能,讀者可自行實現。具體效果如下圖,點選取消後就會結束任務:

下載的效果

備註

由於作者水平有限,難免出現紕漏,如有問題還請不吝賜教。