iOS開發網絡篇之文件下載、大文件下載、斷點下載


這裏寫圖片描述

iOS開發中經常會用到文件的下載與上傳功能,今天咱們來分享一下文件下載的思路。文件上傳下篇再說。

文件下載分爲小文件下載與大文件下載

小文件下載

小文件可以是一張圖片,或者一個文件,這裏指在現行的網絡狀況下基本上不需要等待很久就能下載好的文件。這裏以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方式下載

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];
    }];

就是發送一個異步的Get請求,回調的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寫入沙盒。
代碼如下:

@property (weak, nonatomic) IBOutlet UIProgressView *myPregress;

@property (nonatomic,strong) NSMutableData* fileData;

/**
 *  文件的總長度
 */
@property (nonatomic, assign) long long totalLength;
/**
 *  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。(度孃的地址)
注意:通常大文件下載是需要給用戶展示下載進度的。
這個數值是 已經下載的數據大小/要下載的文件總大小
已經下載的數據我們可以記錄,要下載的文件總大小在服務器返回的響應頭裏面可以拿到,在接受到響應的方法裏執行

 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; 這個方法

- (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,也就是從頭開始下。

上代碼:


#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.得到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];
// 發送post請求 自定義請求頭
[session dataTaskWithRequest:<#(NSURLRequest *)#> completionHandler:<#^(NSData *data, NSURLResponse *response, NSError *error)completionHandler#>]
NSURLSession 下載

使用NSURLSession就非常簡單了,不需要去考慮什麼邊下載邊寫入沙盒的問題,蘋果都幫我們做好了。代碼如下

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

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> 注意不要寫錯
點進去發現協議裏面有三個方法。

#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,代理方法也就不會執行了。

// 得到session對象
    NSURLSessionConfiguration* cfg = [NSURLSessionConfiguration defaultSessionConfiguration]; // 默認配置

    NSURLSession* session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[NSOperationQueue mainQueue]];

    // 創建任務
    NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url];

    // 開始任務
    [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中循環引用的問題

__weak typeof(self) selfVc = self;
    [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
        selfVc.resumeData = resumeData;
        selfVc.downloadTask = nil;
    }];

示例程序下載:https://github.com/hongfenglt/HFDownLoad

這篇博客斷斷續續寫了兩三天,可能某些地方思路有些亂,歡迎大神指正。



文/勤奮的笨老頭(簡書作者)
原文鏈接:http://www.jianshu.com/p/f65e32012f07
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章