写一个基于NSURLSession的网络下载库
前段时间AFNetworking 更新到3.0 ,彻底废弃NSURLConnection的API转由NSURLSession来实现,代码并没有改动很大,AF封装的很好了,读完源码感觉收获很大。
下载不可避免的会遇到多线程异步下载问题,iOS中多线程解决方案一般是GCD和NSOperation。为了下载器便于管理,这里是创建一个类继承于NSOperation重写其start方法即可(GCD版本基本一致,最后会提到其实现代码和思路)
相关代码和下载库地址都在这里:https://github.com/LeeBlaze/LNDownload.git
NSOperation 版本
1 typedef NS_ENUM(NSInteger, DownloadState){ 2 3 DownloadState_Ready = 0, 4 5 DownloadState_Suspend, 6 7 DownloadState_Doing, 8 9 DownloadState_Success, 10 11 DownloadState_Cancel, 12 13 DownloadState_Fail, 14 };
首先定义了下载状态的枚举类型,不多解释,下面看看相关的属性
@protocol LNDownloaderDelegate ; @interface LNDownloader : NSOperation @property (nonatomic, weak)id<LNDownloaderDelegate>delegate; @property (nonatomic, assign,readonly) DownloadState state; @property (nonatomic, assign,readonly) float downloadRate; @property (nonatomic, assign,readonly) float progress; @property (nonatomic, copy, readonly)NSURL *downloadURL; @property (nonatomic, copy, readonly)NSString *downloadPath; @property (nonatomic, copy, readonly)NSString *filePath; @property (nonatomic, copy, readwrite)NSString *fileName; @property (nonatomic, strong, readonly) NSMutableURLRequest *fileRequest; @property (readonly,getter=isSuspend) BOOL isSuspend;
一开始声明了一个代理,用来提供下载进度,下载状态和下载地址等信息,相关代码待会再说。
可以看到几乎所有属性都是只读状态,这里避免用户对下载状态,进度,下载地址等进行更改,由下载库统一管理,只暴露出相关只读属性返回当前任务状态进度等相关信息(只读属性的赋值参见KVO)
在下载回调时,一般有两种常用方式,代理和block,AF中采用了block得方式,这里提供两种方式:
代理和block
/* 利用代理回调的初始化方式; */ - (instancetype) initWithDownloadURL:(NSURL *)url downloafPath:(NSString *)path;
/*
block回调的初始化方式;
*/
- (instancetype) initWithDownloadURL:(NSURL *)url
downloafPath:(NSString *)path
progress:(void (^)(int64_t writtenByte,int64_t totalByte,float progress))progress
error:(void (^)(NSError *error))error
complete:(void (^)(BOOL downloadFinished, NSURLSessionDownloadTask *task))completBlock;
当然还要有下载的开启,取消,删除等方法:
- (void)cancelDownloaderAndRemoveFile:(BOOL)remove; - (void)pause; - (void)resume;
下载库写的比较简略,一些对于错误的处理没有涉及,实际项目中可以加上。
.m文件中:
// Copyright © 2015年 Lee. All rights reserved. // #define FILE_MANAGER [NSFileManager defaultManager] #import "LNDownloader.h" @interface LNDownloader()<NSURLSessionDownloadDelegate> @property (nonatomic, strong) NSMutableURLRequest *fileRequest; @property (nonatomic, copy) NSURL *downloadURL; @property (nonatomic, copy) NSString *downloadPath; @property (nonatomic, assign) DownloadState state; @property (nonatomic, assign) float progress; @property (nonatomic, strong) NSMutableData *receiveData; @property (nonatomic, strong) NSData *resumeData; @property (nonatomic, assign)int64_t writtenByte; @property (nonatomic, assign)int64_t expectTotalByte; @property (nonatomic, strong)NSURLSession *session; @property (nonatomic, strong)NSURLSessionDownloadTask *downloadTask; @property (nonatomic,copy)void(^progressBlock)(int64_t writtenByte,int64_t totalByte,float progress); @property (nonatomic,copy)void(^errorBlock)(NSError *error); @property (nonatomic,copy)void(^completeBlock)(BOOL downloadFinished, NSURLSessionDownloadTask *task);
这里定义了三个block,第一个用来回调下载进度,第二个是下载失败的回调,第三个是下载完成的回调。因为我们在.h文件中声明了一个isSuspend属性,算是对NSOperation属性的一个扩充吧,在.m中我们要实现其getter方法,因此我们需要@dynamic isSuspend
1 @dynamic isSuspend; 2 #pragma mark init 3 - (instancetype)initWithDownloadURL:(NSURL *)url 4 downloafPath:(NSString *)path 5 { 6 self = [super init]; 7 if (self) 8 { 9 self.downloadURL = url; 10 self.state = DownloadState_Ready; 11 self.downloadPath = path; 12 self.fileRequest = [[NSMutableURLRequest alloc] initWithURL:url 13 cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:30]; 14 } 15 return self; 16 } 17 18 - (instancetype)initWithDownloadURL:(NSURL *)url 19 downloafPath:(NSString *)path 20 progress:(void (^)(int64_t, int64_t,float))progress 21 error:(void (^)(NSError *))error 22 complete:(void (^)(BOOL, NSURLSessionDownloadTask *))completBlock 23 { 24 self = [self initWithDownloadURL:url downloafPath:path]; 25 if (self) 26 { 27 self.progressBlock = progress; 28 self.errorBlock = error; 29 self.completeBlock = completBlock; 30 } 31 return self; 32 }
在初始化方法中,分别对downloadUrl state downloadPath和fileRequest等进行了赋值和request的初始化,对于一些progress state属性等是readonly声明,所以在内部赋值的时候需要使用KVC
- (void)start { [self willChangeValueForKey:@"isExecuting"]; self.state = DownloadState_Doing; [self didChangeValueForKey:@"isExecuting"]; self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil]; self.downloadTask = [self.session downloadTaskWithRequest:self.fileRequest]; [self.downloadTask resume]; }
在重写resmue的时候因为可能是暂停下载任务继续下载的情况,所以通过判断resumeData是否为空来获得或新建对应的task.下载的进度和状态完成情况都是通过代理回调给我们需要注意的是
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error这个方法在我们暂停或者下载失败的时候都会回调,因此我们需要通过当前task的状态进行判断:
if (error) { if (self.state == DownloadState_Suspend) { } else { [self downloadWithError:error task:self.downloadTask]; } }
在暂停时,这里没有做任何处理。
下载的progress和状态等通过通知和代理传值,监听相关的通知即可获取需要的所有信息:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { self.writtenByte = totalBytesWritten; self.expectTotalByte = totalBytesExpectedToWrite; dispatch_async(dispatch_get_main_queue(), ^{ if (self.progressBlock) { self.progressBlock(totalBytesWritten,totalBytesExpectedToWrite,self.progress); } if (self.writtenByte *1.0 / self.expectTotalByte > 0.01 || self.writtenByte *1.0 /self.expectTotalByte >= 0.99) { self.progress = (float)self.writtenByte / self.expectTotalByte; if ([self.delegate respondsToSelector:@selector(download:progress:)]) { [self.delegate download:self progress:self.progress]; } } }); } - (void)downloadWithError:(NSError *)error task:(NSURLSessionDownloadTask *)task { if (error) { dispatch_async(dispatch_get_main_queue(), ^{ if (self.errorBlock) { self.errorBlock(error); } if ([self.delegate respondsToSelector:@selector(download:didStopWithError:)]) { [self.delegate download:self didStopWithError:error]; } }); } BOOL success = error == nil; dispatch_async(dispatch_get_main_queue(), ^{ if (self.completeBlock) { self.completeBlock(success,task); } if (self.state == DownloadState_Suspend) { return ; } if ([self.delegate respondsToSelector:@selector(download:didFinishWithSuccess:atPath:)]) { [self.delegate download:self didFinishWithSuccess:success atPath:[self.downloadPath stringByAppendingPathComponent:self.fileName]]; } }); if (self.state == DownloadState_Suspend) { return ; } DownloadState state = success ? DownloadState_Success :DownloadState_Fail; [self cancelAndSetDownloadStateWhenDownloadFinish:state]; } - (void)cancelAndSetDownloadStateWhenDownloadFinish:(DownloadState)state { [self.downloadTask cancel]; [self willChangeValueForKey:@"isFinished"]; [self willChangeValueForKey:@"isExecuting"]; self.state = state; [self didChangeValueForKey:@"isExecuting"]; [self didChangeValueForKey:@"isFinished"]; } - (void)cancelDownloaderAndRemoveFile:(BOOL)remove { [self.downloadTask cancel]; self.delegate = nil; [self removeFile]; [self cancel]; }