iOS多線程——你要知道的NSOperation都在這里

你要知道的iOS多線程NSThread、GCD、NSOperation、RunLoop都在這里

轉(zhuǎn)載請注明出處 http://www.itdecent.cn/p/bf0916ee1492

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

NSOperation&&NSOperationQueue的使用姿勢全解

經(jīng)過前面的學(xué)習(xí),講解了最基礎(chǔ)的NSThread使用方法,封裝更完善的GCD,GCD提供了極其便捷的方法來編寫多線程程序,可以自動實現(xiàn)多核的真正并行計算,自動管理線程的生命周期,好處不言而喻,但可定制性就有點不足了,Foundation框架提供了NSOperationNSOperationQueue這一面向?qū)ο蟮亩嗑€程類,這兩個類與GCD提供的功能類似,NSOperation提供任務(wù)的封裝,NSOperationQueue顧名思義,提供執(zhí)行隊列,可以自動實現(xiàn)多核并行計算,自動管理線程的生命周期,如果是并發(fā)的情況,其底層也使用線程池模型來管理,基本上可以說這兩個類提供的功能覆蓋了GCD,并且提供了更多可定制的開發(fā)方式,開發(fā)者可以按需選擇。

使用NSOperationNSOperationQueue來編寫多線程程序非常簡單,只需要創(chuàng)建一個任務(wù)對象,創(chuàng)建一個執(zhí)行隊列或者和獲取主線程一樣獲取一個主任務(wù)隊列,然后將任務(wù)提交給隊列即可實現(xiàn)并發(fā),如過你想要串行只需要將隊列的并發(fā)數(shù)設(shè)置為一即可。接下來將分別介紹兩個類的使用。

NSOperation “任務(wù)的封裝”

GCD類似,GCD向隊列提交任務(wù),NSOperation就是對任務(wù)進(jìn)行的封裝,封裝好的任務(wù)交給不同的NSOperationQueue即可進(jìn)行串行隊列的執(zhí)行或并發(fā)隊列的執(zhí)行。這里的任務(wù)就是NSOperation類的一個方法,main方法或start方法(兩個方法有區(qū)別,后文會講),但NSOperation類的這兩個方法是空方法,沒有干任何事情,因此,我們需要自定義繼承NSOperation類并重寫相關(guān)方法,OC也提供了兩個子類供我們使用NSBlockOperationNSInvocationOperation

接下來看一下NSOperation類中比較重要的屬性和方法:

/*
對于并發(fā)Operation需要重寫該方法
也可以不把operation加入到隊列中,手動觸發(fā)執(zhí)行,與調(diào)用普通方法一樣
*/
- (void)start;

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

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

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

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

/*
只讀屬性,任務(wù)是否結(jié)束,如果自定義子類,需要重寫該方法
對于加入到隊列的任務(wù)來說,當(dāng)finished設(shè)置為YES后,隊列會將任務(wù)移除出隊列
*/
@property (readonly, getter=isFinished) BOOL finished;

//是否為并發(fā)任務(wù),該屬性已經(jīng)被標(biāo)識即將棄用,應(yīng)該使用下面的asynchronous屬性
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below

/*
只讀屬性,判斷任務(wù)是否為并發(fā)任務(wù),默認(rèn)返回NO
如果需要自定義并發(fā)任務(wù)子類,需要重寫getter方法返回YES
*/
@property (readonly, getter=isAsynchronous) BOOL asynchronous;

/*
只讀屬性,任務(wù)是否準(zhǔn)備就緒
對于加入隊列的任務(wù),當(dāng)ready為YES,標(biāo)識該任務(wù)即將開始執(zhí)行
如果任務(wù)有依賴的任務(wù)沒有執(zhí)行完成ready為NO
*/
@property (readonly, getter=isReady) BOOL ready;

/*
添加一個NSOperation為當(dāng)前任務(wù)的依賴
如果一個任務(wù)有依賴,需要等待依賴的任務(wù)執(zhí)行完成才能開始執(zhí)行
*/
- (void)addDependency:(NSOperation *)op;

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

//任務(wù)在隊列里的優(yōu)先級
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

//任務(wù)在隊列里的優(yōu)先級
@property NSOperationQueuePriority queuePriority;

/*
任務(wù)完成后的回調(diào)方法
當(dāng)finished屬性設(shè)置為YES時才會執(zhí)行該回調(diào)
*/
@property (nullable, copy) void (^completionBlock)(void);

上述內(nèi)容中有一些屬性和方法是在自定義NSOperation子類中必須要重寫的,自定義子類能夠提供更高的可定制性,因此,編寫自定義子類更復(fù)雜,自定義子類在后面會講,如果我們只需要實現(xiàn)GCD那樣的功能,提交一個并發(fā)的任務(wù),OC為我們提供了兩個子類NSBlockOperationNSInvocationOperation,這兩個子類已經(jīng)幫我們完成了各種屬性的設(shè)置操作,我們只需要編寫一個任務(wù)的block或者一個方法即可像使用GCD一樣方便的編寫多線程程序。

接下來舉兩個創(chuàng)建任務(wù)的栗子:

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

/*
創(chuàng)建一個NSInvocationOperation對象,指定執(zhí)行的對象和方法
該方法可以接收一個參數(shù)即object
*/
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task:) object:@"Hello, World!"];

可以發(fā)現(xiàn),創(chuàng)建任務(wù)真的很簡單,就像GCD中創(chuàng)建任務(wù)一樣簡潔,任務(wù)創(chuàng)建完成就可以創(chuàng)建隊列了。

NSOperationQueue

NSOperationQueue就是任務(wù)的執(zhí)行隊列,看一下該類中有哪些比較重要的屬性和方法:

//向隊列中添加一個任務(wù)
- (void)addOperation:(NSOperation *)op;

/*
向隊列中添加一組任務(wù)
是否等待任務(wù)完成,如果YES,則阻塞當(dāng)前線程直到所有任務(wù)完成
如果為False,不阻塞當(dāng)前線程
*/
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;

//向隊列中添加一個任務(wù),任務(wù)以block的形式傳入,使用更方便
- (void)addOperationWithBlock:(void (^)(void))block;

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

//獲取隊列中的任務(wù)數(shù)量
@property (readonly) NSUInteger operationCount;

/*
隊列支持的最大任務(wù)并發(fā)數(shù)
如果為1,則只支持同時處理一個任務(wù),即串行隊列,主隊列就是串行隊列使用主線程執(zhí)行任務(wù)
如果為大于1的數(shù),則支持同時處理多個任務(wù),即并發(fā)隊列,底層使用線程池管理多個線程來執(zhí)行任務(wù)
*/
@property NSInteger maxConcurrentOperationCount;

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

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

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

//阻塞當(dāng)前線程直到所有任務(wù)完成
- (void)waitUntilAllOperationsAreFinished;

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

//類屬性,獲取主隊列,任務(wù)添加到主隊列就會使用主線程執(zhí)行,主隊列的任務(wù)并發(fā)數(shù)為1,即串行隊列
@property (class, readonly, strong) NSOperationQueue *mainQueue;

上述屬性中比較重要的就是maxConcurrentOperationCount,該屬性直接決定了隊列是串行的還是并發(fā)的,接下來看一個栗子:

- (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];
}

上面這個栗子就很簡單了,首先創(chuàng)建了一個隊列,最大任務(wù)并發(fā)數(shù)設(shè)置為2,接下來創(chuàng)建了兩個任務(wù)并添加進(jìn)了隊列,摘取幾個輸出如下:

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

從輸出中可以發(fā)現(xiàn),兩個任務(wù)使用了兩個不同的線程來執(zhí)行,如果將最大任務(wù)并發(fā)數(shù)量設(shè)置為1這里就會使用同一個線程串行執(zhí)行,任務(wù)2必須得等任務(wù)1執(zhí)行完成才能開始執(zhí)行,就不再做實驗了。使用Foundation提供的NSBlockOperationNSInvocationOperation很方便,這兩個子類已經(jīng)幫我們完成了各個重要屬性的設(shè)置操作,當(dāng)block或傳入的方法任務(wù)在執(zhí)行時會設(shè)置executing屬性值為YES,執(zhí)行完成后將executing設(shè)置為NO并將finished設(shè)置為YES,但是,如果在block中使用另一個線程或是GCD異步執(zhí)行任務(wù),block或方法會立即返回,此時就會將finished設(shè)置為YES,但是其實任務(wù)并沒有完成,所以這種情況下不能使用該屬性,當(dāng)需要更高定制性時需要使用自定義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];
}

上述栗子添加了五個任務(wù),任務(wù)依賴關(guān)系如下圖所示:

任務(wù)依賴關(guān)系圖

如圖所示,任務(wù)2依賴任務(wù)1,任務(wù)3依賴任務(wù)1,任務(wù)4依賴任務(wù)3,而任務(wù)5是獨立的,所以任務(wù)2需要等待任務(wù)1完成后才可以開始執(zhí)行,任務(wù)3也是同樣,而任務(wù)4需要等待任務(wù)3完成后才可以開始執(zhí)行,所以任務(wù)34是串行執(zhí)行的,任務(wù)5是獨立的沒有任何依賴,所以任務(wù)5與其他任務(wù)并行執(zhí)行,輸出結(jié)果就不給出了,我們還可以根據(jù)業(yè)務(wù)的不同設(shè)置不同的更復(fù)雜的依賴。

自定義NSOperation子類

經(jīng)過前文的講解,關(guān)于NSOperationNSOperationQueue的基礎(chǔ)使用已經(jīng)有了一個初步的掌握,如果我們?nèi)ラ喿xAFNetworkingSDWebImage的源碼時可以發(fā)現(xiàn),這些庫中大量用了NSOperationNSOperationQueue,當(dāng)然也用了GCD,比如SDWebImage下載圖片的任務(wù)是自定義的NSOperation子類SDWebImageDownloaderOperation,之所以選擇使用自定義子類,正是因為自定義子類可以提供更多定制化的方法,而不僅僅局限于一個block或一個方法,接下來將講解具體的自定義實現(xiàn)方法。

在官方文檔中指出經(jīng)自定義NSOperation子類有兩種形式,并發(fā)和非并發(fā),非并發(fā)形式只需要繼承NSOperation類后實現(xiàn)main方法即可,而并發(fā)形式就比較復(fù)雜了,接下來會分別介紹兩種形式。

非并發(fā)的NSOperation自定義子類

官方文檔中有說明,非并發(fā)的自定義子類只需要實現(xiàn)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并且實現(xiàn)了main方法,在官方文檔中指出,非并發(fā)任務(wù),直接調(diào)用main方法即可,調(diào)用之后就和調(diào)用普通對象的方法一樣,使用當(dāng)前線程來執(zhí)行main方法,在本栗中即主線程,這個栗子沒有什么特別奇特的地方,但其實也可以將其加入到隊列中,但這樣存在一個問題,由于我們沒有實現(xiàn)finished屬性,所以獲取finished屬性時只會返回NO,任務(wù)加入到隊列后不會被隊列刪除,一直會保存,而且任務(wù)執(zhí)行完成后的回調(diào)塊也不會執(zhí)行,所以最好不要只實現(xiàn)一個main方法就交給隊列去執(zhí)行,即使我們沒有實現(xiàn)start方法,這里調(diào)用start方法以后依舊會執(zhí)行main方法。這個非并發(fā)版本不建議寫,好像也沒有什么場景需要這樣寫,反而更加復(fù)雜,如果不小心加入到隊列中還會產(chǎn)生未知的錯誤。

并發(fā)的NSOperation自定義子類

關(guān)于并發(fā)的NSOperation自定義子類就比較復(fù)雜了,但可以提供更高的可定制性,這也是為什么SDWebImage使用自定義子類來實現(xiàn)下載任務(wù)。

按照官方文檔的要求,實現(xiàn)并發(fā)的自定義子類需要重寫以下幾個方法或?qū)傩?

  • start方法: 任務(wù)加入到隊列后,隊列會管理任務(wù)并在線程被調(diào)度后適時調(diào)用start方法,start方法就是我們編寫的任務(wù),需要注意的是,不論怎樣都不允許調(diào)用父類的start方法

  • isExecuting: 任務(wù)是否正在執(zhí)行,需要手動調(diào)用KVO方法來進(jìn)行通知,這樣,其他類如果監(jiān)聽了任務(wù)的該屬性就可以獲取到通知

  • isFinished: 任務(wù)是否結(jié)束,需要手動調(diào)用KVO方法來進(jìn)行通知,隊列也需要監(jiān)聽該屬性的值,用于判斷任務(wù)是否結(jié)束,由于我們編寫的任務(wù)很可能是異步的,所以start方法返回也不一定代表任務(wù)就結(jié)束了,任務(wù)結(jié)束需要開發(fā)者手動修改該屬性的值,隊列就可以正常的移除任務(wù)

  • isAsynchronous: 是否并發(fā)執(zhí)行,之前需要使用isConcurrent,但isConcurrent被廢棄了,該屬性標(biāo)識是否并發(fā)

直接看栗子吧:

@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
{
    //在任務(wù)開始前設(shè)置executing為YES,在此之前可能會進(jìn)行一些初始化操作
    self.executing = YES;
    for (int i = 0; i < 500; i++)
    {
        /*
        需要在適當(dāng)?shù)奈恢门袛嗤獠渴欠裾{(diào)用了cancel方法
        如果被cancel了需要正確的結(jié)束任務(wù)
        */
        if (self.isCancelled)
        {
            //任務(wù)被取消正確結(jié)束前手動設(shè)置狀態(tài)
            self.executing = NO;
            self.finished = YES;
            return;
        }
        //輸出任務(wù)的各個狀態(tài)以及隊列的任務(wù)數(shù)
        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.");
    //任務(wù)執(zhí)行完成后手動設(shè)置狀態(tài)
    self.executing = NO;
    self.finished = YES;
}

- (void)setExecuting:(BOOL)executing
{
    //調(diào)用KVO通知
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    //調(diào)用KVO通知
    [self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isExecuting
{
    return _executing;
}

- (void)setFinished:(BOOL)finished
{
    //調(diào)用KVO通知
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    //調(diào)用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];
    
}

上面的栗子也比較簡單,各個狀態(tài)需要根據(jù)業(yè)務(wù)邏輯來設(shè)置,需要注意的是,一定要正確的設(shè)置各個狀態(tài),并且在設(shè)置狀態(tài)時需要手動觸發(fā)KVO進(jìn)行通知,因為可能有其他對象在監(jiān)聽任務(wù)的某一個狀態(tài),比如finished屬性,隊列就會監(jiān)聽任務(wù)的屬性,start方法內(nèi)部很可能會有異步方法的執(zhí)行,所以start方法返回并不代表任務(wù)結(jié)束,隊列不能根據(jù)start方法是否返回來判斷任務(wù)是否結(jié)束,所以需要開發(fā)者手動修改相關(guān)屬性并觸發(fā)KVO通知。

上述栗子的輸出如下:

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

//任務(wù)正在執(zhí)行的時候,點擊按鈕的輸出
MyOperation Cancel:0 Executing:1 Finished:0 QueueOperationCount:1

//當(dāng)任務(wù)執(zhí)行完成后,點擊按鈕的輸出
MyOperation Cancel:0 Executing:0 Finished:1 QueueOperationCount:0

從輸出中可以看到任務(wù)和執(zhí)行隊列的相關(guān)屬性的變化。

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

//FileDownloadOperation.h文件代碼
#ifndef FileDownloadOperation_h
#define FileDownloadOperation_h

#import <Foundation/Foundation.h>

@class FileDownloadOperation;

//定義一個協(xié)議,用于反饋下載狀態(tài)
@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;
//初始化構(gòu)造函數(shù),文件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進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)的獲取
@property (nonatomic, strong) NSURLConnection *connection;
//定義一個可變的NSMutableData對象,用于添加獲取的數(shù)據(jù)
@property (nonatomic, strong) NSMutableData *fileMutableData;
//記錄要下載文件的總長度
@property (nonatomic, assign) NSUInteger fileTotalLength;
//記錄已經(jīng)下載了的文件的長度
@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
{
    //設(shè)置executing屬性需要手動觸發(fā)KVO方法進(jìn)行通知
    [self willChangeValueForKey:@"executing"];
    _executing = executing;
    [self didChangeValueForKey:@"executing"];
}
//executing屬性的getter
- (BOOL)isExecuting
{
    return _executing;
}
//finished屬性的setter
- (void)setFinished:(BOOL)finished
{
    //同上,需要手動觸發(fā)KVO方法進(jìn)行通知
    [self willChangeValueForKey:@"finished"];
    _finished = finished;
    [self didChangeValueForKey:@"finished"];
}
//finished屬性的getter
- (BOOL)isFinished
{
    return _finished;
}
//返回YES標(biāo)識為并發(fā)Operation
- (BOOL)isAsynchronous
{
    return YES;
}
//內(nèi)部函數(shù),用于結(jié)束任務(wù)
- (void)finishTask
{
    //中斷網(wǎng)絡(luò)連接
    [self.connection cancel];
    //設(shè)置finished屬性為YES,將任務(wù)從隊列中移除
    //會調(diào)用setter方法,并觸發(fā)KVO方法進(jìn)行通知
    self.finished = YES;
    //設(shè)置executing屬性為NO
    self.executing = NO;
}
//初始化構(gòu)造函數(shù)
- (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
{
    //任務(wù)開始執(zhí)行前檢查是否被取消,取消就結(jié)束任務(wù)
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //構(gòu)造NSURLConnection對象,并設(shè)置不立即開始,手動開始
    self.connection = [[NSURLConnection alloc] initWithRequest:[[NSURLRequest alloc] initWithURL:self.fileURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:25] delegate:self startImmediately:NO];
    //判斷是否連接,沒有連接就結(jié)束任務(wù)
    if (self.connection == nil)
    {
        [self finishTask];
        return;
    }
    //成功連接到服務(wù)器后檢查是否取消任務(wù),取消任務(wù)就結(jié)束
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //設(shè)置任務(wù)開始執(zhí)行
    self.executing = YES;
    //獲取當(dāng)前RunLoop
    NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    //將任務(wù)交由RunLoop規(guī)劃
    [self.connection scheduleInRunLoop:currentRunLoop forMode:NSRunLoopCommonModes];
    //開始從服務(wù)端獲取數(shù)據(jù)
    [self.connection start];
    //判斷執(zhí)行任務(wù)的是否為主線程
    if (currentRunLoop != [NSRunLoop mainRunLoop])
    {
        //不為主線程啟動RunLoop
        CFRunLoopRun();
    }
}

//MARK - NSURLConnectionDelegate 方法

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    //獲取并設(shè)置將要下載文件的長度大小
    self.fileTotalLength = response.expectedContentLength;
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    //網(wǎng)絡(luò)獲取失敗,調(diào)用代理方法
    if ([self.delegate respondsToSelector:@selector(fileDownloadOperation:didFailWithError:)])
    {
        //需要將代理方法放到主線程中執(zhí)行,防止代理方法需要修改UI
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate fileDownloadOperation:self didFailWithError:error];
        });
    }
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    //收到數(shù)據(jù)包后判斷任務(wù)是否取消,取消則結(jié)束任務(wù)
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //添加獲取的數(shù)據(jù)
    [self.fileMutableData appendData:data];
    //修改已下載文件長度
    self.downloadedLength += [data length];
    //調(diào)用回調(diào)函數(shù)
    if ([self.delegate respondsToSelector:@selector(fileDownloadOperation:downloadProgress:)])
    {
        //計算下載比例
        double progress = self.downloadedLength * 1.0 / self.fileTotalLength;
        //同上,放在主線程中調(diào)用,防止主線程有修改UI的操作
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate fileDownloadOperation:self downloadProgress:progress];
        });
    }
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    //網(wǎng)絡(luò)下載完成前檢查是否取消任務(wù),取消就結(jié)束任務(wù)
    if (self.isCancelled)
    {
        [self finishTask];
        return;
    }
    //調(diào)用回調(diào)函數(shù)
    if ([self.delegate respondsToSelector:@selector(fileDownloadOperation:didFinishWithData:)])
    {
        //同理,放在主線程中調(diào)用
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate fileDownloadOperation:self didFinishWithData:self.fileMutableData];
        });
    }
    //下載完成,任務(wù)結(jié)束
    [self finishTask];
}

@end

上述代碼的注釋很詳盡,就不再贅述了,只提供了取消下載的功能,還可以添加暫停和斷點下載的功能,讀者可自行實現(xiàn)。具體效果如下圖,點擊取消后就會結(jié)束任務(wù):

下載的效果

備注

由于作者水平有限,難免出現(xiàn)紕漏,如有問題還請不吝賜教。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容