咱们用AFNetworking小试牛刀,写一个简单的下载器来演示功能。git
为何AFNetworking可以成为顶级框架?咱们究竟该如何领悟它的精髓所在?这都是很难的问题。安全,高效,流畅,这3个特性缺一不可。假如咱们要封装一个通用的网络框架,提供一个文件下载器是颇有必要的。按照 管理编程原则 ,这个下载管理器应该管理全部的下载任务和依据。github
这是一个简单的下载器,只为了功能演示编程
写一个下载器,必定须要一个对象来描述下载的文件。在这个下载器中,咱们使用MCDownloadReceipt。既然是一个信息的载体,那么从设计角度来讲,咱们应该使用它来存储跟文件相关的内容,不该该让他完成其余更多的事情,好比说开始,暂停等等。缓存
MCDownloadReceipt使用归档进行本地化存储。安全
核心下载使用NSURLSession实现,下边咱们会介绍详情。网络
MCDownloadReceipt的主要功能是用于记录下载信息。即便下载未完成,也能在MCDownloadReceipt的filePath
路径下找个这个文件。session
咱们先来看看暴露出来的头文件信息:app
NSString *url
做为MCDownloadReceipt的惟一标识。NSString *filePath
MCDownloadReceipt的文件索引。NSString *filename
MCDownloadReceipt的文件名,命名规则为:把url进行MD5编码后做为文件名,url中若是有后缀,就拼接后缀。MCDownloadState state
MCDownloadReceipt的状态
long long totalBytesWritten
总共写入的数据的大小totalBytesExpectedToWrite
文件总大小NSOutputStream *stream
用于把数据写入到路径中一般咱们把文件的下载URL进行MD5编码后在拼接上后缀名来做为本地文件的名称。框架
把一个字符串转为MD5字符串:less
static NSString * getMD5String(NSString *str) { if (str == nil) return nil; const char *cstring = str.UTF8String; unsigned char bytes[CC_MD5_DIGEST_LENGTH]; CC_MD5(cstring, (CC_LONG)strlen(cstring), bytes); NSMutableString *md5String = [NSMutableString string]; for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) { [md5String appendFormat:@"%02x", bytes[i]]; } return md5String; }
拼接名称:
- (NSString *)filename { if (_filename == nil) { NSString *pathExtension = self.url.pathExtension; if (pathExtension.length) { _filename = [NSString stringWithFormat:@"%@.%@", getMD5String(self.url), pathExtension]; } else { _filename = getMD5String(self.url); } } return _filename; }
首先咱们要获取一个缓存的路径:
NSString * const MCDownloadCacheFolderName = @"MCDownloadCache"; static NSString * cacheFolder() { NSFileManager *filemgr = [NSFileManager defaultManager]; static NSString *cacheFolder; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ if (!cacheFolder) { NSString *cacheDir = NSHomeDirectory(); cacheFolder = [cacheDir stringByAppendingPathComponent:MCDownloadCacheFolderName]; } NSError *error = nil; if(![filemgr createDirectoryAtPath:cacheFolder withIntermediateDirectories:YES attributes:nil error:&error]) { NSLog(@"Failed to create cache directory at %@", cacheFolder); cacheFolder = nil; } }); return cacheFolder; }
拼接路径和文件名:
- (NSString *)filePath { NSString *path = [cacheFolder() stringByAppendingPathComponent:self.filename]; if (![path isEqualToString:_filePath] ) { if (_filePath && ![[NSFileManager defaultManager] fileExistsAtPath:_filePath]) { NSString *dir = [_filePath stringByDeletingLastPathComponent]; [[NSFileManager defaultManager] createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil]; } _filePath = path; } return _filePath; }
获取某个路径下文件的大小:
static unsigned long long fileSizeForPath(NSString *path) { signed long long fileSize = 0; NSFileManager *fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:path]) { NSError *error = nil; NSDictionary *fileDict = [fileManager attributesOfItemAtPath:path error:&error]; if (!error && fileDict) { fileSize = [fileDict fileSize]; } } return fileSize; }
获取本对象的文件大小:
- (long long)totalBytesWritten { return fileSizeForPath(self.filePath); }
- (NSOutputStream *)stream { if (_stream == nil) { _stream = [NSOutputStream outputStreamToFileAtPath:self.filePath append:YES]; } return _stream; }
- (NSProgress *)progress { if (_progress == nil) { _progress = [[NSProgress alloc] initWithParent:nil userInfo:nil]; } _progress.totalUnitCount = self.totalBytesExpectedToWrite; _progress.completedUnitCount = self.totalBytesWritten; return _progress; }
- (instancetype)initWithURL:(NSString *)url { if (self = [self init]) { self.url = url; self.totalBytesExpectedToWrite = 1; } return self; } #pragma mark - NSCoding - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.url forKey:NSStringFromSelector(@selector(url))]; [aCoder encodeObject:self.filePath forKey:NSStringFromSelector(@selector(filePath))]; [aCoder encodeObject:@(self.state) forKey:NSStringFromSelector(@selector(state))]; [aCoder encodeObject:self.filename forKey:NSStringFromSelector(@selector(filename))]; [aCoder encodeObject:@(self.totalBytesWritten) forKey:NSStringFromSelector(@selector(totalBytesWritten))]; [aCoder encodeObject:@(self.totalBytesExpectedToWrite) forKey:NSStringFromSelector(@selector(totalBytesExpectedToWrite))]; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if (self) { self.url = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(url))]; self.filePath = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(filePath))]; self.state = [[aDecoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(state))] unsignedIntegerValue]; self.filename = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(filename))]; self.totalBytesWritten = [[aDecoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(totalBytesWritten))] unsignedIntegerValue]; self.totalBytesExpectedToWrite = [[aDecoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(totalBytesExpectedToWrite))] unsignedIntegerValue]; } return self; }
是这样的,若是咱们要给某个对象扩展一类的功能或者方法,那么咱们最好使用协议。在AFNetworking的AFURLResponseSerialization和AFURLRequestSerialization就是最好的例子。
@protocol MCDownloadControlDelegate <NSObject> - (void)resumeWithURL:(NSString * _Nonnull)url; - (void)resumeWithDownloadReceipt:(MCDownloadReceipt * _Nonnull)receipt; - (void)suspendWithURL:(NSString * _Nonnull)url; - (void)suspendWithDownloadReceipt:(MCDownloadReceipt * _Nonnull)receipt; - (void)removeWithURL:(NSString * _Nonnull)url; - (void)removeWithDownloadReceipt:(MCDownloadReceipt * _Nonnull)receipt; @end
实现部分:
#pragma mark - MCDownloadControlDelegate - (void)resumeWithURL:(NSString *)url { if (url == nil) return; MCDownloadReceipt *receipt = [self downloadReceiptForURL:url]; [self resumeWithDownloadReceipt:receipt]; } - (void)resumeWithDownloadReceipt:(MCDownloadReceipt *)receipt { if ([self isActiveRequestCountBelowMaximumLimit]) { [self startTask:self.tasks[receipt.url]]; }else { receipt.state = MCDownloadStateWillResume; [self saveReceipts:self.allDownloadReceipts]; [self enqueueTask:self.tasks[receipt.url]]; } } - (void)suspendAll { for (NSURLSessionDownloadTask *task in self.queuedTasks) { [task suspend]; MCDownloadReceipt *receipt = [self downloadReceiptForURL:task.taskDescription]; receipt.state = MCDownloadStateSuspened; } [self saveReceipts:self.allDownloadReceipts]; } -(void)suspendWithURL:(NSString *)url { if (url == nil) return; MCDownloadReceipt *receipt = [self downloadReceiptForURL:url]; [self suspendWithDownloadReceipt:receipt]; } - (void)suspendWithDownloadReceipt:(MCDownloadReceipt *)receipt { [self updateReceiptWithURL:receipt.url state:MCDownloadStateSuspened]; NSURLSessionDataTask *task = self.tasks[receipt.url]; if (task) { [task suspend]; } } - (void)removeWithURL:(NSString *)url { if (url == nil) return; MCDownloadReceipt *receipt = [self downloadReceiptForURL:url]; [self removeWithDownloadReceipt:receipt]; } - (void)removeWithDownloadReceipt:(MCDownloadReceipt *)receipt { NSURLSessionDataTask *task = self.tasks[receipt.url]; if (task) { [task cancel]; } [self.queuedTasks removeObject:task]; [self safelyRemoveTaskWithURLIdentifier:receipt.url]; [self.allDownloadReceipts removeObject:receipt]; [self saveReceipts:self.allDownloadReceipts]; NSFileManager *fileManager = [NSFileManager defaultManager]; [fileManager removeItemAtPath:receipt.filePath error:nil]; }
初始化MCDownloadManager跟AFNetworking中AFImageDownloader的初始化很像,作一些网络配置。参数配置。咱们规定下载任务的建立都放在一个专有的同步队列中完成。咱们还要监听applicationWillTerminate
和 applicationDidReceiveMemoryWarning
这两个通知,并在通知方法中,暂停多有的下载任务。
初始化示例代码:
+ (NSURLSessionConfiguration *)defaultURLSessionConfiguration { NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; configuration.HTTPShouldSetCookies = YES; configuration.HTTPShouldUsePipelining = NO; configuration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy; configuration.allowsCellularAccess = YES; configuration.timeoutIntervalForRequest = 60.0; return configuration; } - (instancetype)init { NSURLSessionConfiguration *defaultConfiguration = [self.class defaultURLSessionConfiguration]; NSOperationQueue *queue = [[NSOperationQueue alloc] init]; queue.maxConcurrentOperationCount = 1; NSURLSession *session = [NSURLSession sessionWithConfiguration:defaultConfiguration delegate:self delegateQueue:queue]; return [self initWithSession:session downloadPrioritization:MCDownloadPrioritizationFIFO maximumActiveDownloads:4 ]; } - (instancetype)initWithSession:(NSURLSession *)session downloadPrioritization:(MCDownloadPrioritization)downloadPrioritization maximumActiveDownloads:(NSInteger)maximumActiveDownloads { if (self = [super init]) { self.session = session; self.downloadPrioritizaton = downloadPrioritization; self.maximumActiveDownloads = maximumActiveDownloads; self.queuedTasks = [[NSMutableArray alloc] init]; self.tasks = [[NSMutableDictionary alloc] init]; self.activeRequestCount = 0; NSString *name = [NSString stringWithFormat:@"com.mc.downloadManager.synchronizationqueue-%@", [[NSUUID UUID] UUIDString]]; self.synchronizationQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; } return self; } + (instancetype)defaultInstance { static MCDownloadManager *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; }
初始化完成后,咱们须要在MCDownloadManager中拿到全部的下载的数据,以及可以保存这些数据到本地。
示例代码:
- (NSMutableArray *)allDownloadReceipts { if (_allDownloadReceipts == nil) { NSArray *receipts = [NSKeyedUnarchiver unarchiveObjectWithFile:LocalReceiptsPath()]; _allDownloadReceipts = receipts != nil ? receipts.mutableCopy : [NSMutableArray array]; } return _allDownloadReceipts; } - (void)saveReceipts:(NSArray <MCDownloadReceipt *>*)receipts { [NSKeyedArchiver archiveRootObject:receipts toFile:LocalReceiptsPath()]; }
下载的核心方法:
- (MCDownloadReceipt *)downloadFileWithURL:(NSString *)url progress:(void (^)(NSProgress * _Nonnull,MCDownloadReceipt *receipt))downloadProgressBlock destination:(NSURL * (^)(NSURL * _Nonnull, NSURLResponse * _Nonnull))destination success:(nullable void (^)(NSURLRequest * _Nullable, NSHTTPURLResponse * _Nullable, NSURL * _Nonnull))success failure:(nullable void (^)(NSURLRequest * _Nullable, NSHTTPURLResponse * _Nullable, NSError * _Nonnull))failure { __block MCDownloadReceipt *receipt = [self downloadReceiptForURL:url]; dispatch_sync(self.synchronizationQueue, ^{ NSString *URLIdentifier = url; if (URLIdentifier == nil) { if (failure) { NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil]; dispatch_async(dispatch_get_main_queue(), ^{ failure(nil, nil, error); }); } return; } receipt.successBlock = success; receipt.failureBlock = failure; receipt.progressBlock = downloadProgressBlock; if (receipt.state == MCDownloadStateCompleted) { dispatch_async(dispatch_get_main_queue(), ^{ if (receipt.successBlock) { receipt.successBlock(nil,nil,[NSURL URLWithString:receipt.url]); } }); return ; } if (receipt.state == MCDownloadStateDownloading) { dispatch_async(dispatch_get_main_queue(), ^{ if (receipt.progressBlock) { receipt.progressBlock(receipt.progress,receipt); } }); return ; } NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:receipt.url]]; NSString *range = [NSString stringWithFormat:@"bytes=%zd-", receipt.totalBytesWritten]; [request setValue:range forHTTPHeaderField:@"Range"]; NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request]; task.taskDescription = receipt.url; self.tasks[receipt.url] = task; [self.queuedTasks addObject:task]; [self resumeWithURL:receipt.url]; }); return receipt; }
--
- (NSURLSessionDownloadTask*)safelyRemoveTaskWithURLIdentifier:(NSString *)URLIdentifier { __block NSURLSessionDownloadTask *task = nil; dispatch_sync(self.synchronizationQueue, ^{ task = [self removeTaskWithURLIdentifier:URLIdentifier]; }); return task; } //This method should only be called from safely within the synchronizationQueue - (NSURLSessionDownloadTask *)removeTaskWithURLIdentifier:(NSString *)URLIdentifier { NSURLSessionDownloadTask *task = self.tasks[URLIdentifier]; [self.tasks removeObjectForKey:URLIdentifier]; return task; } - (void)safelyDecrementActiveTaskCount { dispatch_sync(self.synchronizationQueue, ^{ if (self.activeRequestCount > 0) { self.activeRequestCount -= 1; } }); } - (void)safelyStartNextTaskIfNecessary { dispatch_sync(self.synchronizationQueue, ^{ if ([self isActiveRequestCountBelowMaximumLimit]) { while (self.queuedTasks.count > 0) { NSURLSessionDownloadTask *task = [self dequeueTask]; MCDownloadReceipt *receipt = [self downloadReceiptForURL:task.taskDescription]; if (task.state == NSURLSessionTaskStateSuspended && receipt.state == MCDownloadStateWillResume) { [self startTask:task]; break; } } } }); } - (void)startTask:(NSURLSessionDownloadTask *)task { [task resume]; ++self.activeRequestCount; [self updateReceiptWithURL:task.taskDescription state:MCDownloadStateDownloading]; } - (void)enqueueTask:(NSURLSessionDownloadTask *)task { switch (self.downloadPrioritizaton) { case MCDownloadPrioritizationFIFO: // [self.queuedTasks addObject:task]; break; case MCDownloadPrioritizationLIFO: // [self.queuedTasks insertObject:task atIndex:0]; break; } } - (NSURLSessionDownloadTask *)dequeueTask { NSURLSessionDownloadTask *task = nil; task = [self.queuedTasks firstObject]; [self.queuedTasks removeObject:task]; return task; } - (BOOL)isActiveRequestCountBelowMaximumLimit { return self.activeRequestCount < self.maximumActiveDownloads; }
根据URL获取receipt对象:
- (MCDownloadReceipt *)downloadReceiptForURL:(NSString *)url { if (url == nil) return nil; for (MCDownloadReceipt *receipt in self.allDownloadReceipts) { if ([receipt.url isEqualToString:url]) { return receipt; } } MCDownloadReceipt *receipt = [[MCDownloadReceipt alloc] initWithURL:url]; receipt.state = MCDownloadStateNone; receipt.totalBytesExpectedToWrite = 1; [self.allDownloadReceipts addObject:receipt]; [self saveReceipts:self.allDownloadReceipts]; return receipt; }
NSURLSessionDataDelegate:
在接到响应后,保存totalBytesExpectedToWrite和state
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler { MCDownloadReceipt *receipt = [self downloadReceiptForURL:dataTask.taskDescription]; receipt.totalBytesExpectedToWrite = dataTask.countOfBytesExpectedToReceive; receipt.state = MCDownloadStateDownloading; @synchronized (self) { [self saveReceipts:self.allDownloadReceipts]; } [receipt.stream open]; completionHandler(NSURLSessionResponseAllow); }
在接收到数据后,写入文件而且调用progressBlock
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { MCDownloadReceipt *receipt = [self downloadReceiptForURL:dataTask.taskDescription]; [receipt.stream write:data.bytes maxLength:data.length]; receipt.progress.totalUnitCount = receipt.totalBytesExpectedToWrite; receipt.progress.completedUnitCount = receipt.totalBytesWritten; dispatch_async(dispatch_get_main_queue(), ^{ if (receipt.progressBlock) { receipt.progressBlock(receipt.progress,receipt); } }); }
下载完成后
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { MCDownloadReceipt *receipt = [self downloadReceiptForURL:task.taskDescription]; [receipt.stream close]; receipt.stream = nil; if (error) { receipt.state = MCDownloadStateFailed; dispatch_async(dispatch_get_main_queue(), ^{ if (receipt.failureBlock) { receipt.failureBlock(task.originalRequest,(NSHTTPURLResponse *)task.response,error); } }); }else { receipt.state = MCDownloadStateCompleted; dispatch_async(dispatch_get_main_queue(), ^{ if (receipt.successBlock) { receipt.successBlock(task.originalRequest,(NSHTTPURLResponse *)task.response,task.originalRequest.URL); } }); } @synchronized (self) { [self saveReceipts:self.allDownloadReceipts]; } [self safelyDecrementActiveTaskCount]; [self safelyStartNextTaskIfNecessary]; }
这个下载器就介绍到这里了,能够在https://github.com/agelessman/MCDownloadManager.git下载demo。如发现任何问题或改进意见,能够留言,我会尽力完成。