天天看點

iOS開發之小檔案下載下傳、大檔案下載下傳、斷點下載下傳

轉載位址http://www.jianshu.com/p/f65e32012f07?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

檔案下載下傳分為小檔案下載下傳與大檔案下載下傳

小檔案下載下傳

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

 協定.進去看看有幾個代理方法,其實我們能用到的也就三個。

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error

{

}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response

{

}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data

{

}

- (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;

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response

{

    self.fileData = [NSMutableData data];

    // 擷取要下載下傳的檔案的大小

    self.totalLength = response.expectedContentLength;

}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data

{

    [self.fileData appendData:data];

    self.myPregress.progress = (double)self.fileData.length / self.totalLength;

}

- (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 這句代表擷取下載下傳的檔案名

iOS開發之小檔案下載下傳、大檔案下載下傳、斷點下載下傳

這裡寫圖檔描述

題外話扯的有點多,言歸正傳,這樣我們确實可以下載下傳檔案,最後拿到的檔案也能正常運作

iOS開發之小檔案下載下傳、大檔案下載下傳、斷點下載下傳

但是有個緻命的問題,記憶體!用來接受檔案的NSMutableData一直都在記憶體中,會随着檔案的下載下傳一直變大,

iOS開發之小檔案下載下傳、大檔案下載下傳、斷點下載下傳

這裡寫圖檔描述

所有這種處理方式絕對是不合理的。

合理的方式在我們擷取一部分data的時候就寫入沙盒中,然後釋放記憶體中的data。

這裡要用到NSFilehandle這個類,這個類可以實作對檔案的讀取、寫入、更新。下面總結了一些常用的NSFileHandle的方法,在這個表中,fh是一個NSFileHandle對象,data是一個NSData對象,path是一個NSString 對象,offset是易額Unsigned long long變量。

iOS開發之小檔案下載下傳、大檔案下載下傳、斷點下載下傳

這裡寫圖檔描述

具體關于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;

}

- (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;

}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection

{

    self.currentLength = 0;

    self.totalLength = 0;

    // 關閉檔案

    [self.writeHandle closeFile];

    self.writeHandle = nil;

}

這樣在下載下傳過程中記憶體就會一直很穩定了,并且下載下傳的檔案也是沒問題的。

iOS開發之小檔案下載下傳、大檔案下載下傳、斷點下載下傳

這裡寫圖檔描述

斷點下載下傳

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

iOS開發之小檔案下載下傳、大檔案下載下傳、斷點下載下傳

這裡寫圖檔描述

不難看出,通過設定請求頭的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

iOS開發之小檔案下載下傳、大檔案下載下傳、斷點下載下傳

不過在下載下傳完成之後會自動删除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

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask

didFinishDownloadingToURL:(NSURL *)location

{

}

- (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

繼續閱讀