iOS開發中經常會用到檔案的下載與上傳功能,今天我們們來分享一下檔案下載的思路。檔案上傳下篇再說。
檔案下載分為小檔案下載與大檔案下載
小檔案下載
小檔案可以是一張圖片,或者一個檔案,這裡指在現行的網路狀況下基本上不需要等待很久就能下載好的檔案。這裡以picjumbo裡的一張圖片為例子。
NSData方式
其實我們經常用的 [NSData dataWithContentsOfURL]
就是一種檔案下載方式,猜測這裡面應該是傳送了Get請求。
1 2 |
NSURL *url = [NSURL URLWithString:@"https://picjumbo.imgix.net/HNCK8461.jpg?q=40&w=1650♯=30"]; NSData *data = [NSData dataWithContentsOfURL:url]; |
當然下載程式碼應該放到子執行緒執行
NSURLConnection方式下載
1 2 3 4 5 |
NSURL* url = [NSURL URLWithString:@"https://picjumbo.imgix.net/HNCK8461.jpg?q=40&w=1650♯=30"]; [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { self.imageView.image = [UIImage imageWithData:data]; }]; |
就是傳送一個非同步的Get請求,回撥的data就是我們下載到的圖片。這些都很簡單,今天主要說的是大檔案的下載。
大檔案下載
NSURLConnection下載
通過上面的兩個方法去下載大檔案是不合理的,因為這兩個方法都是一次性返回整個下載到的檔案,返回的data在記憶體中,如果下載一個幾百兆的東西,記憶體肯定會爆的。其實NSURLConnection還提供了另外一種傳送請求的方式
1 2 |
// 傳送請求去下載 (建立完conn物件後,會自動發起一個非同步請求) [NSURLConnection connectionWithRequest:request delegate:self]; |
這裡用到了代理,那肯定要遵守協議了.遵守 NSURLConnectionDataDelegate
協議.
進去看看有幾個代理方法,其實我們能用到的也就三個。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
/** * 請求失敗時呼叫(請求超時、網路異常) * * @param error 錯誤原因 */ - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { } /** * 1.接收到伺服器的響應就會呼叫 * * @param response 響應 */ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { } /** * 2.當接收到伺服器返回的實體資料時呼叫(具體內容,這個方法可能會被呼叫多次) * * @param data 這次返回的資料 */ - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { } /** * 3.載入完畢後呼叫(伺服器的資料已經完全返回後) */ - (void)connectionDidFinishLoading:(NSURLConnection *)connection { } |
通過執行下載操作,分別log上面三個方法,會發現didReceiveData這個方法會被頻繁的呼叫,每次都會傳回來一部分data,下面是官方api對這個方法的說明
is called with a single immutable NSData object to the delegate,representing the next portion of the data loaded from the connection. This is the only guaranteed for the delegate to receive the data from the resource load.
由此我們可以知道,這種下載方式是通過這個代理方法每次傳回來一部分檔案,最終我們把每次傳回來的資料合併成一個我們需要的檔案。
這時候我們通常想到的方法是定義一個全域性的NSMutableData,接受到響應的時候初始化這個MutableData,在didReceiveData方法裡面去拼接
[self.totalData appendData:data];
最後在完成下載的方法裡面吧整個MutableData寫入沙盒。
程式碼如下:
1 2 3 4 5 6 7 8 |
@property (weak, nonatomic) IBOutlet UIProgressView *myPregress; @property (nonatomic,strong) NSMutableData* fileData; /** * 檔案的總長度 */ @property (nonatomic, assign) long long totalLength; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/** * 1.接收到伺服器的響應就會呼叫 * * @param response 響應 */ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { self.fileData = [NSMutableData data]; // 獲取要下載的檔案的大小 self.totalLength = response.expectedContentLength; } /** * 2.當接收到伺服器返回的實體資料時呼叫(具體內容,這個方法可能會被呼叫多次) * * @param data 這次返回的資料 */ - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.fileData appendData:data]; self.myPregress.progress = (double)self.fileData.length / self.totalLength; } /** * 3.載入完畢後呼叫(伺服器的資料已經完全返回後) */ - (void)connectionDidFinishLoading:(NSURLConnection *)connection { // 拼接檔案路徑 NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *file = [cache stringByAppendingPathComponent:response.suggestedFilename]; // 寫到沙盒中 [self.fileData writeToFile:file atomically:YES]; } |
我這裡下載的是javajdk。(度孃的地址)
注意:通常大檔案下載是需要給使用者展示下載進度的。
這個數值是 已經下載的資料大小/要下載的檔案總大小
已經下載的資料我們可以記錄,要下載的檔案總大小在伺服器返回的響應頭裡面可以拿到,在接受到響應的方法裡執行
1 2 3 4 5 |
NSHTTPURLResponse *res = (NSHTTPURLResponse*)response; NSDictionary *headerDic = res.allHeaderFields; NSLog(@"%@",headerDic); self.fileLength = [[headerDic objectForKey:@"Content-Length"] intValue]; |
不得不說蘋果太為開發者考慮了,我們不必這麼麻煩的去獲取檔案總大小了,
response.expectedContentLength 這句程式碼就搞定了。
response.suggestedFilename 這句代表獲取下載的檔名
題外話扯的有點多,言歸正傳,這樣我們確實可以下載檔案,最後拿到的檔案也能正常執行
但是有個致命的問題,記憶體!用來接受檔案的NSMutableData一直都在記憶體中,會隨著檔案的下載一直變大,
所有這種處理方式絕對是不合理的。
合理的方式在我們獲取一部分data的時候就寫入沙盒中,然後釋放記憶體中的data。
這裡要用到NSFilehandle這個類,這個類可以實現對檔案的讀取、寫入、更新。
下面總結了一些常用的NSFileHandle的方法,在這個表中,fh是一個NSFileHandle物件,data是一個NSData物件,path是一個NSString 物件,offset是易額Unsigned long long變數。
具體關於NSFileHandle的用法各位自行搜尋。
在接受到響應的時候就在沙盒中建立一個空的檔案,然後每次接收到資料的時候就拼接到這個檔案的最後面,通過 - (unsigned long long)seekToEndOfFile;
這個方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { // 檔案路徑 NSString* ceches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString* filepath = [ceches stringByAppendingPathComponent:response.suggestedFilename]; // 建立一個空的檔案到沙盒中 NSFileManager* mgr = [NSFileManager defaultManager]; [mgr createFileAtPath:filepath contents:nil attributes:nil]; // 建立一個用來寫資料的檔案控制程式碼物件 self.writeHandle = [NSFileHandle fileHandleForWritingAtPath:filepath]; // 獲得檔案的總大小 self.totalLength = response.expectedContentLength; } /** * 2.當接收到伺服器返回的實體資料時呼叫(具體內容,這個方法可能會被呼叫多次) * * @param data 這次返回的資料 */ - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { // 移動到檔案的最後面 [self.writeHandle seekToEndOfFile]; // 將資料寫入沙盒 [self.writeHandle writeData:data]; // 累計寫入檔案的長度 self.currentLength += data.length; // 下載進度 self.myPregress.progress = (double)self.currentLength / self.totalLength; } /** * 3.載入完畢後呼叫(伺服器的資料已經完全返回後) */ - (void)connectionDidFinishLoading:(NSURLConnection *)connection { self.currentLength = 0; self.totalLength = 0; // 關閉檔案 [self.writeHandle closeFile]; self.writeHandle = nil; } |
這樣在下載過程中記憶體就會一直很穩定了,並且下載的檔案也是沒問題的。
斷點下載
暫停/繼續下載也是現在下載中必備的功能了,如果沒有暫停功能,使用者體驗相比會很差,而且如果突然網路不好中斷了,沒有實現斷點下載的話只有重新下了。。。
下面讓我們來加入斷點下載功能吧。
NSURLConnection 只提供了一個cancel方法,這並不是暫停,而是取消下載任務。如果要實現斷點下載必須要了解HTTP協議中請求頭的Range。
不難看出,通過設定請求頭的Range我們可以指定下載的位置、大小。
那麼我們這樣設定 bytes=500- 從500位元組以後的所有位元組
,
只需要在didReceiveData中記錄已經寫入沙盒中檔案的大小(self.currentLength),
把這個大小設定到請求頭中,因為第一次下載肯定是沒有執行過didReceive方法,self.currentLength也就為0,也就是從頭開始下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
上程式碼: #pragma mark --按鈕點選事件 - (IBAction)btnClicked:(UIButton *)sender { // 狀態取反 sender.selected = !sender.isSelected; // 斷點續傳 // 斷點下載 if (sender.selected) { // 繼續(開始)下載 // 1.URL NSURL *url = [NSURL URLWithString:@"http://localhost:8080//term_app/hdgg.zip"]; // 2.請求 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; // 設定請求頭 NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength]; [request setValue:range forHTTPHeaderField:@"Range"]; // 3.下載 self.connection = [NSURLConnection connectionWithRequest:request delegate:self]; } else { // 暫停 [self.connection cancel]; self.connection = nil; } } |
在下載過程中,為了提高效率,充分利用cpu效能,通常會執行多執行緒下載,程式碼就不貼了,分析一下思路:
下載開始,建立一個和要下載的檔案大小相同的檔案(如果要下載的檔案為100M,那麼就在沙盒中建立一個100M的檔案,然後計算每一段的下載量,開啟多條執行緒下載各段的資料,分別寫入對應的檔案部分)。
NSURLSession下載方式
上面這種下載檔案的方式確實比較複雜,要自己去控制記憶體寫入相應的位置,不過在蘋果在iOS7推出了一個新的類 NSURLSession
,它具備了NSURLConnection所具備的方法,同時也比它更強大。蘋果推出它的目的大有取代NSURLConnection的趨勢或者目的。
NSURLSession
也可以傳送Get/Post請求,實現檔案的下載和上傳。
在NSURLSesiion中,任何請求都可以被看做是一個任務。其中有三種任務型別
// NSURLSessionDataTask : 普通的GET\POST請求
// NSURLSessionDownloadTask : 檔案下載
// NSURLSessionUploadTask : 檔案上傳(很少用,一般伺服器不支援)
NSURLSession 簡單使用
NSURLSession傳送請求非常簡單,與connection不同的是,任務建立後不會自動傳送請求,需要手動開始執行任務。
1 2 3 4 5 6 7 8 9 10 11 |
// 1.得到session物件 NSURLSession* session = [NSURLSession sharedSession]; NSURL* url = [NSURL URLWithString:@""]; // 2.建立一個task,任務 NSURLSessionDataTask* dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // data 為返回資料 }]; // 3.開始任務 [dataTask resume]; |
1 2 |
// 傳送post請求 自定義請求頭 [session dataTaskWithRequest:<#(NSURLRequest *)#> completionHandler:<#^(NSData *data, NSURLResponse *response, NSError *error)completionHandler#>] |
NSURLSession 下載
使用NSURLSession就非常簡單了,不需要去考慮什麼邊下載邊寫入沙盒的問題,蘋果都幫我們做好了。程式碼如下
1 2 3 4 5 6 7 8 9 10 11 |
NSURL* url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/9d/25765/sogou_mac_32c_V3.2.0.1437101586.dmg"]; // 得到session物件 NSURLSession* session = [NSURLSession sharedSession]; // 建立任務 NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { }]; // 開始任務 [downloadTask resume]; |
是不是跟NSURLConnection很像,但仔細看會發現回撥的方法裡面並沒用NSData傳回來,多了一個location,顧名思義,location就是下載好的檔案寫入沙盒的地址,列印一下發現下載好的檔案被自動寫入的temp資料夾下面了。
location:file:///Users/yeaodong/Library/Developer/CoreSimulator/Devices/E52B4B95-53E1-46A2-9881-8C969958FBC0/data/Containers/Data/Application/BFB9F0CA-0F50-4682-BBBD-B71B54C39EBE/tmp/CFNetworkDownload_YNnuIS.tmp
不過在下載完成之後會自動刪除temp中的檔案,所有我們需要做的只是在回撥中把檔案移動(或者複製,反正之後會自動刪除)到caches中。
1 2 3 4 5 6 7 8 9 10 |
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; // response.suggestedFilename : 建議使用的檔名,一般跟伺服器端的檔名一致 NSString *file = [caches stringByAppendingPathComponent:response.suggestedFilename]; // 將臨時檔案剪下或者複製Caches資料夾 NSFileManager *mgr = [NSFileManager defaultManager]; // AtPath : 剪下前的檔案路徑 // ToPath : 剪下後的檔案路徑 [mgr moveItemAtPath:location.path toPath:file error:nil]; |
不過通過這種方式下載有個缺點就是無法監聽下載進度,要監聽下載進度,蘋果通常的作法是通過delegate,這裡也一樣。而且NSURLSession的建立方式也有所不同。
首先遵守協議 <NSURLSessionDownloadDelegate>
注意不要寫錯
點進去發現協議裡面有三個方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#pragma mark -- NSURLSessionDownloadDelegate /** * 下載完畢會呼叫 * * @param location 檔案臨時地址 */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { } /** * 每次寫入沙盒完畢呼叫 * 在這裡面監聽下載進度,totalBytesWritten/totalBytesExpectedToWrite * * @param bytesWritten 這次寫入的大小 * @param totalBytesWritten 已經寫入沙盒的大小 * @param totalBytesExpectedToWrite 檔案總大小 */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { self.pgLabel.text = [NSString stringWithFormat:@"下載進度:%f",(double)totalBytesWritten/totalBytesExpectedToWrite]; } /** * 恢復下載後呼叫, */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { } |
這上面的註釋已經很詳細了,相信大家都能看懂吧。
NSURLSession建立方式,這裡就不能使用Block回撥方式了,如果給下載任務設定了completionHandler這個block,也實現了下載的代理方法,優先執行block,代理方法也就不會執行了。
1 2 3 4 5 6 7 8 9 10 |
// 得到session物件 NSURLSessionConfiguration* cfg = [NSURLSessionConfiguration defaultSessionConfiguration]; // 預設配置 NSURLSession* session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[NSOperationQueue mainQueue]]; // 建立任務 NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url]; // 開始任務 [downloadTask resume]; |
相比之前的NSURLConnection方式簡單很多吧,用NSURLSessionDownloadTask做斷點下載也很簡單,我們先了解一下任務的取消方法
1 |
- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler; |
取消操作以後會呼叫一個Block,並傳入一個resumeData,該引數包含了繼續下載檔案的位置資訊。也就是說,當你下載了10M得檔案資料,暫停了。那麼你下次繼續下載的時候是從第10M這個位置開始的,而不是從檔案最開始的位置開始下載。因而為了儲存這些資訊,所以才定義了這個NSData型別的這個屬性:resumeData。這個data只包含了url跟已經下載了多少資料,不會很大,不用擔心記憶體問題。
另外,session還提供了通過resumeData來建立任務的方法
1 |
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData; |
我們只需要在取消操作的回撥中記錄好resumeData,然後在恢復下載的適合通過上面的方法建立任務就好了,相比NSURLconnection簡單太多了。需要注意的是Block中迴圈引用的問題
1 2 3 4 5 |
__weak typeof(self) selfVc = self; [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) { selfVc.resumeData = resumeData; selfVc.downloadTask = nil; }]; |
示例程式下載: https://github.com/hongfenglt/HFDownLoad
這篇部落格斷斷續續寫了兩三天,可能某些地方思路有些亂,歡迎大神指正。