1.前言
iOS開(kāi)發(fā)中斷點(diǎn)下載功能很常見(jiàn),網(wǎng)上也有很多框架,本文選擇了原生的
NSURLSession
和NSFileHandle
,實(shí)現(xiàn)了多任務(wù)茶鉴、大文件的斷點(diǎn)下載,保證了較低的內(nèi)存占用景用。
2.預(yù)覽
3.設(shè)計(jì)思路
-
斷點(diǎn)下載方案:
NSMutableData
如果文件很大會(huì)出現(xiàn)內(nèi)存警告
NSURLConnection
iOS9之后棄用涵叮,沒(méi)有暫停的方法
NSURLSession
推薦
將下載記錄保存到plist,重啟應(yīng)用時(shí)就能拿到下載記錄伞插,從而繼續(xù)下載割粮。
-
文件存儲(chǔ)方案:
NSMutableData
如果文件很大會(huì)出現(xiàn)內(nèi)存警告
NSOutputStream
推薦
NSFileHandle
推薦
實(shí)際使用發(fā)現(xiàn)NSOutputSteam
和NSFileHandle
差別不大,但要注意不再寫(xiě)入時(shí)需要調(diào)用close
方法蜂怎。
內(nèi)存占用:
-
多任務(wù)
創(chuàng)建下載任務(wù)時(shí)我們采用了NSURLSessionDataTask
穆刻,它是NSURLSessionTask
的子類(lèi),其擁有只讀屬性taskIdentifier杠步,若要將其作為任務(wù)的唯一標(biāo)識(shí)符需要利用KVC氢伟。這里我們沒(méi)有這么做,而是創(chuàng)建了NSURLSession
的分類(lèi)幽歼,為其添加了taskKey屬性作為任務(wù)的為一標(biāo)識(shí)符朵锣。創(chuàng)建任務(wù)時(shí)將傳入的URL的MD5值作為key,并將其作查詢(xún)下載任務(wù)甸私、本地文件緩存诚些、下載記錄的為一索引。
4.代碼
-
文件結(jié)構(gòu)
創(chuàng)建下載管理者類(lèi)MYDownloadManager
,考慮到會(huì)在多個(gè)地方調(diào)用下載功能诬烹,所以將其設(shè)計(jì)為單例模式砸烦。管理者擁有多個(gè)下載任務(wù),每個(gè)任務(wù)有其唯一的key绞吁,所以創(chuàng)建一個(gè)保存著多個(gè)任務(wù)的字典幢痘。
@interface MYDownloadManager ()<NSURLSessionDataDelegate>
@property (nonatomic, strong) NSMutableDictionary *downloadDict; // {Key : md5, Value : <MYDonwload *>}
@end
創(chuàng)建下載類(lèi)
@interface MYDownload : NSObject
@property (nonatomic, copy) NSString *url; // 下載地址
@property (nonatomic, assign) long long downloadedLength; // 已下載大小
@property (nonatomic, assign) long long totalLength; // 總大小
@property (nonatomic, strong) NSURLSessionDataTask *task; // 任務(wù)
@property (nonatomic, strong) NSFileHandle *fileHandle; // 文件句柄
@property (nonatomic, copy) ProgressBlock progressBlock; // 下載進(jìn)度回調(diào)
@property (nonatomic, copy) StateBlock stateBlock; // 下載狀態(tài)回調(diào)
@end
下載記錄Download.plist
結(jié)構(gòu)
-
開(kāi)始、恢復(fù)下載
我們要從上次結(jié)束的位置開(kāi)始下載家破,所以需要設(shè)置請(qǐng)求頭颜说,下載指定范圍的文件,設(shè)置規(guī)則如下:
表示頭500個(gè)字節(jié):Range: bytes=0-499
表示第二個(gè)500字節(jié):Range: bytes=500-999
表示最后500個(gè)字節(jié):Range: bytes=-500
表示500字節(jié)以后的范圍:Range: bytes=500-
同時(shí)指定幾個(gè)范圍:Range: bytes=100-199,400-500
- (void)downloadWithUrl:(NSString *)url resume:(BOOL)resume progress:(ProgressBlock)progressBlock state:(StateBlock)stateBlock {
if (!url.length) {
return;
}
// 將url的md5值作為key
NSString *key = [url md5];
long long totalLength = [self getTotalLengthWithKey:key];
long long downloadedLength = [self getDownloadedLengthWithKey:key];
// 任務(wù)已完成
if (totalLength == downloadedLength && totalLength > 0) {
if (progressBlock) {
progressBlock(1.0, downloadedLength, totalLength);
}
if (stateBlock) {
stateBlock(MYDownloadStateComplete);
}
}
// 查詢(xún)?nèi)蝿?wù)是否存在
MYDownload *download = [self.downloadDict valueForKey:key];
if (download) {
// 取出下載任務(wù)
if (resume) {
[download.task resume];
} else {
[download.task suspend];
if (download.stateBlock) {
download.stateBlock(MYDownloadStateSuspend);
}
}
} else {
// 創(chuàng)建下載任務(wù)
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
NSString *rangeValue = [NSString stringWithFormat:@"bytes=%lld-", downloadedLength];
[request setValue:rangeValue forHTTPHeaderField:@"Range"];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
task.taskKey = key;
task.url = url;
if (resume) {
[task resume];
}
// 創(chuàng)建并保存下載對(duì)象
download = [MYDownload new];
download.url = url;
download.task = task;
download.progressBlock = progressBlock;
download.stateBlock = stateBlock;
[self.downloadDict setValue:download forKey:key];
}
}
-
實(shí)現(xiàn)代理方法
NSURLSessionDataDelegate
根據(jù)dataTask的taskKey可以得到當(dāng)前下載對(duì)象汰聋,從服務(wù)器返回的response我們可以得到任務(wù)的總大小门粪。開(kāi)辟緩存文件,創(chuàng)建文件句柄準(zhǔn)備寫(xiě)入文件烹困,保存下載記錄到plist玄妈。
// 收到服務(wù)器相應(yīng)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(nonnull NSURLResponse *)response completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler {
NSString *key = dataTask.taskKey;
NSString *url = dataTask.url;
NSString *filePath = [self getDownloadedPathWithFileName:response.suggestedFilename];
MYDownload *download = [self.downloadDict valueForKey:key];
// 計(jì)算總大小并保存到plist
long long expectedLength = response.expectedContentLength;
long long downloadedLength = [self getDownloadedLengthWithKey:key];
long long totalLength = expectedLength + downloadedLength;
if (totalLength == 0) {
if (download.progressBlock) {
download.progressBlock(0.f, downloadedLength, totalLength);
}
if (download.stateBlock) {
download.stateBlock(MYDownloadStateError);
}
return;
}
NSDictionary *dict = @{@"TotalLength" : @(totalLength),
@"Url" : url,
@"FileName" : response.suggestedFilename
};
[self setPlistValue:dict forKey:key];
// 創(chuàng)建NSFileHandle
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:filePath]) {
[fileManager createFileAtPath:filePath contents:nil attributes:nil];
}
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
// 設(shè)置下載對(duì)象
download.totalLength = totalLength;
download.downloadedLength = downloadedLength;
download.fileHandle = fileHandle;
completionHandler(NSURLSessionResponseAllow);
}
開(kāi)始接收數(shù)據(jù),利用NSFileHandle
將文件寫(xiě)入沙盒韭邓,不會(huì)導(dǎo)致內(nèi)存占用過(guò)高措近。
// 收到數(shù)據(jù)(多次調(diào)用)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSString *key = dataTask.taskKey;
// 寫(xiě)數(shù)據(jù)
MYDownload *download = [self.downloadDict valueForKey:key];
if (download) {
[download.fileHandle seekToEndOfFile];
[download.fileHandle writeData:data];
download.downloadedLength += data.length;
CGFloat progress = (CGFloat) download.downloadedLength / download.totalLength;
if (download.progressBlock) {
download.progressBlock(progress, download.downloadedLength, download.totalLength);
}
if (download.stateBlock) {
download.stateBlock(MYDownloadStateDownloading);
}
}
}
任務(wù)完成、中止時(shí)關(guān)閉NSFileHandle
女淑,移除下載對(duì)象
// 任務(wù)完成、中止
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSString *key = task.taskKey;
// 關(guān)閉寫(xiě)數(shù)據(jù)流
MYDownload *download = [self.downloadDict valueForKey:key];
[download.fileHandle closeFile];
download.fileHandle = nil;
if (download.stateBlock) {
if (error) {
download.stateBlock(MYDownloadStateError);
} else {
download.stateBlock(MYDownloadStateComplete);
}
}
[self.downloadDict removeObjectForKey:key];
}
5.其他
- Git地址: MultitaskSuspendDownload