一播掷、整體框架及思路
主要涉及兩個(gè)類,
1撼班、DownloadManager歧匈,文件下載管理器是一個(gè)單例,負(fù)責(zé)調(diào)度和管理具體的下載操作权烧。它主要有兩個(gè)屬性眯亦,下載操作緩存(字典類型),以u(píng)rl為key般码,防止相同的下載操作多次執(zhí)行。下載隊(duì)列(NSOperationQueue)乱顾,用來(lái)控制并發(fā)數(shù)板祝,防止隊(duì)列中同時(shí)有過(guò)多的下載操作同時(shí)執(zhí)行。
2走净、DownloadOperation,文件下載操作券时,對(duì)應(yīng)具體的下載操作,繼承于NSOperation伏伯。相關(guān)注意點(diǎn):1橘洞、執(zhí)行下載請(qǐng)求前,先同步發(fā)送一個(gè)HEAD請(qǐng)求说搅,獲取目標(biāo)文件的大小炸枣,經(jīng)過(guò)和本地文件比對(duì)后確定斷點(diǎn)下載的斷點(diǎn)。 2、通過(guò)設(shè)置range請(qǐng)求頭告訴服務(wù)器斷點(diǎn)适肠,這是斷點(diǎn)下載的核心霍衫。3、在循環(huán)接收服務(wù)器返回的文件數(shù)據(jù)過(guò)程中侯养,如果用全局屬性去拼接敦跌,會(huì)造成內(nèi)存浪費(fèi)」淇可以用NSOutputStream解決內(nèi)存峰值問(wèn)題柠傍,每接收一點(diǎn),就寫到本地文件中辩稽。
外界調(diào)動(dòng)示例:
//要下載的文件的URL
NSURL *url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/2a/25677/QQ_V4.1.1.1456905733.dmg"];
//通過(guò)下載管理器單例執(zhí)行下載
[[downloadManager sharedManager] downloadWith:url pregressBlock:^(CGFloat progress) {
//進(jìn)度回調(diào)携兵,返回下載進(jìn)度
dispatch_async(dispatch_get_main_queue(), ^{
//回到主線程刷新進(jìn)度提醒
self.progress.progress = progress;
});
} complete:^(NSString *path, NSError *error) {
//完成回調(diào),返回文件路徑或者失敗信息
NSLog(@"下載完成%@",path);
}];
二
實(shí)現(xiàn)代碼:
DownloadManager.m
@interface downloadManager()
/**
* 下載操作緩存,以u(píng)rl為key搂誉,防止多次下載
*/
@property (nonatomic,strong)NSMutableDictionary *downloadCache;
/**
* 下載隊(duì)列,可以控制并發(fā)數(shù)
*/
@property (nonatomic,strong)NSOperationQueue *downloadQueue;
@end
@implementation downloadManager
/**
* 下載管理器單例
*
* @return 返回唯一的實(shí)例
*/
+ (instancetype)sharedManager{
static dispatch_once_t onceToken;
static downloadManager *manager;
dispatch_once(&onceToken, ^{
manager = [[self alloc]init];
});
return manager;
}
/**
* 下載操作
*
* @param url 要下載的url
* @param progressBlock 進(jìn)度回調(diào)
* @param complete 完成回調(diào)
*/
- (void)downloadWith:(NSURL *)url pregressBlock:(void (^)(CGFloat progress))progressBlock complete:(void(^)(NSString *path,NSError *error))complete{
//在開始下載之前徐紧,判斷是否正在下載
if ([self.downloadCache objectForKey:url]) {
NSLog(@"正在下載");
return;
}
//開始下載文件
downloadOperation *download = [downloadOperation downloadWith:url progressBlock:progressBlock complete:^(NSString *path, NSError *error) {
//下載完成
//移除下載操作緩存
[self.downloadCache removeObjectForKey:url];
//調(diào)用控制器傳過(guò)來(lái)的完成回調(diào)
complete(path,error);
}];
//把下載操作保存起來(lái)
[self.downloadCache setObject:download forKey:url];
//把操作發(fā)到隊(duì)列中
[self.downloadQueue addOperation:download];
}
- (void)cancelDownload:(NSURL *)url{
//從字典中取出下載操作
downloadOperation *down = [self.downloadCache objectForKey:url];
[down cancleDown];
//從字典中移除
[self.downloadCache removeObjectForKey:url];
}
#pragma mark -數(shù)據(jù)懶加載
- (NSMutableDictionary *)downloadCache{
if (_downloadCache == nil) {
_downloadCache = [NSMutableDictionary dictionary];
}
return _downloadCache;
}
- (NSOperationQueue *)downloadQueue{
if (_downloadQueue == nil) {
_downloadQueue = [[NSOperationQueue alloc]init];
//設(shè)置最大并發(fā)數(shù)
_downloadQueue.maxConcurrentOperationCount = 3;
}
return _downloadQueue;
}
@end
DownloadOperation.m
@interface downloadOperation()
/**
* 文件總大小
*/
@property (nonatomic,assign)long long expectLength;
/**
* 已經(jīng)下載的大小
*/
@property (nonatomic,assign)long long downloadLength;
/**
* 文件輸出流
*/
@property (nonatomic,strong)NSOutputStream *outPut;
/**
* 下載連接
*/
@property (nonatomic,strong)NSURLConnection *connection;
/**
* 進(jìn)度回調(diào)block
*/
@property (nonatomic,copy)void (^progressBlock)(CGFloat);
/**
* 完成回調(diào)block
*/
@property (nonatomic,copy)void (^complete)(NSString *,NSError *);
/**
* 文件保存的路徑
*/
@property (nonatomic,copy)NSString *targetPath;
/**
* <#Description#>
*/
@property (nonatomic,strong)NSURL *url;
@end
@implementation downloadOperation
+ (instancetype)downloadWith:(NSURL *)url progressBlock:(void (^)(CGFloat progress))progressBlock complete:(void (^)(NSString *path,NSError *error))complete{
downloadOperation *down = [[self alloc]init];
//把block保存起來(lái)
down.progressBlock = progressBlock;
down.complete = complete;
//拼接文件保存的路徑
down.targetPath = [url.path appendCaches];
down.url = url;
//調(diào)用下載方法
//[down download:url];
return down;
}
- (void)main{
@autoreleasepool {
[self download];
}
}
- (void)download{
NSURL *url = self.url;
//斷點(diǎn)下載,如果完全相同炭懊,不下載并级。否則,從fileSize開始下載
long long fileSize = [self checkServerFileSize:url];
if (fileSize == self.expectLength) {
return ;
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//斷點(diǎn)續(xù)傳必須設(shè)置range請(qǐng)求頭
NSString *range = [NSString stringWithFormat:@"bytes=%lld-",fileSize];
//下載進(jìn)度就是本地文件的大小
self.downloadLength = fileSize;
//設(shè)置請(qǐng)求頭
[request setValue:range forHTTPHeaderField:@"range"];
//開始下載,通過(guò)遵守代理協(xié)議侮腹,回調(diào)下載過(guò)程
[NSURLConnection connectionWithRequest:request delegate:self];
//如果子線程在執(zhí)行完當(dāng)前代碼后需要一直保持的嘲碧,需要把runloop跑起來(lái)
[[NSRunLoop currentRunLoop] run];
}
//檢查服務(wù)器上文件的大小
- (long long)checkServerFileSize:(NSURL *)url{
//要下載文件的url
//NSURL *url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/2a/25677/QQ_V4.1.1.1456905733.dmg"];
//創(chuàng)建獲取文件大小的請(qǐng)求
NSMutableURLRequest *headRequest = [NSMutableURLRequest requestWithURL:url];
//請(qǐng)求方法
headRequest.HTTPMethod = @"HEAD";
//創(chuàng)建一個(gè)響應(yīng)頭
NSURLResponse *headResponse;
[NSURLConnection sendSynchronousRequest:headRequest returningResponse:&headResponse error:nil];
long long serverSize = headResponse.expectedContentLength;
//記錄服務(wù)器文件的大小,用于計(jì)算進(jìn)度
self.expectLength = serverSize;
//拼接文件路徑
NSString *path = [headResponse.suggestedFilename appendCaches];
//判斷服務(wù)器文件大小跟本地文件大小的關(guān)系
NSFileManager *manager = [NSFileManager defaultManager];
if (![manager fileExistsAtPath:path]) {
NSLog(@"不存在父阻,從頭開始下載");
return 0;
}
//獲取文件的屬性
NSDictionary *dict = [manager attributesOfItemAtPath:path error:nil];
//獲取本地文件的大小
long long fileSize = dict.fileSize;
if (fileSize > serverSize) {
//文件出錯(cuò)愈涩,刪除文件
[manager removeItemAtPath:path error:nil];
NSLog(@"從頭開始");
return 0;
}else if(fileSize < serverSize){
NSLog(@"從本地文件大小開始下載");
return fileSize;
}else{
NSLog(@"已經(jīng)下載完畢");
return fileSize;
}
}
#pragma mark -下載回調(diào)
//獲得相應(yīng),響應(yīng)頭中包含了文件總大小的信息
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// //設(shè)置文件總大小
// self.expectLength = response.expectedContentLength;
NSString *path = self.targetPath;
//不需要手動(dòng)去創(chuàng)建,如果文件不存在加矛,自動(dòng)創(chuàng)建
self.outPut = [NSOutputStream outputStreamToFileAtPath:path append:YES];
//在入文件之前履婉,先打開文件
[self.outPut open];
}
//接收到數(shù)據(jù)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
//分多次接收數(shù)據(jù),加起來(lái)就是總數(shù)據(jù)
self.downloadLength += data.length;
//計(jì)算當(dāng)前下載進(jìn)度
CGFloat progress = self.downloadLength *1.0 / self.expectLength;
//NSLog(@"當(dāng)前進(jìn)度%f",progress);
//進(jìn)度回調(diào)調(diào)用的頻率非常高斟览,我們可以在子線程回調(diào)毁腿。用戶是否要刷新UI,由自己決定
if (self.progressBlock) {
self.progressBlock(progress);
}
//保存每次接收到的數(shù)據(jù)
//#warning 每次的數(shù)據(jù)都保存在內(nèi)存中苛茂,消耗大量?jī)?nèi)存
// [self.data appendData:data];
//解決方法:使用文件句柄
[self.outPut write:data.bytes maxLength:data.length];
}
//下載完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
//關(guān)閉文件
[self.outPut close];
//下載完成后已烤,自動(dòng)回到主線程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.complete(self.targetPath,nil);
});
}
//取消下載
- (void)cancleDown{
[self.connection cancel];
}
@end
其他
NSString+Path.m
@implementation NSString (Path)
- (NSString *)appendCaches {
// 獲取緩存目錄
NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 獲取路徑的最后一部份(文件名)
NSString *filename = [self lastPathComponent];
return [cache stringByAppendingPathComponent:filename];
}
- (NSString *)appendDocuments {
// 獲取緩存目錄
NSString *document = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
// 獲取路徑的最后一部份(文件名)
NSString *filename = [self lastPathComponent];
return [document stringByAppendingPathComponent:filename];
}
- (NSString *)appendTmp {
// 獲取緩存目錄
NSString *temp = NSTemporaryDirectory();
// 獲取路徑的最后一部份(文件名)
NSString *filename = [self lastPathComponent];
return [temp stringByAppendingPathComponent:filename];
}