iOS開發之網路程式設計--4、NSURLSessionDataTask實現檔案下載(離線斷點續傳下載)
前言:根據前篇《iOS開發之網路程式設計--2、NSURLSessionDownloadTask檔案下載》或者《iOS開發之網路程式設計--3、NSURLSessionDataTask實現檔案下載(離線斷點續傳下載)》,都遺留了一個細節未處理的問題,那就是在離線斷點下載的過程中,當應用程式重新啟動之後,進度條的進度值預設沒有設定為之前已經下載的進度,根據基本公式"當前進度值 = 已經下載的資料長度 ÷ 最終下載完的資料總長度",已經下載的資料長度可以由沙盒中已經下載的那部分資料獲取,但是最終下載完的資料總長度就需要通過網路返回的資訊了,但是別忘了,每一次重新啟動應用程式初始狀態預設都是暫停下載,或者是斷網的情況下無法請求網路資料,那麼如何獲取這個"最終下載完的資料總長度"呢?
本篇還涉及到在子執行緒建立下載任務,然後通過執行緒通知UI主執行緒更新進度條控制元件顯示進度。因為delegateQueue這個屬性可以設定主佇列執行緒或者是子佇列執行緒。
先看看效果:
問題解決:"最終下載完的資料總長度"可以在首次從0開始下載的時候通過網路獲取,然後將其"最終下載完的資料總長度"這個值儲存在快取中的某個檔案(這個檔案可以是字典等等)中,等待下一次獲取。
而我則採用的方法是將這個"最終下載完的資料總長度"作為檔案的屬性新增進檔案屬性列表中,以備下一次讀取的時候,獲得到這個檔案之後,就可以讀取該檔案的屬性列表中的"最終下載完
的資料總長度"的屬性和屬性值。
在這裡不得不先介紹一個工具類,讀者可以通過本人的另一篇博文隨筆先了解其功能:iOS開發 -- 為本地檔案新增自定義屬性的工具類
為本地檔案新增屬性之後,可以列印看的到:
本人花了點時間將網路下載這部分簡單的封裝成了一個工具類:DownloadTool,下面就展示原始碼:
DownloadTool.h
1 #import <Foundation/Foundation.h> 2 3 4 // 定義一個block用來傳遞進度值 5 typedef void (^SetProgressValue)(float progressValue); 6 7 @interfaceDownloadTool : NSObject 8 9 /** 建立下載工具物件 */ 10 + (instancetype)DownloadWithURLString:(NSString*)urlString setProgressValue:(SetProgressValue)setProgressValue; 11 /** 開始下載 */ 12 -(void)startDownload; 13 /** 暫停下載 */ 14 -(void)suspendDownload; 15 16 @end
DownloadTool.m
1 #import "DownloadTool.h" 2 3 #import "ExpendFileAttributes.h" 4 5 #define Key_FileTotalSize @"Key_FileTotalSize" 6 7 @interface DownloadTool () <NSURLSessionDataDelegate> 8 /** Session會話 */ 9 @property (nonatomic,strong)NSURLSession *session; 10 /** Task任務 */ 11 @property (nonatomic,strong)NSURLSessionDataTask *task; 12 /** 檔案的全路徑 */ 13 @property (nonatomic,strong)NSString *fileFullPath; 14 /** 傳遞進度值的block */ 15 @property (nonatomic,copy) SetProgressValue setProgressValue; 16 /** 當前已經下載的檔案的長度 */ 17 @property (nonatomic,assign)NSInteger currentFileSize; 18 /** 輸出流 */ 19 @property (nonatomic,strong)NSOutputStream *outputStream; 20 /** 不變的檔案總長度 */ 21 @property (nonatomic,assign)NSInteger fileTotalSize; 22 @end 23 24 @implementation DownloadTool 25 26 + (instancetype)DownloadWithURLString:(NSString*)urlString setProgressValue:(SetProgressValue)setProgressValue{ 27 DownloadTool* download = [[DownloadTool alloc] init]; 28 download.setProgressValue = setProgressValue; 29 [download getFileSizeWithURLString:urlString]; 30 [download creatDownloadSessionTaskWithURLString:urlString]; 31 NSLog(@"%@",download.fileFullPath); 32 return download; 33 } 34 // 剛建立該網路下載工具類的時候,就需要查詢本地是否有已經下載的檔案,並返回該檔案已經下載的長度 35 -(void)getFileSizeWithURLString:(NSString*)urlString{ 36 // 建立檔案管理者 37 NSFileManager* fileManager = [NSFileManager defaultManager]; 38 // 獲取檔案各個部分 39 NSArray* fileComponents = [fileManager componentsToDisplayForPath:urlString]; 40 // 獲取下載之後的檔名 41 NSString* fileName = [fileComponents lastObject]; 42 // 根據檔名拼接沙盒全路徑 43 NSString* fileFullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:fileName]; 44 self.fileFullPath = fileFullPath; 45 46 NSDictionary* attributes = [fileManager attributesOfItemAtPath:fileFullPath 47 error:nil]; 48 // 如果有該檔案,且為下載沒完成,就直接拿出該檔案的長度設定進度值,並設定當前的檔案長度 49 NSInteger fileCurrentSize = [attributes[@"NSFileSize"] integerValue]; 50 // 如果檔案長度為0,就不需要計算進度值了 51 if (fileCurrentSize != 0) { 52 // 獲取最終的檔案中長度 53 NSInteger fileTotalSize = [[ExpendFileAttributes stringValueWithPath:self.fileFullPath key:Key_FileTotalSize] integerValue]; 54 self.currentFileSize = fileCurrentSize; 55 self.fileTotalSize = fileTotalSize; 56 // 設定進度條的值 57 self.setProgressValue(1.0 * fileCurrentSize / fileTotalSize); 58 } 59 NSLog(@"當前檔案長度:%lf" , self.currentFileSize * 1.0); 60 } 61 #pragma mark - 建立網路請求會話和任務,並啟動任務 62 -(void)creatDownloadSessionTaskWithURLString:(NSString*)urlString{ 63 //判斷檔案是否已經下載完畢 64 if (self.currentFileSize == self.fileTotalSize && self.currentFileSize != 0) { 65 NSLog(@"檔案已經下載完畢"); 66 return; 67 } 68 NSURLSession* session = 69 [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] 70 delegate:self 71 delegateQueue:[[NSOperationQueue alloc]init]]; 72 NSURL* url = [NSURL URLWithString:urlString]; 73 NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url]; 74 //2.3 設定請求頭 75 NSString *range = [NSString stringWithFormat:@"bytes=%zd-",self.currentFileSize]; 76 [request setValue:range forHTTPHeaderField:@"Range"]; 77 NSURLSessionDataTask* task = [session dataTaskWithRequest:request]; 78 self.session = session; 79 self.task = task; 80 } 81 82 #pragma mark - 控制下載的狀態 83 // 開始下載 84 -(void)startDownload{ 85 [self.task resume]; 86 } 87 // 暫停下載 88 -(void)suspendDownload{ 89 [self.task suspend]; 90 } 91 #pragma mark - NSURLSessionDataDelegate 的代理方法 92 // 收到響應呼叫的代理方法 93 -(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse: 94 (NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler{ 95 NSLog(@"執行了收到響應呼叫的代理方法"); 96 // 建立輸出流,並開啟流 97 NSOutputStream* outputStream = [[NSOutputStream alloc] initToFileAtPath:self.fileFullPath append:YES]; 98 [outputStream open]; 99 self.outputStream = outputStream; 100 // 如果當前已經下載的檔案長度等於0,那麼就需要將總長度資訊寫入檔案中 101 if (self.currentFileSize == 0) { 102 NSInteger totalSize = response.expectedContentLength; 103 NSString* totalSizeString = [NSString stringWithFormat:@"%ld",totalSize]; 104 [ExpendFileAttributes extendedStringValueWithPath:self.fileFullPath key:Key_FileTotalSize value:totalSizeString]; 105 // 別忘了設定總長度 106 self.fileTotalSize = totalSize; 107 } 108 // 允許收到響應 109 completionHandler(NSURLSessionResponseAllow); 110 } 111 // 收到資料呼叫的代理方法 112 -(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{ 113 NSLog(@"執行了收到資料呼叫的代理方法"); 114 // 通過輸出流寫入資料 115 [self.outputStream write:data.bytes maxLength:data.length]; 116 // 將寫入的資料的長度計算加進當前的已經下載的資料長度 117 self.currentFileSize += data.length; 118 // 設定進度值 119 NSLog(@"當前檔案長度:%lf,總長度:%lf",self.currentFileSize * 1.0,self.fileTotalSize * 1.0); 120 NSLog(@"進度值: %lf",self.currentFileSize * 1.0 / self.fileTotalSize); 121 // 獲取主執行緒 122 NSOperationQueue* mainQueue = [NSOperationQueue mainQueue]; 123 [mainQueue addOperationWithBlock:^{ 124 self.setProgressValue(self.currentFileSize * 1.0 / self.fileTotalSize); 125 }]; 126 } 127 // 資料下載完成呼叫的方法 128 -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{ 129 // 關閉輸出流 並關閉強指標 130 [self.outputStream close]; 131 self.outputStream = nil; 132 // 關閉會話 133 [self.session invalidateAndCancel]; 134 NSLog(@"%@",[NSThread currentThread]); 135 } 136 -(void)dealloc{ 137 } 138 @end
使用示例原始碼:
1 #import "ViewController.h" 2 #import "RainbowProgress.h" 3 4 #import "DownloadTool.h" 5 6 #define MP4_URL_String @"http://120.25.226.186:32812/resources/videos/minion_12.mp4" 7 8 9 @interface ViewController () 10 @property (weak, nonatomic) IBOutlet UILabel *showDownloadState; 11 /** 彩虹進度條 */ 12 @property (nonatomic,weak)RainbowProgress *rainbowProgress; 13 /** 網路下載工具物件 */ 14 @property (nonatomic,strong)DownloadTool *download; 15 @end 16 17 @implementation ViewController 18 19 - (void)viewDidLoad { 20 [super viewDidLoad]; 21 [self setSelfView]; 22 [self addProgress]; 23 [self addDownload]; 24 25 } 26 // 啟動和關閉的網路下載開關 27 - (IBAction)SwitchBtn:(UISwitch *)sender { 28 if (sender.isOn) { 29 self.showDownloadState.text = @"開始下載"; 30 [self.download startDownload]; 31 }else{ 32 self.showDownloadState.text = @"暫停下載"; 33 [self.download suspendDownload]; 34 } 35 } 36 #pragma mark - 設定控制器View 37 -(void)setSelfView{ 38 self.view.backgroundColor = [UIColor blackColor]; 39 } 40 #pragma mark - 新增彩虹進度條 41 -(void)addProgress{ 42 // 建立彩虹進度條,並啟動動畫 43 RainbowProgress* rainbowProgress = [[RainbowProgress alloc] init]; 44 [rainbowProgress startAnimating]; 45 [self.view addSubview:rainbowProgress]; 46 self.rainbowProgress = rainbowProgress; 47 } 48 #pragma mark - 建立網路下載任務 49 -(void)addDownload{ 50 DownloadTool* download = [DownloadTool DownloadWithURLString:MP4_URL_String setProgressValue:^(float progressValue) { 51 self.rainbowProgress.progressValue = progressValue; 52 }]; 53 self.download = download; 54 } 55 56 #pragma mark - 設定狀態列樣式 57 -(UIStatusBarStyle)preferredStatusBarStyle{ 58 return UIStatusBarStyleLightContent; 59 } 60 61 @end
效果圖(中間有個過程是重新啟動應用程式看看進度條顯示的效果,然後繼續測試開始下載和暫停下載):