(一二六)單線程文件下載與斷點續傳

本文討論單線程的文件下載和斷點續傳,通過從本地服務器下載一個較大的文件,實現顯示進度、中途暫停與斷點續傳。

下載過程大致如下:

①通過NSURL創建指向特定下載地址的請求,本文中下載的文件位於網站根目錄的lesson1下的nav.dmg,因此URL應爲http://127.0.0.1/lesson1/nav.dmg。

②通過NSURL創建URLRequest,爲了能夠更改HTTP請求頭,實現特定字節的下載,使用NSMutableURLRequest,然後設置請求頭的Range範圍,range的主要寫法是bytes=x-y,代表下載x-y的字節,注意字節的起始是0。也可以忽略一端,例如x-代表下載x和以後的字節;而-y表示下載最後y個字節。

③使用NSURLConnection執行請求,並且設置代理,通過代理方法接收數據。

爲了能夠實時計算進度,我們使用一系列變量,記錄下載了的字節數和總字節數:

@property (nonatomic, assign) long long totalLength;
@property (nonatomic, assign) long long currentLength;

【NSURLConnection的代理方法】

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

當出錯時來到這裏。

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

這個比較重要,在下載文件之前會先拿到這個響應頭,從中可以拿到文件的大小,在這裏適合做一些初始化工作,例如0B文件的創建和總大小、當前大小的初始化。

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

每當文件下載完一小塊,就會調用一次,從這裏可以進行文件拼接,進度更新。

- (void)connectionDidFinishLoading:(NSURLConnection *)connection

當文件下載完成時調用這個方法,一般在這裏關閉文件。

很顯然網絡暢通時的調用順序是②->多次③->④。

【文件的操作】

①通過NSFileManager創建空文件,用於數據拼接。

// 先創建一個空文件
NSFileManager *mgr = [NSFileManager defaultManager];
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"download.dmg"];
[mgr createFileAtPath:path contents:nil attributes:nil];
②爲了能夠方便拼接,使用NSFileHandle指向文件,並且保存成成員變量。

@property (nonatomic, strong) NSFileHandle *handle;
// 使用文件句柄操作文件
NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:path];
_handle = handle;
_totalLength = response.expectedContentLength;
_currentLength = 0;
③文件的拼接

通過NSFileHandle的seekToEndOfFile方法可以指向當前文件尾,然後使用writeData方法向後拼接。

// 下載拼接思路:先創建空文件,然後一部分一部分的拼接
[self.handle seekToEndOfFile];
[self.handle writeData:data];
總體來說,思路是一部分一部分的拼出最終文件。

【斷點續傳】

暫停只能通過結束NSURLConnection實現,connection一旦結束就會失效,只能重新創建。

[self.connection cancel];
self.connection = nil;
爲了實現下次再從中斷的位置下載,只需要通過_currentLength即可,讓Range爲bytes:_currentLength-即可,因爲字節是從0開始的,當前長度爲len代表下一個要下載的字節編號就是len。

請求代碼如下:

NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/lesson1/nav.dmg"];
// 通過設置請求頭range來設置下載數據的範圍
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSString *range = [NSString stringWithFormat:@"bytes=%lld-",self.currentLength];
[request setValue:range forHTTPHeaderField:@"Range"];
self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
另一個注意點是因爲新創建了請求,會再去獲取響應頭,這樣又會用空文件覆蓋原文件,重新下載,我們加一個判斷,如果_currentLength≠0,直接返回即可。


下面是完整的代碼,其中downloadBar是進度條,btn用於控制下載開始與暫停,二者均來自storyboard。btn點擊後延時0.5秒開始下載是爲了防止點擊後按鈕被阻塞無法到達暫停鍵狀態。

//
//  ViewController.m
//  大文件下載
//
//  Copyright (c) 2015 soulghost. All rights reserved.
//

#import "ViewController.h"

@interface ViewController () <NSURLConnectionDataDelegate>

@property (nonatomic, strong) NSFileHandle *handle;
@property (nonatomic, assign) long long totalLength;
@property (nonatomic, assign) long long currentLength;
@property (weak, nonatomic) IBOutlet UIProgressView *downloadBar;
@property (weak, nonatomic) IBOutlet UIButton *btn;

@property (nonatomic, strong) NSURLConnection *connection;

@end

@implementation ViewController

- (void)startDownload{
    
    NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/lesson1/nav.dmg"];

    // 通過設置請求頭range來設置下載數據的範圍

    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    NSString *range = [NSString stringWithFormat:@"bytes=%lld-",self.currentLength]; // 從0開始,下到10應該是0-9.下面應該下載10.
    [request setValue:range forHTTPHeaderField:@"Range"];
    self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
    
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    
    
    
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
    
    NSLog(@"下載出錯");
    
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    
    if (_currentLength) {
        return;
    }
    
    // 先創建一個空文件
    NSFileManager *mgr = [NSFileManager defaultManager];

    NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"download.dmg"];
    [mgr createFileAtPath:path contents:nil attributes:nil];
    
    // 使用文件句柄操作文件
    NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:path];
    _handle = handle;
    _totalLength = response.expectedContentLength;
    _currentLength = 0;
    self.downloadBar.progress = 0;
    
    
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
    
    // 下載拼接思路:先創建空文件,然後一部分一部分的拼接
    [self.handle seekToEndOfFile];
    [self.handle writeData:data];
    
    self.currentLength += data.length;
    self.downloadBar.progress = (float)self.currentLength / self.totalLength;
    NSLog(@"進度:%f",self.downloadBar.progress);;
    
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
    
    [self.handle closeFile];
    [self.btn setTitle:@"開始" forState:UIControlStateNormal];
    self.currentLength = 0;
    
}

- (IBAction)btnClick:(UIButton *)sender {
    
    if ([sender.titleLabel.text isEqualToString:@"開始"]) {
        [sender setTitle:@"暫停" forState:UIControlStateNormal];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self startDownload];
        });
    }else{
        [sender setTitle:@"開始" forState:UIControlStateNormal];
        [self.connection cancel];
        self.connection = nil;
    }
    
    
}

@end

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章