iOS 網(wǎng)絡(luò):『文件下載、斷點下載』的實現(xiàn)(一):NSURLConnection

目錄

  1. 文件下載簡介
    1.1 文件下載分類
    1.1.1 按文件大小劃分
    1.1.2 按實現(xiàn)方法劃分
  2. 文件下載實現(xiàn)講解
    2.1 NSData(適用于小文件下載)
    2.2 NSURLConnection
    2.2.1 NSURLConnection(小文件下載)
    2.2.2 NSURLConnection(大文件下載)
    2.2.3 NSURLConnection(斷點下載 | 支持離線)

關(guān)于『文件下載、斷點下載』所有實現(xiàn)的Demo地址:Demo地址

iOS網(wǎng)絡(luò)--『文件下載、斷點下載』的實現(xiàn)相關(guān)文章:

1. 文件下載簡介

在iOS開發(fā)過程中,我們經(jīng)常會遇到文件下載的需求,比如說圖片下載、音樂下載、視頻下載,還有其他文件資源下載等等。

下面我們就把文件下載相關(guān)方法和知識點總結(jié)一下。

1.1 文件下載分類

1.1.1 按文件大小劃分

按照開發(fā)中實際需求,如果按下載的文件大小來分類的話,可以分為:小文件下載、大文件下載。

因為小文件下載基本不需要等待,可以使用返回整個文件的下載方式來進(jìn)行文件下載,比如說圖片。但是大文件下載需要考慮很多情況來改善用戶體驗,比如說:下載進(jìn)度的顯示、暫停下載以及斷點續(xù)傳、離線斷點續(xù)傳,還有下載時占用手機內(nèi)存情況等等。

1.1.2 按實現(xiàn)方法劃分

如果按照開發(fā)中使用到的下載方法的話,我們可以使用NSData、NSURLConnection(iOS9.0之后舍棄)、NSURLSession(推薦),以及使用第三方框架AFNetworking等方式下載文件。

下面我們就根據(jù)文件大小,以及對應(yīng)的實現(xiàn)方法來講解下『文件下載、斷點下載』的具體實現(xiàn)。本文主要講解NSData和NSURLConnection。

2. 文件下載實現(xiàn)講解

2.1 NSData(適用于小文件下載)

NSData小文件下載效果.gif
  • 我們可以使用NSData的 + (id)dataWithContentsOfURL:(NSURL *)url;進(jìn)行小文件的下載
  • 這個方法實際上是發(fā)送一次GET請求,然后返回整個文件。
  • 注意:需要將下面的代碼放到子線程中。

具體實現(xiàn)代碼如下:

// 創(chuàng)建下載路徑
NSURL *url = [NSURL URLWithString:@"http://pics.sc.chinaz.com/files/pic/pic9/201508/apic14052.jpg"];

// 使用NSData的dataWithContentsOfURL:方法下載
NSData *data = [NSData dataWithContentsOfURL:url];

// 如果下載的是將要顯示的圖片,則可以顯示出來
// 如果下載的是其他文件,然后可以將data轉(zhuǎn)存為本地文件

2.2 NSURLConnection

2.2.1 NSURLConnection(小文件下載)

NSURLConnection小文件下載效果.gif

我們可以通過NSURLConnection發(fā)送異步GET請求來下載文件。

// 創(chuàng)建下載路徑
NSURL *url = [NSURL URLWithString:@"http://pics.sc.chinaz.com/files/pic/pic9/201508/apic14052.jpg"];

// 使用NSURLConnection發(fā)送異步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(大文件下載)

NSURLConnection大文件下載效果.gif

對于大文件的下載,我們就不能使用上邊的方法來下載了。因為你如果是幾百兆以上的大文件,那么上邊的方法返回的data就會一直在內(nèi)存里,這樣內(nèi)存必然會爆掉,所以用上邊的方法不合適。那么我們可以使用NSURLConnection的另一個方法+ (NSURLConnection*)connectionWithRequest:(NSURLRequest *)request delegate:(id)delegate通過發(fā)送異步請求,并實現(xiàn)相關(guān)代理方法來實現(xiàn)大文件的下載。

// 創(chuàng)建下載路徑
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_15.mp4"];
// 使用NSURLConnection發(fā)送異步GET請求,并實現(xiàn)相應(yīng)的代理方法,該方法iOS9.0之后廢除了(推薦使用NSURLSession)。
[NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url] delegate:self];

這里使用到了代理,所以我們要實現(xiàn)NSURLConnectionDataDelegate的相關(guān)方法。主要用到以下幾個方法。

/**
 * 接收到響應(yīng)的時候就會調(diào)用
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;

/**
 * 接收到具體數(shù)據(jù)的時候會調(diào)用,會頻繁調(diào)用
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;

/**
 * 下載完文件之后調(diào)用
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;

/** 
 *  請求失敗時調(diào)用(請求超時、網(wǎng)絡(luò)異常) 
 */ 
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

其中,didReceiveData方法會在接受到具體數(shù)據(jù)的時候被頻繁調(diào)用,而且每一次都傳過來一部分data。

所以,我們可以創(chuàng)建一個全局NSMutableData來拼接每部分?jǐn)?shù)據(jù),最后將拼接完整的Data保存為文件。

但是這樣的話,NSMutableData會隨著拼接的數(shù)據(jù)而逐漸變得越來越大,這樣會導(dǎo)致內(nèi)存爆掉。這樣做顯然不適合。

那么我們應(yīng)該怎么做呢?

我們應(yīng)該在每獲取一部分?jǐn)?shù)據(jù)的時候,就將這部分?jǐn)?shù)據(jù)寫入沙盒中保存起來,并把這部分?jǐn)?shù)據(jù)釋放掉。

所幸我們有NSFilehandle(文件句柄)類,可以實現(xiàn)對文件的讀取、寫入、更新。

我們需要做如下幾步:

  1. 在接受到響應(yīng)的時候,即在didReceiveResponse中創(chuàng)建一個空的沙盒文件,并且創(chuàng)建一個NSFilehandle類。

  2. 在接受到具體數(shù)據(jù)的時候,即在didReceiveData中向沙盒文件中寫入數(shù)據(jù)。

    • 通過NSFilehandle的- (void)seekToFileOffset:(unsigned long long)offset;方法,制定文件的寫入位置?;蛘咄ㄟ^NSFilehandle的- (unsigned long long)seekToEndOfFile;方法,直接制定文件的寫入位置為文件末尾。
    • 然后通過NSFilehandle的writeData方法,我們可以想沙盒中的文件不斷寫入新數(shù)據(jù)。
  3. 在下載完成之后,關(guān)閉沙盒文件。

具體實現(xiàn)過程如下:

  • 定義下載文件需要用到的類和要實現(xiàn)的代理
@interface ViewController () <NSURLConnectionDataDelegate>

/** 下載進(jìn)度條 */
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
/** 下載進(jìn)度條Label */
@property (weak, nonatomic) IBOutlet UILabel *progressLabel;

/** NSURLConnection下載大文件需用到的屬性 **********/
/** 文件的總長度 */
@property (nonatomic, assign) NSInteger fileLength;
/** 當(dāng)前下載長度 */
@property (nonatomic, assign) NSInteger currentLength;
/** 文件句柄對象 */
@property (nonatomic, strong) NSFileHandle *fileHandle;

@end

  • 然后使用NSURLConnection的代理方式下載大文件

// 創(chuàng)建下載路徑
NSURL *url = [NSURL URLWithString:@"http://bmob-cdn-8782.b0.upaiyun.com/2017/01/17/24b0b37f40d8722480a23559298529f4.mp3"];

// 使用NSURLConnection發(fā)送異步Get請求,并實現(xiàn)相應(yīng)的代理方法,該方法iOS9.0之后廢除了。
[NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url] delegate:self];

  • 最后實現(xiàn)相關(guān)的NSURLConnectionDataDelegate方法
#pragma mark - <NSURLConnectionDataDelegate> 實現(xiàn)方法

/**
 * 接收到響應(yīng)的時候:創(chuàng)建一個空的沙盒文件和文件句柄
 */
- (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);

    // 創(chuàng)建一個空的文件到沙盒中
    [[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil];

    // 創(chuàng)建文件句柄
    self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:path];
}

/**
 * 接收到具體數(shù)據(jù):把數(shù)據(jù)寫入沙盒文件中
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    // 指定數(shù)據(jù)的寫入位置 -- 文件內(nèi)容的最后面
    [self.fileHandle seekToEndOfFile];

    // 向沙盒寫入數(shù)據(jù)
    [self.fileHandle writeData:data];

    // 拼接文件總長度
    self.currentLength += data.length;

    // 下載進(jìn)度
    self.progressView.progress =  1.0 * self.currentLength / self.fileLength;
    self.progressLabel.text = [NSString stringWithFormat:@"當(dāng)前下載進(jìn)度:%.2f%%",100.0 * self.currentLength / self.fileLength];
}

/**
 *  下載完文件之后調(diào)用:關(guān)閉文件、清空長度
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    // 關(guān)閉fileHandle
    [self.fileHandle closeFile];
    self.fileHandle = nil;

    // 清空長度
    self.currentLength = 0;
    self.fileLength = 0;
}

2.2.3 NSURLConnection(斷點下載 | 支持離線)

NSURLConnection離線斷點下載效果.gif

NSURLConnection并沒有提供暫停下載的方法,只提供了取消下載任務(wù)的cancel方法。

那么,如果我們想要使用NSURLConnection來實現(xiàn)斷點下載的功能,就需要先了解HTTP請求頭中Range的知識點。

HTTP請求頭中的Range可以只請求實體的一部分,指定范圍。

Range請求頭的格式為: Range: bytes=start-end

例如:
Range: bytes=10-:表示第10個字節(jié)及最后個字節(jié)的數(shù)據(jù)。
Range: bytes=40-100:表示第40個字節(jié)到第100個字節(jié)之間的數(shù)據(jù)。

注意:這里的[start,end],即是包含請求頭的start及end字節(jié)的。所以,下一個請求,應(yīng)該是上一個請求的[end+1, nextEnd]。

所以我們需要做的步驟為:

  1. 添加需要實現(xiàn)斷點下載的[開始/暫停]按鈕。
  2. 設(shè)置一個NSURLConnection的全局變量。
  3. 如果繼續(xù)下載,設(shè)置HTTP請求頭的Range為當(dāng)前已下載文件的長度位置到最后文件末尾位置。然后創(chuàng)建一個NSURLConnection發(fā)送異步下載,并監(jiān)聽代理方法。
  4. 如果暫停下載,那么NSURLConnection發(fā)送取消下載方法,并清空。

具體實現(xiàn)過程如下:

  • 定義下載文件需要用到的類和要實現(xiàn)的代理
@interface ViewController () <NSURLConnectionDataDelegate>

/** 下載進(jìn)度條 */
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
/** 下載進(jìn)度條Label */
@property (weak, nonatomic) IBOutlet UILabel *progressLabel;

/** NSURLConnection實現(xiàn)斷點下載(支持離線)需要用到的屬性 **********/
/** 文件的總長度 */
@property (nonatomic, assign) NSInteger fileLength;
/** 當(dāng)前下載長度 */
@property (nonatomic, assign) NSInteger currentLength;
/** 文件句柄對象 */
@property (nonatomic, strong) NSFileHandle *fileHandle;

/* connection */
@property (nonatomic, strong) NSURLConnection *connection;

@end
  • 添加支持?jǐn)帱c下載的[開始下載/暫停下載]按鈕,并實現(xiàn)相應(yīng)功能的代碼
/**
 * 點擊按鈕 -- 使用NSURLConnection斷點下載(支持離線)
 */
- (IBAction)resumeDownloadBtnClicked:(UIButton *)sender {
    // 按鈕狀態(tài)取反
    sender.selected = !sender.isSelected;
    
    if (sender.selected) {  // [開始下載/繼續(xù)下載]
        // 沙盒文件路徑
        NSString *path = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"QQ_V5.4.0.dmg"];
        
        // fileLengthForPath: 方法用來判斷已下載文件大小
        NSInteger currentLength = [self fileLengthForPath:path];
        if (currentLength > 0) {  // [繼續(xù)下載]
            self.currentLength = currentLength;
        }
        // 1. 創(chuàng)建下載URL
        NSURL *url = [NSURL URLWithString:@"http://dldir1.qq.com/qqfile/QQforMac/QQ_V5.4.0.dmg"];
            
        // 2. 創(chuàng)建request請求
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
            
        // 3. 設(shè)置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;
}
  • 最后實現(xiàn)相關(guān)的NSURLConnectionDataDelegate方法,這里和上邊使用NSURLConnection實現(xiàn)大文件下載的代碼一致。
#pragma mark <NSURLConnectionDataDelegate> 實現(xiàn)方法

/**
 * 接收到響應(yīng)的時候:創(chuàng)建一個空的沙盒文件
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    
    // 獲得下載文件的總長度:請求下載的文件長度 + 當(dāng)前已經(jīng)下載的文件長度
    self.fileLength = response.expectedContentLength + self.currentLength;
    
    // 沙盒文件路徑
    NSString *path = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"QQ_V5.4.0.dmg"];
    
    NSLog(@"File downloaded to: %@",path);
    
    // 創(chuàng)建一個空的文件到沙盒中
    NSFileManager *manager = [NSFileManager defaultManager];
    
    if (![manager fileExistsAtPath:path]) {
        // 如果沒有下載文件的話,就創(chuàng)建一個文件。如果有下載文件的話,則不用重新創(chuàng)建(不然會覆蓋掉之前的文件)
        [manager createFileAtPath:path contents:nil attributes:nil];
    }
    
    // 創(chuàng)建文件句柄
    self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:path];

}

/**
 * 接收到具體數(shù)據(jù):把數(shù)據(jù)寫入沙盒文件中
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    // 指定數(shù)據(jù)的寫入位置 -- 文件內(nèi)容的最后面
    [self.fileHandle seekToEndOfFile];
    
    // 向沙盒寫入數(shù)據(jù)
    [self.fileHandle writeData:data];
    
    // 拼接文件總長度
    self.currentLength += data.length;
    
    // 下載進(jìn)度
    self.progressView.progress =  1.0 * self.currentLength / self.fileLength;
    self.progressLabel.text = [NSString stringWithFormat:@"當(dāng)前下載進(jìn)度:%.2f%%",100.0 * self.currentLength / self.fileLength];
}

/**
 *  下載完文件之后調(diào)用:關(guān)閉文件、清空長度
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    // 關(guān)閉fileHandle
    [self.fileHandle closeFile];
    self.fileHandle = nil;
    
    // 清空長度
    self.currentLength = 0;
    self.fileLength = 0;
}

這樣就使用NSURLConnection實現(xiàn)了『斷點下載』的需求,并且支持程序被殺死,重新啟動之后也能接著下載的需求。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容