天天看點

iOS 網絡:『檔案下載下傳、斷點下載下傳』的實作(一):NSURLConnection

目錄
  1. 檔案下載下傳簡介

    1.1 檔案下載下傳分類

    1.1.1 按檔案大小劃分

    1.1.2 按實作方法劃分

  2. 檔案下載下傳實作講解

    2.1 NSData(适用于小檔案下載下傳)

    2.2 NSURLConnection

    2.2.1 NSURLConnection(小檔案下載下傳)

    2.2.2 NSURLConnection(大檔案下載下傳)

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

關于『檔案下載下傳、斷點下載下傳』所有實作的Demo位址:​​Demo位址​​

1. 檔案下載下傳簡介

在iOS開發過程中,我們經常會遇到檔案下載下傳的需求,比如說圖檔下載下傳、音樂下載下傳、視訊下載下傳,還有其他檔案資源下載下傳等等。

下面我們就把檔案下載下傳相關方法和知識點總結一下。

1.1 檔案下載下傳分類

1.1.1 按檔案大小劃分

按照開發中實際需求,如果按下載下傳的檔案大小來分類的話,可以分為:小檔案下載下傳、大檔案下載下傳。

因為小檔案下載下傳基本不需要等待,可以使用傳回整個檔案的下載下傳方式來進行檔案下載下傳,比如說圖檔。但是大檔案下載下傳需要考慮很多情況來改善使用者體驗,比如說:下載下傳進度的顯示、暫停下載下傳以及斷點續傳、離線斷點續傳,還有下載下傳時占用手機記憶體情況等等。

1.1.2 按實作方法劃分

如果按照開發中使用到的下載下傳方法的話,我們可以使用NSData、NSURLConnection(iOS9.0之後舍棄)、NSURLSession(推薦),以及使用第三方架構AFNetworking等方式下載下傳檔案。

下面我們就根據檔案大小,以及對應的實作方法來講解下『檔案下載下傳、斷點下載下傳』的具體實作。本文主要講解NSData和NSURLConnection。

2. 檔案下載下傳實作講解

2.1 NSData(适用于小檔案下載下傳)

iOS 網絡:『檔案下載下傳、斷點下載下傳』的實作(一):NSURLConnection

NSData小檔案下載下傳效果.gif

  • 我們可以使用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(小檔案下載下傳)

iOS 網絡:『檔案下載下傳、斷點下載下傳』的實作(一):NSURLConnection

NSURLConnection小檔案下載下傳效果.gif

我們可以通過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(大檔案下載下傳)

iOS 網絡:『檔案下載下傳、斷點下載下傳』的實作(一):NSURLConnection

NSURLConnection大檔案下載下傳效果.gif

對于大檔案的下載下傳,我們就不能使用上邊的方法來下載下傳了。因為你如果是幾百兆以上的大檔案,那麼上邊的方法傳回的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(檔案句柄)類,可以實作對檔案的讀取、寫入、更新。

我們需要做如下幾步:

  1. 在接受到響應的時候,即在​

    ​didReceiveResponse​

    ​中建立一個空的沙盒檔案,并且建立一個NSFilehandle類。
  2. 在接受到具體資料的時候,即在​

    ​didReceiveData​

    ​中向沙盒檔案中寫入資料。
  • 通過NSFilehandle的​

    ​- (void)seekToFileOffset:(unsigned long long)offset;​

    ​​方法,制定檔案的寫入位置。或者通過NSFilehandle的​

    ​- (unsigned long long)seekToEndOfFile;​

    ​方法,直接制定檔案的寫入位置為檔案末尾。
  • 然後通過NSFilehandle的​

    ​writeData​

    ​方法,我們可以想沙盒中的檔案不斷寫入新資料。
  1. 在下載下傳完成之後,關閉沙盒檔案。

具體實作過程如下:

  • 定義下載下傳檔案需要用到的類和要實作的代理
@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(斷點下載下傳 | 支援離線)

iOS 網絡:『檔案下載下傳、斷點下載下傳』的實作(一):NSURLConnection

NSURLConnection離線斷點下載下傳效果.gif

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]。

  1. 添加需要實作斷點下載下傳的[開始/暫停]按鈕。
  2. 設定一個NSURLConnection的全局變量。
  3. 如果繼續下載下傳,設定HTTP請求頭的Range為目前已下載下傳檔案的長度位置到最後檔案末尾位置。然後建立一個NSURLConnection發送異步下載下傳,并監聽代理方法。
  4. 如果暫停下載下傳,那麼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;
}