iOS之[文件下載 / 大文件下載 / 斷點下載]

1.NSData(小文件下載)

[NSData dataWithContentsOfURL:] 就是一種文件下載方式,Get請求objective-c

可是這種下載方式須要放到子線程中服務器

NSURL *url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/test.png"];
    NSData *data = [NSData dataWithContentsOfURL:url];

2.NSURLConnection

2.1小文件下載(NSURLConnection)

經過NSURLConnection發送一個異步的Get請求,一次性將整個文件返回網絡

NSURL* url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/test.png"];

    [NSURLConnection 
        sendAsynchronousRequest:[NSURLRequest requestWithURL:url] 
                          queue:[NSOperationQueue mainQueue] 
              completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {

                    //atomically:原子性
                    [data writeToFile:filePath atomically:YES];      
                }];
2.2 大文件下載(NSURLConnection)

大文件下載不能一次性返回整個文件,不然會形成內存泄漏,系統崩潰session

// 發送請求去下載 (建立完conn對象後,會自動發起一個異步請求)多線程

IOS9已經廢棄:[NSURLConnection connectionWithRequest:request delegate:self];app

須要實現NSURLConnectionDataDelegate 協議,經常使用的幾個以下:異步

/**
 *  請求失敗時調用(請求超時、網絡異常)
 */
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

/**
 *  1.接收到服務器的響應就會調用
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;

/**
 *  2.當接收到服務器返回的實體數據時調用(具體內容,這個方法可能會被調用屢次)
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;

/**
 *  3.加載完畢後調用(服務器的數據已經徹底返回後)
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;

didReceiveData方法會被頻繁的調用,每次都會傳回來一部分data性能

最終咱們把每次傳回來的數據合併成一個咱們須要的文件。atom

一般合併文件是定義一個全局的NSMutableData,經過[mutableData appendData:data];來合併,最後將Data寫入沙盒url

代碼以下:

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

@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, strong) NSURLResponse *response;

@property (nonatomic, strong) NSMutableData *fileData;

@property (nonatomic, assign) long long fileLength; //>>文件長度
@property (nonatomic, strong) NSString *fileName;   //>>文件名
- (void)viewDidLoad {
    [super viewDidLoad];
    
    _request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/objective-c.pdf"]];
    //IOS9.0已經被廢棄
    [NSURLConnection connectionWithRequest:_request delegate:self];
}

/**
 *接收到服務器的響應
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{

    self.response = response;
    self.fileData = [NSMutableData data];
    
    //獲取下載文件大小
    self.fileLength = response.expectedContentLength;
    //獲取文件名
    self.fileName = response.suggestedFilename;
}

/**
 *接收到服務器返回的數據(可能被調用屢次)
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{

    [self.fileData appendData:data];
    
    //更新畫面中的進度
    double progress = (double)self.fileData.length/self.fileLength;
    self.progressView.progress = progress;
}

/**
 *服務器返回數據完了
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{

    NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [cache stringByAppendingPathComponent:_fileName];
    
    [self.fileData writeToFile:filePath atomically:YES];
}

可是有個致命的問題,內存!用來接受文件的NSMutableData一直都在內存中,會隨着文件的下載一直變大,

內存

合理的方式在咱們獲取一部分data的時候就寫入沙盒中,而後釋放內存中的data

要用到NSFilehandle這個類,這個類能夠實現對文件的讀取、寫入、更新

每次接收到數據的時候就拼接文件後面,經過- (unsigned long long)seekToEndOfFile;方法

/**
 *接收到服務器的響應
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    // 文件路徑
    NSString* caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString* filepath = [ceches stringByAppendingPathComponent:response.suggestedFilename];

    // 建立一個空的文件到沙盒中
    NSFileManager* fileManager = [NSFileManager defaultManager];
    [fileManager createFileAtPath:filepath contents:nil attributes:nil];

    // 建立一個用來寫數據的文件句柄對象:用來填充數據
    self.writeHandle = [NSFileHandle fileHandleForWritingAtPath:filepath];

    // 得到文件的總大小
    self.fileLength = response.expectedContentLength;

}
/**
 *  2.當接收到服務器返回的實體數據時調用(具體內容,這個方法可能會被調用屢次)
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    // 移動到文件的最後面
    [self.writeHandle seekToEndOfFile];

    // 將數據寫入沙盒
    [self.writeHandle writeData:data];

    // 累計寫入文件的長度
    self.currentLength += data.length;

    // 下載進度
    self.pregressView.progress = (double)self.currentLength / self.fileLength;
}
/**
 *  3.加載完畢後調用(服務器的數據已經徹底返回後)
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    self.currentLength = 0;
    self.fileLength = 0;

    // 關閉文件
    [self.writeHandle closeFile];
    self.writeHandle = nil;
}

下載過程當中內存就會一直很穩定了,而且下載的文件也是沒問題的

內存正常

2.3 斷點下載(NSURLConnection)

斷點續傳的response狀態碼爲206

暫停/繼續下載也是如今下載中必備的功能

NSURLConnection 只提供了一個cancel方法,這並非暫停,而是取消下載任務。若是要實現斷點下載必需要了解HTTP協議中請求頭的Range

經過設置請求頭的Range咱們能夠指定下載的位置、大小

Range實例:

bytes = 0-499           從0到499的頭500個字節

bytes = 500-999         從500到999的第二個500個字節

bytes = 500-            500以後的全部字節

bytes = -500            最後500個字節

bytes = 0-599,700-899   同時指定多個範圍

||

pragma mark --按鈕點擊事件,

- (IBAction)btnClicked:(UIButton *)sender {

    // 狀態取反
    sender.selected = !sender.isSelected;

    // 斷點下載

    if (sender.selected) { // 繼續(開始)下載
        // 1.URL
        NSURL *url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/Code.pdf"];

        // 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;
    }
}
2.4 斷點下載的所有代碼
@interface NSURLConnectionViewController ()<NSURLConnectionDataDelegate>

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

@property (nonatomic, strong) NSURL *url;
@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, strong) NSHTTPURLResponse *response;
@property (nonatomic, strong) NSURLConnection *connection;

@property (nonatomic, strong) NSFileHandle *fileHandle;

@property (nonatomic, assign) long long currentLength; //>>寫入文件的長度
@property (nonatomic, assign) long long fileLength; //>>文件長度
@property (nonatomic, strong) NSString *fileName;   //>>文件名

@end

@implementation NSURLConnectionViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"%@", NSHomeDirectory());
    self.url = [NSURL URLWithString:@"http://10.167.20.151:8080/AdminConsole/help/objective-c.pdf"];
}

#pragma mark - NSURLConnectionDataDelegate

/**
 *請求失敗
 */
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{

    NSLog(@"error");
}
/**
 *接收到服務器的響應
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{

    self.response = (NSHTTPURLResponse *)response;
    
    if (self.response.statusCode == 206) {//!!!斷點續傳的狀態碼爲206
        
        if (self.currentLength) {
            return;
        }
        
        //獲取下載文件大小
        self.fileLength = response.expectedContentLength;

        //獲取文件名
        self.fileName = response.suggestedFilename;
        
        //文件路徑
        NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];

        NSString *filePath = [caches stringByAppendingPathComponent:_fileName];
        
        //建立一個空的文件到沙盒
        NSFileManager *fileManager = [NSFileManager defaultManager];
        [fileManager createFileAtPath:filePath contents:nil attributes:nil];
        
        //建立一個用來寫數據的文件句柄
        self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
    }else{
    
        [self.connection cancel];
        self.connection = nil;
        NSLog(@"該文件不存在");
    }
    
}

/**
 *接收到服務器返回的數據(可能被調用屢次)
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
        
        //移動到文件末尾
        [self.fileHandle seekToEndOfFile];
        
        //寫入數據到文件
        [self.fileHandle writeData:data];
        
        self.currentLength += data.length;
        
        //更新畫面中的進度

        double progress = (double)self.currentLength/self.fileLength;
        self.progressView.progress = progress;
}

/**
 *服務器返回數據完了
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
     
        self.currentLength = 0;
        self.fileLength = 0;
        [self.fileHandle closeFile];
        self.fileHandle = nil;
}

- (IBAction)pauseDownLoad:(UIButton *)sender {

    //暫停<->開始轉換
    sender.selected = !sender.isSelected;
    
    if (sender.selected) {//開始下載
        
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url];
        
        //設置請求頭(GET)
        NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength];
        [request setValue:range forHTTPHeaderField:@"Range"];
        
        self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
    }else{ //暫停
    
        [self.connection cancel];
        self.connection = nil;
    }
}
@end

在下載過程當中,爲了提升效率,充分利用cpu性能,一般會執行多線程下載。。。待更新!!!

4. NSURLSession

生命週期的兩種方式-1:系統代理-Block方式(流程簡單-優先)

-2:指定代理類:delegate(流程複雜)

上面這種下載文件的方式確實比較複雜,要本身去控制內存寫入相應的位置

iOS7推出了一個新的類NSURLSession,它具有了NSURLConnection所具有的方法,同時也比它更強大

NSURLSession 也能夠發送Get/Post請求,實現文件的下載和上傳。

在NSURLSesiion中,任何請求均可以被看作是一個任務。其中有三種任務類型

NSURLSessionDataTask : 普通的GET\POST請求
    NSURLSessionDownloadTask : 文件下載
    NSURLSessionUploadTask : 文件上傳(不多用,通常服務器不支持)
4.1 NSURLSession 簡單使用

NSURLSession發送請求很是簡單,與connection不一樣的是,任務建立後不會自動發送請求,須要手動開始執行任務

// 1.獲得session對象
    NSURLSession* session = [NSURLSession sharedSession];
    NSURL* url = [NSURL URLWithString:@""];

    // 2.建立一個task,任務(GET)
    NSURLSessionDataTask* dataTask = 
             [session dataTaskWithURL:url 
                    completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

        // data 爲返回數據
    }];

    // 3.開始任務
    [dataTask resume];

POST請求:能夠自定義請求頭

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;
4.2 NSURLSession文件下載

使用NSURLSession就很是簡單了,不須要去考慮什麼邊下載邊寫入沙盒的問題,蘋果都幫咱們作好了

只用將下載好的文件經過NSFileManager剪切到指定位置

須要用到NSURLSession的子類:NSURLSessionDownloadTask

NSURL* url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/test.png"];

    // 獲得session對象
    NSURLSession* session = [NSURLSession sharedSession];

    // 建立任務
    NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {

        //將文件遷移到指定路徑下
        NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];

        NSString *file = [caches stringByAppendingPathComponent:response.suggestedFilename];

        // 將臨時文件剪切或者複製Caches文件夾
        NSFileManager *fileManager = [NSFileManager defaultManager];

        // AtPath : 剪切前的文件路徑
        // ToPath : 剪切後的文件路徑
        [fileManager moveItemAtPath:location.path toPath:file error:nil];
    }];

    // 開始任務
    [downloadTask resume];
  • location就是下載好的文件寫入沙盒的地址
  • 該方式沒法監聽下載進度
  • 若要監聽進度須要實現< NSURLSessionDownloadDelegate >協議,不能使用Block方式
/**
 *  下載完畢會調用
 */
- (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
{...}

/**
 *  恢復下載後調用
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{...}
4.2 NSURLSession斷點下載

:fa-font:1.暫停下載

resumeData,該參數包含了繼續下載文件的位置信息

resumeData只包含了url跟已經下載了多少數據,不會很大,不用擔憂內存問題

- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler;

!!!須要注意的是Block中循環引用的問題

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

:fa-bold:2.恢復下載

- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;
4.3 NSURLSession斷點下載全部代碼
@interface NSURLSessionController () <NSURLSessionDownloadDelegate>

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

//下載任務
@property (nonatomic, strong) NSURLSessionDownloadTask* downloadTask;

//resumeData記錄下載位置
@property (nonatomic, strong) NSData* resumeData;

@property (nonatomic, strong) NSURLSession* session;

@end

@implementation NSURLSessionController
/**
 *  session的懶加載
 */
- (NSURLSession *)session
{
    if (nil == _session) {
        
        NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration];
        self.session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}

- (void)viewDidLoad {
    [super viewDidLoad];
 
}
/**
 *  從0開始下載
 */
- (void)startDownload
{

    NSURL* url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/objective-c.pdf"];
    
    // 建立任務
    self.downloadTask = [self.session downloadTaskWithURL:url];

    // 開始任務
    [self.downloadTask resume];
}

/**
 *  恢復下載
 */
- (void)resume
{
     // 傳入上次暫停下載返回的數據,就能夠恢復下載
    self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
    
    [self.downloadTask resume]; // 開始任務
    
    self.resumeData = nil;
}

/**
 *  暫停
 */
- (void)pause
{
    __weak typeof(self) selfVc = self;
    [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
        //  resumeData : 包含了繼續下載的開始位置\下載的url
        selfVc.resumeData = resumeData;
        selfVc.downloadTask = nil;
    }];
}

#pragma mark -- NSURLSessionDownloadDelegate
/**
 *  下載完畢會調用
 *
 *  @param location     文件臨時地址
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
    //文件路徑
    NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString *file = [caches stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
    
    // 將臨時文件剪切或者複製Caches文件夾
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    // AtPath : 剪切前的文件路徑
    // ToPath : 剪切後的文件路徑
    [fileManager moveItemAtPath:location.path toPath:file error:nil];
    
    NSLog(@"下載完成");
}

/**
 *  每次寫入沙盒完畢調用
 *  在這裏面監聽下載進度,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.myPregress.progress = (double)totalBytesWritten/totalBytesExpectedToWrite;
}

/**
 *  恢復下載後調用,
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
    
}

#pragma mark --按鈕點擊事件

- (IBAction)btnClicked:(UIButton *)sender {
    
    // 按鈕狀態取反
    sender.selected = !sender.isSelected;
    
    if (nil == self.downloadTask) { // 開始(繼續)下載
        if (self.resumeData) { // 繼續下載
            [self resume];
        }else{ // 從0開始下載
            [self startDownload];
        }
        
    }else{ // 暫停
        [self pause];
    }
    
}
相關文章
相關標籤/搜索