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

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

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

本系列文章主要講解iOS中多線程的使用爵赵,包括:NSThread档插、GCD、NSOperation以及RunLoop的使用方法詳解亚再,本系列文章不涉及基礎(chǔ)的線程/進程、同步/異步威彰、阻塞/非阻塞征绸、串行/并行剃浇,這些基礎(chǔ)概念,有不明白的讀者還請自行查閱如捅。本系列文章將分以下幾篇文章進行講解,讀者可按需查閱调煎。

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ù)進行的封裝,封裝好的任務(wù)交給不同的NSOperationQueue即可進行串行隊列的執(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一個機制
自定義子類時需要使用該屬性判斷是否在外部觸發(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ù)并添加進了隊列,摘取幾個輸出如下:

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方法來進行通知,這樣崔拥,其他類如果監(jiān)聽了任務(wù)的該屬性就可以獲取到通知

  • isFinished: 任務(wù)是否結(jié)束极舔,需要手動調(diào)用KVO方法來進行通知,隊列也需要監(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,在此之前可能會進行一些初始化操作
    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進行通知喊递,因為可能有其他對象在監(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進行網(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方法進行通知
    [self willChangeValueForKey:@"executing"];
    _executing = executing;
    [self didChangeValueForKey:@"executing"];
}
//executing屬性的getter
- (BOOL)isExecuting
{
    return _executing;
}
//finished屬性的setter
- (void)setFinished:(BOOL)finished
{
    //同上驱证,需要手動觸發(fā)KVO方法進行通知
    [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方法進行通知
    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)系作者
  • 序言:七十年代末不同,一起剝皮案震驚了整個濱河市厉膀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖站蝠,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卓鹿,居然都是意外死亡菱魔,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門吟孙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澜倦,“玉大人,你說我怎么就攤上這事杰妓≡逯危” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵巷挥,是天一觀的道長桩卵。 經(jīng)常有香客問我,道長倍宾,這世上最難降的妖魔是什么雏节? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮高职,結(jié)果婚禮上钩乍,老公的妹妹穿的比我還像新娘。我一直安慰自己怔锌,他們只是感情好寥粹,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著埃元,像睡著了一般涝涤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上亚情,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天妄痪,我揣著相機與錄音,去河邊找鬼楞件。 笑死衫生,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的土浸。 我是一名探鬼主播罪针,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼黄伊!你這毒婦竟也來了泪酱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎墓阀,沒想到半個月后毡惜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡斯撮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年经伙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勿锅。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡帕膜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出溢十,到底是詐尸還是另有隱情垮刹,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布张弛,位于F島的核電站荒典,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏乌庶。R本人自食惡果不足惜种蝶,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瞒大。 院中可真熱鬧螃征,春花似錦、人聲如沸透敌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酗电。三九已至魄藕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撵术,已是汗流浹背背率。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嫩与,地道東北人寝姿。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像划滋,于是被迫代替她去往敵國和親饵筑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容