一姜凄、 AFN3.0 下載過程
- 第一步肯定是創(chuàng)建
AFURLSessionManager
,配置一些NSURLSessionConfiguration
趾访,這一步我就不做多的敘述了态秧。
不過因?yàn)槲覀兪菙帱c(diǎn)下載,可以在不同的地方都調(diào)用扼鞋,而且為了調(diào)用方便申鱼,我們直接提供類(+)方法愤诱,但有一些成員變量可以重復(fù)使用,例如
AFURLSessionManager
捐友,綜合考慮淫半,將折現(xiàn)成員變量聲明稱靜態(tài)變量,并在+initialize方法里使用dispatch_once初始化楚殿。
-
創(chuàng)建NSURLSessionDownloadTask:
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler {} - (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler {}
我們可以看到AFN給我們提供了兩個創(chuàng)建NSURLSessionDownloadTask的方法撮慨,第一種方式是通過NSURLRequest創(chuàng)建竿痰,第二種方式是通過NSData創(chuàng)建脆粥。通過參數(shù)我們也能看出來,第一種是為了第一次下載提供的影涉,第二種是為了我們做斷點(diǎn)下載提供的变隔。
-
開始下載
[task resume]; 下載時(shí),會在tmp文件中生成下載的臨時(shí)文件蟹倾, 文件名是CFNetworkDownload_XXXXXX.tmp匣缘,后綴由系統(tǒng)隨機(jī)生成 下載完將臨時(shí)文件移動到目的路徑,路徑為創(chuàng)建DownloadTask時(shí)傳入的destination參數(shù)
-
暫停下載
[task suspend] 暫停后task依然有效鲜棠,通過resume又可以恢復(fù)下載
-
取消下載任務(wù)肌厨,取消下載任務(wù),當(dāng)前的task會失效豁陆,如果想繼續(xù)下載柑爸,需要重新創(chuàng)建下載任務(wù)
[task cancel] [task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {}] 1. 第二種手動取消任務(wù),會返回一個resumeData盒音,這個參數(shù)是我們做斷點(diǎn)下載所必須的表鳍,我們可以直接通過resumeData開啟一個新的下載任務(wù),發(fā)起下載請求 2. 取消任務(wù)時(shí)祥诽,只有滿足以下的各條件譬圣,才會產(chǎn)生resumeData 1. 自從資源開始請求后,資源未更改過 2. 任務(wù)必須是 HTTP 或 HTTPS 的 GET 請求 3. 服務(wù)器在response信息匯總提供了 ETag 或 Last-Modified頭部信息 4. 服務(wù)器支持 byte-range 請求 5. 下載的臨時(shí)文件未被刪除
二雄坪、斷點(diǎn)下載的實(shí)現(xiàn)
-
為什么會出現(xiàn)斷點(diǎn)下載厘熟,我分了三種情況
- 手動取消,也就是我們提供給用戶或我們項(xiàng)目內(nèi)部調(diào)用了1.5中的
cancel
方法 - 網(wǎng)絡(luò)维哈、服務(wù)器異常绳姨,導(dǎo)致下載失敗
- 用戶手動kill掉APP
- 手動取消,也就是我們提供給用戶或我們項(xiàng)目內(nèi)部調(diào)用了1.5中的
-
第二情況的斷點(diǎn)下載實(shí)現(xiàn)(部分網(wǎng)絡(luò)錯誤):
JQDownloadManagerCompletion completeBlock = ^(NSURLResponse *response, NSURL *filePath, NSError *error) { if (!error) { // 任務(wù)完成或暫停下載 [self removeResumeDataWithUrl:url]; [self removeTaskWithUrl:url]; } else { // 部分網(wǎng)絡(luò)出錯,會返回resumeData NSData *resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData]; if (resumeData) [self saveResumeData:resumeData url:url]; } }
部分網(wǎng)絡(luò)錯誤笨农,在error.userInfo中我們是可以獲取到resumeData就缆,可以直接用于斷點(diǎn)下載。
-
第三種情況最復(fù)雜
嘗試在網(wǎng)絡(luò)失敗時(shí)獲取resumeData谒亦,由于時(shí)間太短竭宰,不可行
嘗試通過監(jiān)聽UIApplicationWillTerminateNotification的通知空郊,在app要結(jié)束的時(shí)候獲取resumeData并保存,但現(xiàn)實(shí)還是比較殘酷切揭,由于時(shí)間太短還是無法獲取resumeData狞甚,不可行
-
從resumeData入手,進(jìn)行解析
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>NSURLSessionDownloadURL</key> <string>http://downloadUrl</string> <key>NSURLSessionResumeBytesReceived</key> <integer>1474327</integer> <key>NSURLSessionResumeCurrentRequest</key> <data> ...... </data> <key>NSURLSessionResumeEntityTag</key> <string>"XXXXXXXXXX"</string> <key>NSURLSessionResumeInfoTempFileName</key> <string>CFNetworkDownload_XXXXX.tmp</string> <key>NSURLSessionResumeInfoVersion</key> <integer>2</integer> <key>NSURLSessionResumeOriginalRequest</key> <data> ..... </data> <key>NSURLSessionResumeServerDownloadDate</key> <string>week, dd MM yyyy hh:mm:ss </string> </dict></plist>
- 上面就是解析resumeData之后的數(shù)據(jù)廓旬,其實(shí)就是一個plist文件哼审,里面信息包括了下載URL、已接收字節(jié)數(shù)孕豹、臨時(shí)的下載文件名(文件默認(rèn)存在tmp文件夾中)涩盾、當(dāng)前請求、原始請求励背、下載事件春霍、resumeInfo版本、EntityTag這些數(shù)據(jù)
- iOS8生成的resumeData稍有不同叶眉,沒有
NSURLSessionResumeInfoTempFileName
字段址儒,有NSURLSessionResumeInfoLocalPath
,記錄了完整的tmp文件地址
-
主要需要幾個參數(shù):下載URL衅疙、當(dāng)前請求莲趣、已接收字節(jié)數(shù)、臨時(shí)的下載文件名(文件默認(rèn)存在tmp文件夾中)這四個數(shù)據(jù)
- 下載URL:已知
- 當(dāng)前請求:需要通過已經(jīng)下載的大小和URL創(chuàng)建
- 已接收字節(jié)數(shù):需要通過臨時(shí)文件來獲取大小
- 臨時(shí)文件:存放在本地tmp文件夾下饱溢,但由于文件名CFNetworkDownload_XXXXXX.tmp喧伞,是系統(tǒng)隨機(jī)生成的,我們無法將tmp文件和URL對應(yīng)理朋。
-
獲取tmp文件路徑
- 手動cancel絮识,在繼續(xù)任務(wù),在cancel回調(diào)中可以獲取到resumeData嗽上,里面直接包含所有信息次舌,我們只需要把數(shù)據(jù)中的字節(jié)數(shù)和當(dāng)前請求更換就可以。
- 上一種方法兽愤,有顯而易見的缺點(diǎn)彼念,性能時(shí)間都會浪費(fèi),后來通過調(diào)試浅萧,查看信息逐沙,發(fā)現(xiàn)
NSURLSessionDownloadTask
中有個數(shù)據(jù)downloadFile
存放了一些關(guān)于下載的信息,其中一個信息path
就是存放臨時(shí)文件路徑的洼畅,通過lastPathComponent
就可以直接取到相應(yīng)的臨時(shí)文件名吩案。 - 通過tmp文件名獲取tmp文件路徑,這樣做是因?yàn)楸镜匚募窂綍兊鄞兀圆荒苤苯哟鎡ask中的文件路徑徘郭,需要獲取到文件名靠益,通過tmp的路徑獲取到tmp文件路徑
-
生成resumeData
NSData *resumeData; NSFileManager *fileMgr = [NSFileManager defaultManager]; if ([fileMgr fileExistsAtPath:tempFilePath]) { NSDictionary *tempFileAttr = [[NSFileManager defaultManager] attributesOfItemAtPath:tempFilePath error:nil ]; unsigned long long fileSize = [tempFileAttr[NSFileSize] unsignedLongLongValue]; if (fileSize > 0) { NSMutableDictionary *fakeResumeData = [NSMutableDictionary dictionary]; NSMutableURLRequest *newResumeRequest =[NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; NSString *bytesStr =[NSString stringWithFormat:@"bytes=%ld-",fileSize]; [newResumeRequest addValue:bytesStr forHTTPHeaderField:@"Range"]; NSData *newResumeData =[NSKeyedArchiver archivedDataWithRootObject:newResumeRequest]; [fakeResumeData setObject:newResumeData forKey:@"NSURLSessionResumeCurrentRequest"]; [fakeResumeData setObject:url forKey:@"NSURLSessionDownloadURL"]; [fakeResumeData setObject:@(fileSize) forKey:@"NSURLSessionResumeBytesReceived"]; [fakeResumeData setObject:[tempFilePath lastPathComponent] forKey:@"NSURLSessionResumeInfoTempFileName"]; // iOS9以下 需要路徑 resumeData = [NSPropertyListSerialization dataWithPropertyList:fakeResumeData format:NSPropertyListXMLFormat_v1_0 options:0 error:nil]; } }
大功告成,如果獲取到resumeData残揉,可以直接通過resumeData創(chuàng)建task胧后,進(jìn)行斷點(diǎn)下載了。
下載完成抱环,刪除相應(yīng)的數(shù)據(jù)
三壳快、斷點(diǎn)下載中涉及其他的知識點(diǎn)
-
數(shù)據(jù)緩存:
- 正在下載的URL
- 正在下載的URL的回調(diào)函數(shù)
- 下載的URL對應(yīng)的resumeData
- 下載的URL對應(yīng)的tmp文件名
1 2 因?yàn)榭隙ㄊ茿PP本次啟動之后才存在的數(shù)據(jù),所以直接使用靜態(tài)變量存儲就可以镇草。
3 4 則需要本地化眶痰,使用了JQCache存放在Document目錄下 -
數(shù)據(jù)安全問題:
- 創(chuàng)建一個
dispatch_queue
- 使用讀寫鎖來保障數(shù)據(jù)安全
- dispatch_barrier_async
- dispatch_sync
- 還有互斥鎖和自旋鎖
- 信號量,如果有大量下載同時(shí)訪問陶夜,需要控制并發(fā)數(shù)量凛驮,這里我采用了信號量的方式去控制裆站。
- 創(chuàng)建一個
-
同一URL處理
- 針對同一URL的網(wǎng)絡(luò)請求条辟,如果有下載任務(wù),則不在此下載宏胯,判斷方式通過參考SD的原理羽嫡,考慮如果URL過長,全部對URL進(jìn)行MD5處理肩袍,在進(jìn)行判斷和存儲
- 不進(jìn)行下載杭棵,但需要保存當(dāng)前請求的回調(diào)函數(shù),參考了關(guān)聯(lián)對象的實(shí)現(xiàn)邏輯氛赐,對URL的回調(diào)函數(shù)做處理