-
小文件下載
-
小文件可以是一張圖片,或者一個文件,這里指在現行的網絡狀況下基本上不需要等待很久就能下載好的文件。這里以picjumbo里的一張圖片為例子。
-
-
NSData方式
-
其實我們經常用的
[NSData dataWithContentsOfURL]
就是一種文件下載方式,猜測這里面應該是發送了Get請求。
-
NSURL *url = [NSURL URLWithString:@"https://picjumbo.imgix.net/HNCK8461.jpg?q=40&w=1650&sharp=30"]; NSData *data = [NSData dataWithContentsOfURL:url];
-
NSURLConnection方式下載
- 就是發送一個異步的Get請求,回調的data就是我們下載到的圖片。
這些都很簡單,今天主要說的是大文件的下載。
- 就是發送一個異步的Get請求,回調的data就是我們下載到的圖片。
NSURL* url = [NSURL URLWithString:@"https://picjumbo.imgix.net/HNCK8461.jpg?q=40&w=1650&sharp=30"]; [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { self.imageView.image = [UIImage imageWithData:data]; }];
大文件下載
-
NSURLConnection下載
- 通過上面的兩個方法去下載大文件是不合理的,因為這兩個方法都是一次性返回整個下載到的文件,返回的data在內存中,如果下載一個幾百兆的東西,內存肯定會爆的,其實NSURLConnection還提供了另外一種發送請求的方式。
// 發送請求去下載 (創建完conn對象后,會自動發起一個異步請求) [NSURLConnection connectionWithRequest:request delegate:self];
//這里用到了代理,那肯定要遵守協議了.遵守NSURLConnectionDataDelegate 協議./** * 請求失敗時調用(請求超時、網絡異常) * * @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 @property (weak, nonatomic) IBOutlet UIProgressView *myPregress; 2 3 @property (nonatomic,strong) NSMutableData* fileData; 4 5 /** 6 * 文件的總長度 7 */ 8 @property (nonatomic, assign) long long totalLength; 9 /** 10 * 1.接收到服務器的響應就會調用 11 * 12 * @param response 響應 13 */ 14 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 15 { 16 self.fileData = [NSMutableData data]; 17 // 獲取要下載的文件的大小 18 self.totalLength = response.expectedContentLength; 19 } 20 /** 21 * 2.當接收到服務器返回的實體數據時調用(具體內容,這個方法可能會被調用多次) 22 * 23 * @param data 這次返回的數據 24 */ 25 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 26 { 27 [self.fileData appendData:data]; 28 self.myPregress.progress = (double)self.fileData.length / self.totalLength; 29 } 30 /** 31 * 3.加載完畢后調用(服務器的數據已經完全返回后) 32 */ 33 - (void)connectionDidFinishLoading:(NSURLConnection *)connection 34 { 35 // 拼接文件路徑 36 NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; 37 NSString *file = [cache stringByAppendingPathComponent:response.suggestedFilename]; 38 39 // 寫到沙盒中 40 [self.fileData writeToFile:file atomically:YES]; 41 }
我這里下載的是javajdk。(百度的地址)
注意:通常大文件下載是需要給用戶展示下載進度的。
這個數值是 已經下載的數據大小/要下載的文件總大小
已經下載的數據我們可以記錄,要下載的文件總大小在服務器返回的響應頭里面可以拿到,在接受到響應的方法里執行
1 NSHTTPURLResponse *res = (NSHTTPURLResponse*)response; 2 3 NSDictionary *headerDic = res.allHeaderFields; 4 NSLog(@"%@",headerDic); 5 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 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 2 { 3 // 文件路徑 4 NSString* ceches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; 5 NSString* filepath = [ceches stringByAppendingPathComponent:response.suggestedFilename]; 6 7 // 創建一個空的文件到沙盒中 8 NSFileManager* mgr = [NSFileManager defaultManager]; 9 [mgr createFileAtPath:filepath contents:nil attributes:nil]; 10 11 // 創建一個用來寫數據的文件句柄對象 12 self.writeHandle = [NSFileHandle fileHandleForWritingAtPath:filepath]; 13 14 // 獲得文件的總大小 15 self.totalLength = response.expectedContentLength; 16 17 } 18 /** 19 * 2.當接收到服務器返回的實體數據時調用(具體內容,這個方法可能會被調用多次) 20 * 21 * @param data 這次返回的數據 22 */ 23 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 24 { 25 // 移動到文件的最后面 26 [self.writeHandle seekToEndOfFile]; 27 28 // 將數據寫入沙盒 29 [self.writeHandle writeData:data]; 30 31 // 累計寫入文件的長度 32 self.currentLength += data.length; 33 34 // 下載進度 35 self.myPregress.progress = (double)self.currentLength / self.totalLength; 36 } 37 /** 38 * 3.加載完畢后調用(服務器的數據已經完全返回后) 39 */ 40 - (void)connectionDidFinishLoading:(NSURLConnection *)connection 41 { 42 self.currentLength = 0; 43 self.totalLength = 0; 44 45 // 關閉文件 46 [self.writeHandle closeFile]; 47 self.writeHandle = nil; 48 }
- 這樣在下載過程中內存就會一直很穩定了,並且下載的文件也是沒問題的。

斷點下載
- 暫停/繼續下載也是現在下載中必備的功能了,如果沒有暫停功能,用戶體驗相比會很差,而且如果突然網絡不好中斷了,沒有實現斷點下載的話只有重新下了。。。
下面讓我們來加入斷點下載功能吧。
- NSURLConnection 只提供了一個cancel方法,這並不是暫停,而是取消下載任務。如果要實現斷點下載必須要了解HTTP協議中請求頭的Range。

- 不難看出,通過設置請求頭的Range我們可以指定下載的位置、大小。
- 那么我們這樣設置
bytes=500- 從500字節以后的所有字節
, - 只需要在didReceiveData中記錄已經寫入沙盒中文件的大小(self.currentLength),
- 把這個大小設置到請求頭中,因為第一次下載肯定是沒有執行過didReceive方法,self.currentLength也就為0,也就是從頭開始下。
1 #pragma mark --按鈕點擊事件 2 3 - (IBAction)btnClicked:(UIButton *)sender { 4 5 // 狀態取反 6 sender.selected = !sender.isSelected; 7 8 // 斷點續傳 9 // 斷點下載 10 11 if (sender.selected) { // 繼續(開始)下載 12 // 1.URL 13 NSURL *url = [NSURL URLWithString:@"http://localhost:8080//term_app/hdgg.zip"]; 14 15 // 2.請求 16 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; 17 18 // 設置請求頭 19 NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength]; 20 [request setValue:range forHTTPHeaderField:@"Range"]; 21 22 // 3.下載 23 self.connection = [NSURLConnection connectionWithRequest:request delegate:self]; 24 } else { // 暫停 25 26 [self.connection cancel]; 27 self.connection = nil; 28 } 29 }
- 在下載過程中,為了提高效率,充分利用cpu性能,通常會執行多線程下載,代碼就不貼了,分析一下思路:
下載開始,創建一個和要下載的文件大小相同的文件(如果要下載的文件為100M,那么就在沙盒中創建一個100M的文件,然后計算每一段的下載量,開啟多條線程下載各段的數據,分別寫入對應的文件部分)。
NSURLSession下載方式
- 上面這種下載文件的方式確實比較復雜,要自己去控制內存寫入相應的位置,不過在蘋果在iOS7推出了一個新的類
NSURLSession
,它具備了NSURLConnection所具備的方法,同時也比它更強大。蘋果推出它的目的大有取代NSURLConnection的趨勢或者目的。 NSURLSession
也可以發送Get/Post請求,實現文件的下載和上傳。- 在NSURLSesiion中,任何請求都可以被看做是一個任務。其中有三種任務類型
1 // NSURLSessionDataTask : 普通的GET\POST請求 2 // NSURLSessionDownloadTask : 文件下載 3 // NSURLSessionUploadTask : 文件上傳(很少用,一般服務器不支持)
NSURLSession 簡單使用
- NSURLSession發送請求非常簡單,與connection不同的是,任務創建后不會自動發送請求,需要手動開始執行任務。
1 // 1.得到session對象 2 NSURLSession* session = [NSURLSession sharedSession]; 3 NSURL* url = [NSURL URLWithString:@""]; 4 5 // 2.創建一個task,任務 6 NSURLSessionDataTask* dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 7 // data 為返回數據 8 }]; 9 10 // 3.開始任務 11 [dataTask resume]; 12
// 發送post請求 自定義請求頭 [session dataTaskWithRequest:<#(NSURLRequest *)#> completionHandler:<#^(NSData *data, NSURLResponse *response, NSError *error)completionHandler#>]
NSURLSession 下載
- 使用NSURLSession就非常簡單了,不需要去考慮什么邊下載邊寫入沙盒的問題,蘋果都幫我們做好了。代碼如下
1 NSURL* url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/9d/25765/sogou_mac_32c_V3.2.0.1437101586.dmg"]; 2 3 // 得到session對象 4 NSURLSession* session = [NSURLSession sharedSession]; 5 6 // 創建任務 7 NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { 8 9 }]; 10 // 開始任務 11 [downloadTask resume]; 12
- 是不是跟NSURLConnection很像,但仔細看會發現回調的方法里面並沒用NSData傳回來,多了一個location,顧名思義,location就是下載好的文件寫入沙盒的地址,打印一下發現下載好的文件被自動寫入的temp文件夾下面了。

- 不過在下載完成之后會自動刪除temp中的文件,所有我們需要做的只是在回調中把文件移動(或者復制,反正之后會自動刪除)到caches中。
1 NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; 2 // response.suggestedFilename : 建議使用的文件名,一般跟服務器端的文件名一致 3 NSString *file = [caches stringByAppendingPathComponent:response.suggestedFilename]; 4 5 // 將臨時文件剪切或者復制Caches文件夾 6 NSFileManager *mgr = [NSFileManager defaultManager]; 7 8 // AtPath : 剪切前的文件路徑 9 // ToPath : 剪切后的文件路徑 10 [mgr moveItemAtPath:location.path toPath:file error:nil]; 11
- 不過通過這種方式下載有個缺點就是無法監聽下載進度,要監聽下載進度,蘋果通常的作法是通過delegate,這里也一樣。而且NSURLSession的創建方式也有所不同。 首先遵守協議<NSURLSessionDownloadDelegate> 注意不要寫錯 點進去發現協議里面有三個方法:如下
1 #pragma mark -- NSURLSessionDownloadDelegate 2 /** 3 * 下載完畢會調用 4 * 5 * @param location 文件臨時地址 6 */ 7 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask 8 didFinishDownloadingToURL:(NSURL *)location 9 { 10 } 11 /** 12 * 每次寫入沙盒完畢調用 13 * 在這里面監聽下載進度,totalBytesWritten/totalBytesExpectedToWrite 14 * 15 * @param bytesWritten 這次寫入的大小 16 * @param totalBytesWritten 已經寫入沙盒的大小 17 * @param totalBytesExpectedToWrite 文件總大小 18 */ 19 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask 20 didWriteData:(int64_t)bytesWritten 21 totalBytesWritten:(int64_t)totalBytesWritten 22 totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite 23 { 24 self.pgLabel.text = [NSString stringWithFormat:@"下載進度:%f",(double)totalBytesWritten/totalBytesExpectedToWrite]; 25 } 26 27 /** 28 * 恢復下載后調用, 29 */ 30 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask 31 didResumeAtOffset:(int64_t)fileOffset 32 expectedTotalBytes:(int64_t)expectedTotalBytes 33 { 34 35 }
- NSURLSession創建方式,這里就不能使用Block回調方式了,如果給下載任務設置了completionHandler這個block,也實現了下載的代理方法,優先執行block,代理方法也就不會執行了。
1 // 得到session對象 2 NSURLSessionConfiguration* cfg = [NSURLSessionConfiguration defaultSessionConfiguration]; // 默認配置 3 4 NSURLSession* session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[NSOperationQueue mainQueue]]; 5 6 // 創建任務 7 NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url]; 8 9 // 開始任務 10 [downloadTask resume];
- 相比之前的NSURLConnection方式簡單很多吧,用NSURLSessionDownloadTask做斷點下載也很簡單,我們先了解一下任務的取消方法
- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler;
- 取消操作以后會調用一個Block,並傳入一個resumeData,該參數包含了繼續下載文件的位置信息。也就是說,當你下載了10M得文件數據,暫停了。那么你下次繼續下載的時候是從第10M這個位置開始的,而不是從文件最開始的位置開始下載。因而為了保存這些信息,所以才定義了這個NSData類型的這個屬性:resumeData。這個data只包含了url跟已經下載了多少數據,不會很大,不用擔心內存問題。
- 另外,session還提供了通過resumeData來創建任務的方法
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;
- 我們只需要在取消操作的回調中記錄好resumeData,然后在恢復下載的適合通過上面的方法創建任務就好了,相比NSURLconnection簡單太多了。
需要注意的是Block中循環引用的問題
1 __weak typeof(self) selfVc = self; 2 [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) { 3 selfVc.resumeData = resumeData; 4 selfVc.downloadTask = nil; 5 }];