本文首發於我的個人博客:『不羈閣』 https://bujige.net
文章鏈接:https://bujige.net/blog/iOS-Resume-Download-NSURLConnection.html
目錄
- 文件下載簡介
1.1 文件下載分類
1.1.1 按文件大小劃分
1.1.2 按實現方法劃分- 文件下載實現講解
2.1 NSData(適用於小文件下載)
2.2 NSURLConnection
2.2.1 NSURLConnection(小文件下載)
2.2.2 NSURLConnection(大文件下載)
2.2.3 NSURLConnection(斷點下載 | 支持離線)
關於『文件下載、斷點下載』所有實現的Demo地址:Demo地址
iOS網絡--『文件下載、斷點下載』的實現相關文章:
- iOS網絡--『文件下載、斷點下載』的實現(一):NSURLConnection
- iOS網絡--『文件下載、斷點下載』的實現(二):NSURLSession
- iOS網絡--『文件下載、斷點下載』的實現(三):AFNetworking
1. 文件下載簡介
在iOS開發過程中,我們經常會遇到文件下載的需求,比如說圖片下載、音樂下載、視頻下載,還有其他文件資源下載等等。
下面我們就把文件下載相關方法和知識點總結一下。
1.1 文件下載分類
1.1.1 按文件大小劃分
按照開發中實際需求,如果按下載的文件大小來分類的話,可以分爲:小文件下載、大文件下載。
因爲小文件下載基本不需要等待,可以使用返回整個文件的下載方式來進行文件下載,比如說圖片。但是大文件下載需要考慮很多情況來改善用戶體驗,比如說:下載進度的顯示、暫停下載以及斷點續傳、離線斷點續傳,還有下載時佔用手機內存情況等等。
1.1.2 按實現方法劃分
如果按照開發中使用到的下載方法的話,我們可以使用NSData、NSURLConnection(iOS9.0之後捨棄)、NSURLSession(推薦),以及使用第三方框架AFNetworking等方式下載文件。
下面我們就根據文件大小,以及對應的實現方法來講解下『文件下載、斷點下載』的具體實現。本文主要講解NSData和NSURLConnection。
2. 文件下載實現講解
2.1 NSData(適用於小文件下載)
- 我們可以使用NSData的
+ (id)dataWithContentsOfURL:(NSURL *)url;
進行小文件的下載 - 這個方法實際上是發送一次GET請求,然後返回整個文件。
- 注意:需要將下面的代碼放到子線程中。
具體實現代碼如下:
// 創建下載路徑
NSURL *url = [NSURL URLWithString:@"http://pics.sc.chinaz.com/files/pic/pic9/201508/apic14052.jpg"];
// 使用NSData的dataWithContentsOfURL:方法下載
NSData *data = [NSData dataWithContentsOfURL:url];
// 如果下載的是將要顯示的圖片,則可以顯示出來
// 如果下載的是其他文件,然後可以將data轉存爲本地文件
2.2 NSURLConnection
2.2.1 NSURLConnection(小文件下載)
我們可以通過NSURLConnection發送異步GET請求來下載文件。
// 創建下載路徑
NSURL *url = [NSURL URLWithString:@"http://pics.sc.chinaz.com/files/pic/pic9/201508/apic14052.jpg"];
// 使用NSURLConnection發送異步GET請求,該方法在iOS9.0之後就廢除了(推薦使用NSURLSession)
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSLog(@"%@",data);
// 可以在這裏把下載的文件保存起來
}];
2.2.2 NSURLConnection(大文件下載)
對於大文件的下載,我們就不能使用上邊的方法來下載了。因爲你如果是幾百兆以上的大文件,那麼上邊的方法返回的data就會一直在內存裏,這樣內存必然會爆掉,所以用上邊的方法不合適。那麼我們可以使用NSURLConnection的另一個方法+ (NSURLConnection*)connectionWithRequest:(NSURLRequest *)request delegate:(id)delegate
通過發送異步請求,並實現相關代理方法來實現大文件的下載。
// 創建下載路徑
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_15.mp4"];
// 使用NSURLConnection發送異步GET請求,並實現相應的代理方法,該方法iOS9.0之後廢除了(推薦使用NSURLSession)。
[NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
這裏使用到了代理,所以我們要實現NSURLConnectionDataDelegate的相關方法。主要用到以下幾個方法。
/**
* 接收到響應的時候就會調用
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
/**
* 接收到具體數據的時候會調用,會頻繁調用
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
/**
* 下載完文件之後調用
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
/**
* 請求失敗時調用(請求超時、網絡異常)
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
其中,didReceiveData
方法會在接受到具體數據的時候被頻繁調用,而且每一次都傳過來一部分data。
所以,我們可以創建一個全局NSMutableData來拼接每部分數據,最後將拼接完整的Data保存爲文件。
但是這樣的話,NSMutableData會隨着拼接的數據而逐漸變得越來越大,這樣會導致內存爆掉。這樣做顯然不適合。
那麼我們應該怎麼做呢?
我們應該在每獲取一部分數據的時候,就將這部分數據寫入沙盒中保存起來,並把這部分數據釋放掉。
所幸我們有NSFilehandle(文件句柄)類,可以實現對文件的讀取、寫入、更新。
我們需要做如下幾步:
-
在接受到響應的時候,即在
didReceiveResponse
中創建一個空的沙盒文件,並且創建一個NSFilehandle類。 -
在接受到具體數據的時候,即在
didReceiveData
中向沙盒文件中寫入數據。- 通過NSFilehandle的
- (void)seekToFileOffset:(unsigned long long)offset;
方法,制定文件的寫入位置。或者通過NSFilehandle的- (unsigned long long)seekToEndOfFile;
方法,直接制定文件的寫入位置爲文件末尾。 - 然後通過NSFilehandle的
writeData
方法,我們可以想沙盒中的文件不斷寫入新數據。
- 通過NSFilehandle的
-
在下載完成之後,關閉沙盒文件。
具體實現過程如下:
- 定義下載文件需要用到的類和要實現的代理
@interface ViewController () <NSURLConnectionDataDelegate>
/** 下載進度條 */
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
/** 下載進度條Label */
@property (weak, nonatomic) IBOutlet UILabel *progressLabel;
/** NSURLConnection下載大文件需用到的屬性 **********/
/** 文件的總長度 */
@property (nonatomic, assign) NSInteger fileLength;
/** 當前下載長度 */
@property (nonatomic, assign) NSInteger currentLength;
/** 文件句柄對象 */
@property (nonatomic, strong) NSFileHandle *fileHandle;
@end
- 然後使用NSURLConnection的代理方式下載大文件
// 創建下載路徑
NSURL *url = [NSURL URLWithString:@"http://bmob-cdn-8782.b0.upaiyun.com/2017/01/17/24b0b37f40d8722480a23559298529f4.mp3"];
// 使用NSURLConnection發送異步Get請求,並實現相應的代理方法,該方法iOS9.0之後廢除了。
[NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
- 最後實現相關的NSURLConnectionDataDelegate方法
#pragma mark - <NSURLConnectionDataDelegate> 實現方法
/**
* 接收到響應的時候:創建一個空的沙盒文件和文件句柄
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// 獲得下載文件的總長度
self.fileLength = response.expectedContentLength;
// 沙盒文件路徑
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
// 打印下載的沙盒路徑
NSLog(@"File downloaded to: %@",path);
// 創建一個空的文件到沙盒中
[[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil];
// 創建文件句柄
self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:path];
}
/**
* 接收到具體數據:把數據寫入沙盒文件中
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// 指定數據的寫入位置 -- 文件內容的最後面
[self.fileHandle seekToEndOfFile];
// 向沙盒寫入數據
[self.fileHandle writeData:data];
// 拼接文件總長度
self.currentLength += data.length;
// 下載進度
self.progressView.progress = 1.0 * self.currentLength / self.fileLength;
self.progressLabel.text = [NSString stringWithFormat:@"當前下載進度:%.2f%%",100.0 * self.currentLength / self.fileLength];
}
/**
* 下載完文件之後調用:關閉文件、清空長度
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// 關閉fileHandle
[self.fileHandle closeFile];
self.fileHandle = nil;
// 清空長度
self.currentLength = 0;
self.fileLength = 0;
}
2.2.3 NSURLConnection(斷點下載 | 支持離線)
NSURLConnection並沒有提供暫停下載的方法,只提供了取消下載任務的cancel
方法。
那麼,如果我們想要使用NSURLConnection來實現斷點下載的功能,就需要先了解HTTP請求頭中Range的知識點。
HTTP請求頭中的Range可以只請求實體的一部分,指定範圍。
Range請求頭的格式爲: Range: bytes=start-end
例如:Range: bytes=10-
:表示第10個字節及最後個字節的數據。Range: bytes=40-100
:表示第40個字節到第100個字節之間的數據。
注意:這裏的[start,end],即是包含請求頭的start及end字節的。所以,下一個請求,應該是上一個請求的[end+1, nextEnd]。
所以我們需要做的步驟爲:
- 添加需要實現斷點下載的[開始/暫停]按鈕。
- 設置一個NSURLConnection的全局變量。
- 如果繼續下載,設置HTTP請求頭的Range爲當前已下載文件的長度位置到最後文件末尾位置。然後創建一個NSURLConnection發送異步下載,並監聽代理方法。
- 如果暫停下載,那麼NSURLConnection發送取消下載方法,並清空。
具體實現過程如下:
- 定義下載文件需要用到的類和要實現的代理
@interface ViewController () <NSURLConnectionDataDelegate>
/** 下載進度條 */
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
/** 下載進度條Label */
@property (weak, nonatomic) IBOutlet UILabel *progressLabel;
/** NSURLConnection實現斷點下載(支持離線)需要用到的屬性 **********/
/** 文件的總長度 */
@property (nonatomic, assign) NSInteger fileLength;
/** 當前下載長度 */
@property (nonatomic, assign) NSInteger currentLength;
/** 文件句柄對象 */
@property (nonatomic, strong) NSFileHandle *fileHandle;
/* connection */
@property (nonatomic, strong) NSURLConnection *connection;
@end
- 添加支持斷點下載的[開始下載/暫停下載]按鈕,並實現相應功能的代碼
/**
* 點擊按鈕 -- 使用NSURLConnection斷點下載(支持離線)
*/
- (IBAction)resumeDownloadBtnClicked:(UIButton *)sender {
// 按鈕狀態取反
sender.selected = !sender.isSelected;
if (sender.selected) { // [開始下載/繼續下載]
// 沙盒文件路徑
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"QQ_V5.4.0.dmg"];
// fileLengthForPath: 方法用來判斷已下載文件大小
NSInteger currentLength = [self fileLengthForPath:path];
if (currentLength > 0) { // [繼續下載]
self.currentLength = currentLength;
}
// 1. 創建下載URL
NSURL *url = [NSURL URLWithString:@"http://dldir1.qq.com/qqfile/QQforMac/QQ_V5.4.0.dmg"];
// 2. 創建request請求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 3. 設置HTTP請求頭中的Range
NSString *range = [NSString stringWithFormat:@"bytes=%ld-", self.currentLength];
[request setValue:range forHTTPHeaderField:@"Range"];
// 4.下載
self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
} else { // [暫停下載]
[self.connection cancel];
self.connection = nil;
}
}
/**
* 獲取已下載的文件大小
*/
- (NSInteger)fileLengthForPath:(NSString *)path {
NSInteger fileLength = 0;
NSFileManager *fileManager = [[NSFileManager alloc] init]; // default is not thread safe
if ([fileManager fileExistsAtPath:path]) {
NSError *error = nil;
NSDictionary *fileDict = [fileManager attributesOfItemAtPath:path error:&error];
if (!error && fileDict) {
fileLength = [fileDict fileSize];
}
}
return fileLength;
}
- 最後實現相關的NSURLConnectionDataDelegate方法,這裏和上邊使用NSURLConnection實現大文件下載的代碼一致。
#pragma mark <NSURLConnectionDataDelegate> 實現方法
/**
* 接收到響應的時候:創建一個空的沙盒文件
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// 獲得下載文件的總長度:請求下載的文件長度 + 當前已經下載的文件長度
self.fileLength = response.expectedContentLength + self.currentLength;
// 沙盒文件路徑
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"QQ_V5.4.0.dmg"];
NSLog(@"File downloaded to: %@",path);
// 創建一個空的文件到沙盒中
NSFileManager *manager = [NSFileManager defaultManager];
if (![manager fileExistsAtPath:path]) {
// 如果沒有下載文件的話,就創建一個文件。如果有下載文件的話,則不用重新創建(不然會覆蓋掉之前的文件)
[manager createFileAtPath:path contents:nil attributes:nil];
}
// 創建文件句柄
self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:path];
}
/**
* 接收到具體數據:把數據寫入沙盒文件中
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// 指定數據的寫入位置 -- 文件內容的最後面
[self.fileHandle seekToEndOfFile];
// 向沙盒寫入數據
[self.fileHandle writeData:data];
// 拼接文件總長度
self.currentLength += data.length;
// 下載進度
self.progressView.progress = 1.0 * self.currentLength / self.fileLength;
self.progressLabel.text = [NSString stringWithFormat:@"當前下載進度:%.2f%%",100.0 * self.currentLength / self.fileLength];
}
/**
* 下載完文件之後調用:關閉文件、清空長度
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// 關閉fileHandle
[self.fileHandle closeFile];
self.fileHandle = nil;
// 清空長度
self.currentLength = 0;
self.fileLength = 0;
}
這樣就使用NSURLConnection實現了『斷點下載』的需求,並且支持程序被殺死,重新啓動之後也能接着下載的需求。