咱們用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。如發現任何問題或改進意見,能夠留言,我會盡力完成。