總體內(nèi)容
- 在學(xué)習(xí)下面的知識前請在 mac電腦搭建一下 Apache 服務(wù)器
- 1、NSURLConncetion 下載
- 2、NSURLSession下載大文件
- 3、下載管理器(多文件下載)
一、NSURLConncetion 下載
-
1.1、我們先使用NSURLConncetion 下載一個視頻試試,完整代碼在demo中的
Test1ViewController視頻連接:
@"http://images.ciotimes.com/2ittt-zm.mp4"-
<1>、對視頻鏈接進行編碼
在iOS程序中,訪問一些http/https的資源服務(wù)時,如果url中存在中文或者特殊字符時,會導(dǎo)致無法正常的訪問到資源或服務(wù),想要解決這個問題,需要對url進行編碼。NSString *urlStr = @"http://images.ciotimes.com/2ittt-zm.mp4"; // 在 iOS9 之后廢棄了 // urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; // iOS9 之后取代上面的 api urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; -
<2>、string 轉(zhuǎn) NSURL
NSURL *url = [NSURL URLWithString:urlStr]; -
<3>、創(chuàng)建 NSURLRequest 對象
NSURLRequest *request = [NSURLRequest requestWithURL:url]; -
<4>、NSURLConnection 下載 視頻
// iOS9 之后廢棄了 [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) { // 下載視頻的名字 NSString *videoName = [urlStr lastPathComponent]; // 下載到桌面的文件夾 JK視頻下載器 NSString *downPath = [NSString stringWithFormat:@"/Users/wangchong/Desktop/JK視頻下載器/%@",videoName]; // 將數(shù)據(jù)寫入到硬盤 [data writeToFile:downPath atomically:YES]; NSLog(@"下載完成"); }];提示:
NSURLConnection在iOS 2.0之后就有了,sendAsynchronousRequest這個方法是在iOS5.0之后出現(xiàn)的 -
完整的代碼
// 1、對視頻鏈接進行編碼 NSString *urlStr = @"http://images.ciotimes.com/2ittt-zm.mp4"; // iOS9 之后的 api urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; // 2、string 轉(zhuǎn) NSURL NSURL *url = [NSURL URLWithString:urlStr]; // 3、創(chuàng)建 NSURLRequest 對象 NSURLRequest *request = [NSURLRequest requestWithURL:url]; // 4、NSURLConnection 下載 視頻 // iOS9 之后廢棄了 [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) { // 下載視頻的名字 NSString *videoName = [urlStr lastPathComponent]; // 下載到桌面的文件夾 JK視頻下載器 NSString *downPath = [NSString stringWithFormat:@"/Users/wangchong/Desktop/JK視頻下載器/%@",videoName]; // 將數(shù)據(jù)寫入到硬盤 [data writeToFile:downPath atomically:YES]; NSLog(@"下載完成"); }]; -
上面下載會出現(xiàn)的兩個問題:
- (1)、沒有下載進度,會影響用戶的體驗
-
(2)、內(nèi)存偏高,會有最大峰值(一次性把數(shù)據(jù)寫入),內(nèi)存隱患
7651548052881_.pic_hd.jpg
7661548052902_.pic_hd.jpg
-
-
1.2、NSURLConnection 進度監(jiān)聽,完整代碼在demo中的
Test2ViewController-
(1)、在
1.1里面我們使用的是NSURLConnection的block方法進行的下載,會有下載沒有進度和出現(xiàn)峰值的問題,那么下面我們就使用NSURLConnection的代理方法來解決這些問題- 下載沒有進度的解決辦法:通過代理來解決
- 進度跟進:在響應(yīng)頭中獲取文件的總大小,在每次接收數(shù)據(jù)的時候計算數(shù)據(jù)的比例
-
(2)、代理方法選擇
NSURLConnectionDataDelegate,其他兩個的NSURLConnection代理方法都是不對的
代理方法選擇 `NSURLConnectionDataDelegate` -
(3)、定義一個記錄總視頻大小的屬性和接收到的數(shù)據(jù)包或者下載的數(shù)據(jù)總大小
/** 要下載的文件總大小 */ @property(nonatomic,assign) long long exceptedContentLength; /** 當(dāng)前已經(jīng)下載的文件總大小 */ @property(nonatomic,assign) long long currentDownContentLength;提示:類型要選擇
long long,系統(tǒng)使用的就是這個類型 -
(4)、常用的代理方法
// 1、接收服務(wù)器的響應(yīng) --- 狀態(tài)和響應(yīng)頭做一些準(zhǔn)備工作 // expectedContentLength : 文件的總大小 // suggestedFilename : 服務(wù)器建議保存的名字 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{ // 記錄文件的總大小 self.exceptedContentLength = response.expectedContentLength; // 當(dāng)前下載的文件大小初始化為 0 self.currentDownContentLength = 0; NSLog(@"\nURL=%@\nMIMEType=%@\ntextEncodingName=%@\nsuggestedFilename=%@",response.URL,response.MIMEType,response.textEncodingName,response.suggestedFilename); } // 2、接收服務(wù)器的數(shù)據(jù),由于數(shù)據(jù)是分塊發(fā)送的,這個代理方法會被執(zhí)行多次,因此我們也會拿到多個data - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{ NSLog(@"接收到的數(shù)據(jù)長度=%tu",data.length); // 計算百分比 // progress = (float)long long / long long float progress = (float)self.currentDownContentLength/self.exceptedContentLength; JKLog(@"下載的進度=%f",progress); } // 3、接收到所有的數(shù)據(jù)加載完畢后會有一個通知 - (void)connectionDidFinishLoading:(NSURLConnection *)connection{ NSLog(@"下載完畢"); } // 4、下載錯誤的處理 -(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{ NSLog(@"鏈接失敗"); }提示:計算百分比
progress = (float)long long / long long;要記得轉(zhuǎn)換類型,兩個整數(shù)相除的結(jié)果是不會有小數(shù)的,轉(zhuǎn)成float就好
-
-
1.3、拼接數(shù)據(jù)然后寫入磁盤(不可取,比
1.1更嚴(yán)重),完整代碼在demo中的Test3ViewController由于在
1.1中出現(xiàn)的 峰值 問題,在這里來解決一下,兩種方式嘗試
第一種: 從服務(wù)器獲取完 數(shù)據(jù)包 data 后一次性寫入磁盤
第二種:獲取一個數(shù)據(jù)包就寫入一次磁盤-
(1)、定義視頻下載到的路徑以及數(shù)據(jù)的data
/** 保存下載視頻的路徑 */ @property(nonatomic,strong) NSString *downFilePath; /** 保存視頻數(shù)據(jù) */ @property(nonatomic,strong) NSMutableData *fileData; -(NSMutableData *)fileData{ if (!_fileData) { _fileData = [[NSMutableData alloc]init]; } return _fileData; } -
(2)、代理中的方法
// 1、接收服務(wù)器的響應(yīng) --- 狀態(tài)和響應(yīng)頭做一些準(zhǔn)備工作 // expectedContentLength : 文件的總大小 // suggestedFilename : 服務(wù)器建議保存的名字 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{ // 記錄文件的總大小 self.exceptedContentLength = response.expectedContentLength; // 當(dāng)前下載的文件大小初始化為 0 self.currentDownContentLength = 0; // 創(chuàng)建下載的路徑 self.downFilePath = [@"/Users/wangchong/Desktop/JK視頻下載器" stringByAppendingPathComponent:response.suggestedFilename]; } // 2、接收服務(wù)器的數(shù)據(jù),由于數(shù)據(jù)是分塊發(fā)送的,這個代理方法會被執(zhí)行多次,因此我們也會拿到多個data - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{ // JKLog(@"接收到的數(shù)據(jù)長度=%tu",data.length); self.currentDownContentLength += data.length; // 計算百分比 // progress = (float)long long / long long float progress = (float)self.currentDownContentLength/self.exceptedContentLength; JKLog(@"下載的進度=%f",progress); self.progressLabel.text = [NSString stringWithFormat:@"下載進度:%f",progress]; // 拼接每次獲取到服務(wù)器的數(shù)據(jù)包 data [self.fileData appendData:data]; } // 3、接收到所有的數(shù)據(jù)加載完畢后會有一個通知 - (void)connectionDidFinishLoading:(NSURLConnection *)connection{ JKLog(@"下載完畢"); // 數(shù)據(jù)獲取完,寫入磁盤 [self.fileData writeToFile:self.downFilePath atomically:YES]; } // 4、下載錯誤的處理 -(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{ JKLog(@"鏈接失敗"); }
分析:
第一種: 從服務(wù)器獲取完 數(shù)據(jù)包 data 后一次性寫入磁盤的問題:不僅僅會出現(xiàn)峰值的問題,由于fileData是強引用無法釋放,會造成內(nèi)存暴增,由此可以看出和1.1中的異步效果一樣,應(yīng)該是蘋果底層的實現(xiàn)方式
內(nèi)存暴增 -
1.4、NSFileHandle數(shù)據(jù)包邊下載邊寫入磁盤,完整代碼在demo中的
Test4ViewController-
提起
NSFileHandle,我們老解釋一下它與NSFileManager的區(qū)別-
NSFileManager:主要的功能是創(chuàng)建目錄、檢查目錄是否存在、遍歷目錄、刪除文件
拷貝文件、剪切文件等等,主要是針對文件的操作 -
NSFileHandle:文件"句柄",對文件的操作,主要功能是:對同一個文件進行二進制讀寫
-
-
(1)、我們寫一個寫入數(shù)據(jù)的方法,如下
// 把數(shù)據(jù)寫入到磁盤的方法 -(void)writeFileData:(NSData *)data{ NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:self.downFilePath]; // 如果文件不存在,直接x將數(shù)據(jù)寫入磁盤 if (fp == nil) { [data writeToFile:self.downFilePath atomically:YES]; }else{ // 如果存在,將data追加到現(xiàn)在文件的末尾 [fp seekToEndOfFile]; // 寫入文件 [fp writeData:data]; // 關(guān)閉文件 [fp closeFile]; } }提示:通過測試,邊下載邊寫入磁盤解決了峰值的問題
-
(2)、如何判斷文件是否下載完成 ?
答:判斷進度?判斷完成通知?,判斷時間?判斷大小?這些都不太好,比較好的方式是使用MD5
MD5:- <1>.服務(wù)器對你下載的文件計算好一個MD5,將此 MD5 傳給客戶端
- <2>.開始下載文件......
- <3>.下載完成時,對下載的文件做一次MD5
- <4>.比較服務(wù)器返回的MD5和我們自己計算的MD5,如果二者相等,就代表下載完成
-
-
1.5、
NSOutputStream拼接文件,完整代碼在demo中的Test5ViewController-
(1)、創(chuàng)建一個保存文件的輸出流
NSOutputStream屬性/* 保存文件的輸出流 - (void)open; 寫入之前,打開流 - (void)close; 寫入完畢之后,關(guān)閉流 */ @property(nonatomic,strong)NSOutputStream *fileStream; -
(2)、創(chuàng)建輸出流并打開
// 創(chuàng)建輸出流 self.fileStream = [[NSOutputStream alloc]initToFileAtPath:self.downFilePath append:YES]; [self.fileStream open]; -
(3)、將數(shù)據(jù)拼接起來,并判斷是否可寫如,一般情況下可寫入,除非磁盤空間不足
// 判斷是否有空間可寫 if ([self.fileStream hasSpaceAvailable]) { [self.fileStream write:data.bytes maxLength:data.length]; } -
(4)、關(guān)閉文件流
接收到所有的數(shù)據(jù)加載完畢后會有一個通知 - (void)connectionDidFinishLoading:(NSURLConnection *)connection{ NSLog(@"下載完畢"); [self.fileStream close]; } -
(5)、把NSURLConncetion放到子線程,但是雖然寫入的操作是在子線程,但是默認(rèn)的connection 是在主線程工作,指定了代理的工作的隊列之后,整個下載還是在主線程 。UI事件能夠卡住下載
NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:self]; // 設(shè)置代理工作的操作 [[NSOperationQueue alloc]init] 默認(rèn)創(chuàng)建一個異步并發(fā)隊列 [connection setDelegateQueue:[[NSOperationQueue alloc]init]]; [connection start];
-
-
1.6、解決1.5中 NSURLConncetion的下載在主線程的問題,完整代碼在demo中的
Test6ViewController-
(1)、將網(wǎng)絡(luò)操作放在異步線程,異步的運行循環(huán)默認(rèn)不啟動,沒有辦法監(jiān)聽接下來的網(wǎng)絡(luò)事件
dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 1、對視頻鏈接進行編碼 // 在iOS程序中,訪問一些HTTP/HTTPS的資源服務(wù)時,如果url中存在中文或者特殊字符時,會導(dǎo)致無法正常的訪問到資源或服務(wù),想要解決這個問題,需要對url進行編碼。 NSString *urlStr = @"http://images.ciotimes.com/2ittt-zm.mp4"; urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; // 2、string 轉(zhuǎn) NSURL NSURL *url = [NSURL URLWithString:urlStr]; // 3、創(chuàng)建 NSURLRequest 對象 NSURLRequest *request = [NSURLRequest requestWithURL:url]; // 4、NSURLConnection 下載 視頻 /** By default, for the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode. 為了保證鏈接工作正常,調(diào)用線程的RunLoop,必須在默認(rèn)的運行循環(huán)模式下 */ NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:self]; // 設(shè)置代理工作的操作 [[NSOperationQueue alloc]init] 默認(rèn)創(chuàng)建一個異步并發(fā)隊列 [connection setDelegateQueue:[[NSOperationQueue alloc]init]]; [connection start]; });分析:上面的代碼是有很大的問題的,子線程執(zhí)行完后會直接死掉,不會繼續(xù)執(zhí)行 start 后面的操作,也就是說沒有辦法下載;解決辦法是給子線程創(chuàng)建 Runloop
-
(2)、定義一個保存下載線程的運行循環(huán)
@property(nonatomic,assign)CFRunLoopRef downloadRunloop; -
(3)、在
[connection start];之后啟動我們創(chuàng)建的子線程可以活下去的Runloop/* CoreFoundation 框架 CFRunLoop CFRunloopStop() 停止指定的runloop CFRunloopGetCurrent() 獲取當(dāng)前的Runloop CFRunloopRun() 直接啟動當(dāng)前的運行循環(huán) */ //1、拿到當(dāng)前的運行循環(huán) self.downloadRunloop = CFRunLoopGetCurrent(); //2.啟動當(dāng)前的運行循環(huán) CFRunLoopRun(); -
(4)、在下載完成后停止下載線程所在的runloop
// 所有數(shù)據(jù)加載完畢--所有數(shù)據(jù)加載完畢,會一個通知! - (void)connectionDidFinishLoading:(NSURLConnection *)connection { NSLog(@"完畢!%@",[NSThread currentThread]); //關(guān)閉文件流 [self.fileStream close]; //停止下載線程所在的runloop CFRunLoopStop(self.downloadRunloop); }
-
二、NSURLSession 下載大文件,以下測試我們使用Apache 服務(wù)器里面的數(shù)據(jù)

-
2.1、NSURLSession 簡介 以及 簡單使用,完整代碼在JKNSURLSession中的
Test1ViewController
NSURLSession是在iOS 7.0(15年)的時候推出的,在最開始的時候也會出現(xiàn)峰值,后來解決后大家才重新使用NSURLSession,NSURLSession所有的任務(wù)都是session發(fā)起的,默認(rèn)所有任務(wù)都是“掛起”的,需要resume執(zhí)行。-
簡單的使用:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // 1.創(chuàng)建url: http://localhost/test.json NSURL *url = [NSURL URLWithString:@"http://localhost/test.json"]; [self taskWithUrl:url]; } -(void)taskWithUrl:(NSURL *)url{ [[[NSURLSession sharedSession]dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { // 反序列化 id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; NSLog(@"%@",result); }] resume]; }提示:
NSURLSession是一個單利,目的是使開發(fā)更容易,默認(rèn)是不啟動的,需要開發(fā)者調(diào)用resume啟動NSURLSession,如上面
-
-
2.2、NSURLSession 簡單的下載,完整代碼在JKNSURLSession中的
Test2ViewController-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // 1.創(chuàng)建url:http://localhost NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"]; [self taskWithUrl:url]; } -(void)taskWithUrl:(NSURL *)url{ /* 如果在回調(diào)方法中,不做任何處理,下載的文件會被刪除 下載是默認(rèn)下載到tmp文件夾,系統(tǒng)會自動回收這個區(qū)域 設(shè)計目的: 1.通常從網(wǎng)絡(luò)下載文件,什么樣的格式文件最多?zip 2.如果是zip包,下載之后要解壓 3.解壓之后,原始的zip就不需要了。系統(tǒng)會自動幫你刪除 */ [[[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) { NSLog(@"%@",location); }]resume]; }提示:location打印是:
file:///Users/wangchong/Library/Developer/CoreSimulator/Devices/643379A0-0449-4FE2-AD19-71258BDDBAE6/data/Containers/Data/Application/E6F1AABA-BDBE-4191-A167-02D5DCD19D41/tmp/CFNetworkDownload_OaisFm.tmp,我們可以看到 tmp,臨時存放下載文件的地方 -
2.3、文件解壓縮,完整代碼在JKNSURLSession中的
Test3ViewController-
(1)、這里我們需要使用一個工具
SSZipArchive,在demo里面有
SSZipArchive
SSZipArchive的功能:壓縮文件和解壓文件 -
(2)、解壓我們服務(wù)器的一個文件到
Library/Caches里面-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // 1.創(chuàng)建url:http://localhost NSURL *url = [NSURL URLWithString:@"http://localhost/ftp.docx.zip"]; [self taskWithUrl:url]; } -(void)taskWithUrl:(NSURL *)url{ [[[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) { NSLog(@"%@",location); // 文件解壓目標(biāo)路徑,不能指定目標(biāo)文件。因為我們解壓文件會產(chǎn)生多個文件 NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)lastObject]; [SSZipArchive unzipFileAtPath:location.path toDestination:cachePath]; }]resume]; }解壓文件
-
-
2.4、NSURLSession下載進度監(jiān)聽,完整代碼在JKNSURLSession中的
Test4ViewController-
(1)、創(chuàng)建一個 NSURLSession對象
// 全局的網(wǎng)絡(luò)會話,管理所有的網(wǎng)絡(luò)任務(wù) @property(nonatomic,strong) NSURLSession *session; -(NSURLSession *)session{ if (!_session) { /** 全局網(wǎng)絡(luò)環(huán)境的一個配置 比如:身份驗證,瀏覽器類型以及緩存,超時,這些都會被記錄在 */ NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; } return _session; } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // 1.創(chuàng)建url:http://localhost NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"]; //如果你要監(jiān)聽下載進度,必須使用代理。 //如果你要更進下載進度,就不能block 。 [[self.session downloadTaskWithURL:url]resume]; }提示:
- 如果你要監(jiān)聽下載進度,必須使用代理。
-
[NSURLSession sharedSession]是全局的單例。整個系統(tǒng)都會用,也就是其他的應(yīng)用程序也會用 - 如果你要更進下載進度,就不能block 。
-
(2)、常用的代理方法(其中下載完成的方法是在iOS7.0之后必須要寫的,在iOS7之前,下面的三個方法都必須寫)
/** 1、下載完成的方法 */ -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{ NSLog(@"下載完成"); } /** 2、下載續(xù)傳數(shù)據(jù)的方法 */ -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{ } /** 3、下載進度的方法 @param session 網(wǎng)絡(luò)會話 @param downloadTask 調(diào)用代理方式的下載任務(wù) @param bytesWritten 本次下載的字節(jié)數(shù) @param totalBytesWritten 已經(jīng)下載的字節(jié)數(shù) @param totalBytesExpectedToWrite 期望下載的字節(jié)數(shù) -- 文件的總大小 */ -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{ float progress = (float)totalBytesWritten/totalBytesExpectedToWrite; NSLog(@"進度=%f",progress); }
-
-
2.5、自定義progressview,完整代碼在JKNSURLSession中的
Test5ViewController-
(1)、自定義progressview我們選擇繼承于 UIButton,原因是:button可以設(shè)置文字,展示的時候比較方便,當(dāng)然也可以使用其他的控件,比如lable,那么我們自定義一個類 JKProgressBtn 繼承于UIButton,代碼如下
-
JKProgressBtn.h里面的代碼
#import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface JKProgressBtn : UIButton /** 表示進度的值 */ @property(nonatomic,assign) float progress; @end NS_ASSUME_NONNULL_END -
JKProgressBtn.m里面的代碼
#import "JKProgressBtn.h" @implementation JKProgressBtn -(instancetype)initWithFrame:(CGRect)frame{ self = [super initWithFrame:frame]; if (self) { [self setTitleColor:[UIColor brownColor] forState:UIControlStateNormal]; } return self; } -(void)setProgress:(float)progress{ _progress = progress; // 進度的Label [self setTitle:[NSString stringWithFormat:@"%0.2f%%",_progress*100] forState:UIControlStateNormal]; // 刷新視圖 [self setNeedsDisplay]; } -(void)drawRect:(CGRect)rect{ CGSize s = rect.size; // 圓心 CGPoint center = CGPointMake(s.width*0.5, s.height*0.5); // 半徑 CGFloat r = (s.height > s.width) ? s.width*0.5:s.height*0.5; r = r - 5; // 其實角度 CGFloat startAngle = -M_PI_2; // 結(jié)束角度 CGFloat endAngle = self.progress*2*M_PI + startAngle; /** 第1個參數(shù):圓心 第2個參數(shù):半徑 第3個參數(shù):起始角度 第4個參數(shù):結(jié)束角度 第5個參數(shù):YES:順時針 / NO:逆時針 */ UIBezierPath *bezierPath = [UIBezierPath bezierPathWithArcCenter:center radius:r startAngle:startAngle endAngle:endAngle clockwise:YES]; // 圓環(huán)的寬度 bezierPath.lineWidth = 10.0; // 設(shè)置圓環(huán)的樣式(圓形) bezierPath.lineCapStyle = kCGLineCapRound; // 給圓環(huán)添加顏色 [[UIColor purpleColor]setStroke]; // 繪制路徑 [bezierPath stroke]; } @end提示:
UIBezierPath:貝塞爾曲線的起始角度是時鐘的3點,也就是數(shù)學(xué)上x的正軸方向,故上面我們把起始角度設(shè)置為-M_PI_2,也就是 時鐘的12點,同理數(shù)學(xué)上y的正軸方向,其他的參數(shù)在上面描述的很清楚- 解釋一下:
bezierPath.lineWidth = 10.0;,在貝塞爾曲線里面,半徑?jīng)Q定后,圓環(huán)的寬度是以半徑向外擴展的,所以才有上面的:r = r - 5;
- 解釋一下:
-
-
(2)、JKProgressBtn 的使用,在NSURLSession下載進度的方法里面刷新JKProgressBtn的進度,如下:
自定義下載進度圖-
先在控制器里面定義一個 JKProgressBtn屬性并初始化添加到控制器
// 進度的View @property(nonatomic,strong) JKProgressBtn *progressView; -(JKProgressBtn *)progressView{ if (!_progressView) { _progressView = [[JKProgressBtn alloc]initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-50, [UIScreen mainScreen].bounds.size.height/2-50, 100, 100)]; _progressView.backgroundColor = [UIColor yellowColor]; } return _progressView; } // 添加進度View [self.view addSubview:self.progressView]; -
在下載進度的方法里面設(shè)置主線程刷新
progressView的值/** 3、下載進度的方法 @param session 網(wǎng)絡(luò)會話 @param downloadTask 調(diào)用代理方式的下載任務(wù) @param bytesWritten 本次下載的字節(jié)數(shù) @param totalBytesWritten 已經(jīng)下載的字節(jié)數(shù) @param totalBytesExpectedToWrite 期望下載的字節(jié)數(shù) -- 文件的總大小 */ -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{ float progress = (float)totalBytesWritten/totalBytesExpectedToWrite; NSLog(@"進度=%f",progress); dispatch_async(dispatch_get_main_queue(), ^{ self.progressView.progress = progress; }); }提示:
NSURLSession創(chuàng)建的下載是在子線程執(zhí)行的,所以上面才在主線程刷新UI
-
-
-
2.6、斷點續(xù)傳,完整代碼在JKNSURLSession中的
Test6ViewController-
(1)、創(chuàng)建三個按鈕,開始下載、暫停下載,繼續(xù)下載
創(chuàng)建三個按鈕,**開始下載**、**暫停下載**,**繼續(xù)下載** -
(2)、創(chuàng)建一個全局的下載任務(wù)
/** 設(shè)置一個全局的下載任務(wù) */ @property(nonatomic,strong) NSURLSessionDownloadTask *downloadTask; -
(3)、開始下載、暫停下載,繼續(xù)下載 三個方法對應(yīng)的代碼如下
#pragma mark 開始下載 -(void)startLoadTask{ NSLog(@"開始下載"); // 1.創(chuàng)建url:http://localhost NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"]; self.downloadTask = [self.session downloadTaskWithURL:url]; // 2、執(zhí)行下載 [self.downloadTask resume]; } #pragma mark 暫停下載 -(void)pauseLoadTask{ NSLog(@"暫停下載"); [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) { self.resumeData = resumeData; //釋放任務(wù) self.downloadTask = nil; }]; } #pragma mark 繼續(xù)下載 -(void)resumeLoadTask{ NSLog(@"繼續(xù)下載"); // 防止繼續(xù)下載被執(zhí)行兩次,故下面把self.resumeData賦為nil if (self.resumeData == nil) return; self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData]; self.resumeData = nil; [self.downloadTask resume]; }提示:斷點續(xù)傳其實也就是在暫停下載的時候獲取下載的
resumeData,再次接著下載的時候,用resumeData再獲取一個NSURLSessionDownloadTask,從而接著下載
-
-
2.7、NSURLSession 強引用 問題
-
(1)、
NSURLSession是一個強引用,在下載完成的時候要進行釋放,不管是是否支持不在當(dāng)前界面下載,當(dāng)所有的下載任務(wù)都完成后,需要進行釋放 session,并賦nil,否則會造成內(nèi)存泄漏/** 1、下載完成的方法 */ -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{ NSLog(@"下載完成"); [self.session finishTasksAndInvalidate]; self.session = nil; } -
(2)、如果只支持當(dāng)前界面下載的情況,退出當(dāng)前界面,取消下載,session 賦nil
[self.session invalidateAndCancel]; self.session = nil;
-
2.8、完整代碼請看 JKNSURLSession 中的
Test6ViewController
三、下載管理器 demo地址:JKDownloaderKit
-
3.1、下載思路
-
(1)、創(chuàng)建另兩個文件夾:
JKDownloadCompleted(下載完成的文件夾)和JKDownLoading(下載中的文件夾),在下載中的資源會存在JKDownLoading中,下載完成后會移動到JKDownloadCompleted里面
創(chuàng)建另兩個文件夾 (2)、先查看服務(wù)器上的文件大小
-
(3)、查看本地是否存在文件,如果存在如下
- 如果文件小于服務(wù)器文件的大小,從本地文件長度開始下載(斷點續(xù)傳)
- 如果文件等于服務(wù)器文件的大小,再把文件生成一個MD5與服務(wù)器對文件返回的MD5做對比,如果一樣,代表下載完成
- 如果文件大于服務(wù)器文件的大小,發(fā)生錯誤,直接刪除文件,重新下載
(4)、如果本地不存在該文件,直接下載
- 上傳視頻的思路:
在上傳視頻的時候,如果視頻斷開了(程序退出了),那么就要去服務(wù)器請求看看自己之前上傳了多少,接著上傳就好,和視頻的下載原理是一樣的,對比 -
總體思路圖
下載與上傳的思路圖
-
(5)、在監(jiān)聽下載的方法中,當(dāng)下載完成后做如下的操作
- 在沒有
error的情況下,文件下載是完畢了,但是不一定成功,分析如下- 判斷, 本地緩存
==文件總大小 (如果不相等,說明下載有問題,刪除下載路徑下的文件,重新下載;如果相等在驗證文件內(nèi)容的MD5值是否一樣,一樣的話才是真正的下載完成,否則下載是有問題的,刪除下載路徑下的文件,重新下載)
- 判斷, 本地緩存
- 下載-文件完整性驗證機制:驗證文件的合法性, 下載數(shù)據(jù)是否完整可用
- 服務(wù)器返回文件下載信息的同時, 會返回該文件內(nèi)容的md5值
- 本地下載完成后, 可以, 在本地已下載的文件的MD5值和服務(wù)器返回的進行對比;
- 為了簡單, 有的, 服務(wù)器返回的下載文件MD5值, 直接就是命名稱為對應(yīng)的文件名稱
- 在沒有
-
-
3.2、創(chuàng)建一個管理下載的類
-
命名下載的類為:
JKDownLoader,繼承于NSObject,定義一個下載的方法/** 定義一個下載的方法 (1)、先查看服務(wù)器上的文件大小 (2)、查看本地是否存在文件,如果存在如下 2.1、如果文件小于服務(wù)器文件的大小,從本地文件長度開始下載(斷點續(xù)傳) 2.2、如果文件等于服務(wù)器文件的大小,再把文件生成一個MD5與服務(wù)器對文件返回的MD5做對比,如果一樣,代表下載完成 2.3、如果文件大于服務(wù)器文件的大小,發(fā)生錯誤,直接刪除文件,重新下載 (3)、如果本地不存在該文件,直接下載 @param url 下載的url */ -(void)downloadWithUrl:(NSURL *)url{ }
-
-
3.3、從 服務(wù)器 獲取下載文件的信息
-
我們需要設(shè)置下載的總大小以及下載后存放的位置
/** 文件的總大小 */ @property(nonatomic,assign) long long expectdContentLength; /** 文件的下載路徑 */ @property(nonatomic,strong) NSString *downloadPath; -
獲取文件信息的私有ipa
#pragma mark 私有方法 -(void)selectServerFileInfoWithUrl:(NSURL *)url{ // 1.請求信息:我們只需要獲取頭部信息就好 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeOut]; request.HTTPMethod = @"HEAD"; // 2.建立網(wǎng)絡(luò)連接 NSURLResponse *response = nil; [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL]; NSLog(@"%@ %@ %lld",data,response,response.expectedContentLength); // 3.記錄服務(wù)器的文件信息 // 3.1、文件長度 self.expectdContentLength = response.expectedContentLength; // 3.2、建議保存的文件名字,將在文件保存在tmp,系統(tǒng)會自動回收 self.downloadPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename]; }- 提示:這里采用的是同步方法,因為我們需要根據(jù)文件的信息去操作下面的下載操作,不能使用異步
-
NSURLConnection:默認(rèn)是在 主線程 進行操作,而NSURLSession是在 子線程 進行操作
-
-
3.4、檢查 本地 下載文件的信息
/** 2.從本地檢查要下載的文件信息(除了文件下載完,其他的情況都需要下載文件) @return YES:需要下載,NO:不需要下載 */ -(BOOL)checkLocalFileInfo{ long long fileSize = 0; // 1.判斷文件是否存在 if ([[NSFileManager defaultManager]fileExistsAtPath:self.downloadPath]) { // 獲取本地存在文件大小 NSDictionary *attributes = [[NSFileManager defaultManager]attributesOfItemAtPath:self.downloadPath error:NULL]; NSLog(@"%@",attributes); fileSize = [attributes[NSFileSize] longLongValue]; } // 2.根據(jù)文件大小來判斷文件是否存在 if(fileSize > self.expectdContentLength){ // 文件異常,刪除該文件 [[NSFileManager defaultManager]removeItemAtPath:self.downloadPath error:NULL]; fileSize = 0; }else if (fileSize == self.expectdContentLength) { // 文件已經(jīng)下載完 return NO; } return YES; } -
3.5、文件下載實現(xiàn)
-
(1)、定義一個屬性保存下載的地址 URL
/** 視頻的下載地址 URL */ @property(nonatomic,strong) NSURL *downloadUrl; -
(2)、視頻下載從當(dāng)前的字節(jié)開始下載,不管字節(jié)是不是0,都是檢查過本地路徑的字節(jié),本地有的話,當(dāng)前字節(jié)就不是0,也就是斷點續(xù)傳;沒有的話就是0,也就是從頭開始下載
-
拓展一個 HTTP 屬性 Range,下載會用到
Bytes = 0-499 : 從 0 到 499 的 500 個字節(jié) Bytes = 500-999 : 從500-999的第二個500字節(jié) Bytes = 500- : 從500開始到以后所有的字節(jié) Bytes = -500 最后500個字節(jié) Bytes = 500-699,800-1299,1600-2000 同時指定多個范圍
-
-
(3)、開始下載,這里先使用上面
一中的NSURLConncetion放在子線程,開啟Runloop的代理方法來下載,把 NSURLConncetion 放在異步并發(fā)的隊列,用文件流拼接寫入路徑,下面只展示部分代碼,完整代碼看 demo/* 保存文件的輸出流 - (void)open; 寫入之前,打開流 - (void)close; 寫入完畢之后,關(guān)閉流 */ @property(nonatomic,strong)NSOutputStream *fileStream; /* 保存下載線程的運行循環(huán),也就是下載任務(wù)的 runloop */ @property(nonatomic,assign)CFRunLoopRef downloadRunloop; #pragma mark 下載文件 -(void)downloadFile{ dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 1.建立請求 NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:self.downloadUrl cachePolicy:1 timeoutInterval:MAXFLOAT]; // 2.設(shè)置下載的字節(jié)范圍,self.currentLength到之后所有的字節(jié) NSString *downloadRangeStr = [NSString stringWithFormat:@"bytes=%lld-",self.currentContentLength]; // 3.設(shè)置請求頭字段 [request setValue:downloadRangeStr forHTTPHeaderField:@"Range"]; // 4.開始網(wǎng)絡(luò)連接 NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self]; // 5.設(shè)置代理工作的操作 [[NSOperationQueue alloc]init] 默認(rèn)創(chuàng)建一個異步并發(fā)隊列 [connection setDelegateQueue:[[NSOperationQueue alloc]init]]; // 啟動連接 [connection start]; //5.啟動運行循環(huán) /* CoreFoundation 框架 CFRunLoop CFRunloopStop() 停止指定的runloop CFRunloopGetCurrent() 獲取當(dāng)前的Runloop CFRunloopRun() 直接啟動當(dāng)前的運行循環(huán) */ // (1)、拿到當(dāng)前的運行循環(huán) self.downloadRunloop = CFRunLoopGetCurrent(); // (2)、啟動當(dāng)前的運行循環(huán) CFRunLoopRun(); }); } #pragma mark NSURLConnection的代理NSURLConnectionDataDelegate的方法 // 1、接收服務(wù)器的響應(yīng) --- 狀態(tài)和響應(yīng)頭做一些準(zhǔn)備工作 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{ self.fileStream = [[NSOutputStream alloc]initToFileAtPath:self.downloadPath append:YES]; [self.fileStream open]; } // 2、接收服務(wù)器的數(shù)據(jù),由于數(shù)據(jù)是分塊發(fā)送的,這個代理方法會被執(zhí)行多次,因此我們也會拿到多個data - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{ // 接收數(shù)據(jù),用輸出流拼接,計算下載進度 // 將數(shù)據(jù)拼接起來,并判斷是否可寫如,一般情況下可寫入,除非磁盤空間不足 if ([self.fileStream hasSpaceAvailable]) { [self.fileStream write:data.bytes maxLength:data.length]; } // 當(dāng)前長度拼接 self.currentContentLength += data.length; // 計算百分比 // progress = (float)long long / long long float progress = (float)self.currentContentLength/self.expectdContentLength; // 傳送百分比 if (self.progress) { self.progress(progress); } NSLog(@"%f %@",progress,[NSThread currentThread]); } // 3、接收到所有的數(shù)據(jù)下載完畢后會有一個通知 - (void)connectionDidFinishLoading:(NSURLConnection *)connection; { NSLog(@"下載完畢"); [self.fileStream close]; // 下載完成的回調(diào) if (self.completion) { self.completion(self.downloadPath); } // 關(guān)閉當(dāng)前下載完的 RunLoop CFRunLoopStop(self.downloadRunloop); } // 4、下載錯誤的處理 -(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{ NSLog(@"連接失?。?@",error.localizedDescription); // 關(guān)閉流 [self.fileStream close]; // 關(guān)閉當(dāng)前的 RunLoop CFRunLoopStop(self.downloadRunloop); }
-
-
3.6、暫停下載操作
暫停下載操作直接調(diào)用:
NSURLConnection的cancel就好 -
3.7、多文件下載管理
我們創(chuàng)建一個下載管理器
JKDownloaderManger,設(shè)置成單利,用來下載多個文件,同時創(chuàng)建下載緩存池,避免多次下載同一個文件-
(1)、單利的實現(xiàn)(一個靜態(tài)變量,三個方法,才是完整的單利)
static id instance; +(instancetype)allocWithZone:(struct _NSZone *)zone{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [super allocWithZone:zone]; }); return instance; } -(id)copy{ return instance; } +(instancetype)shareDownloaderManger{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc]init]; }); return instance; } -
(2)、創(chuàng)建字典緩存池
@property(nonatomic,strong) NSMutableDictionary *downloadCache; -
(3)、下載方法的實現(xiàn)( 以NSURL的url.path 為鍵 )
-(void)jk_downloadWithUrl:(NSURL *)url withDownProgress:(downProgress)progress completion:(downCompletion)completion fail:(downFailed)failed { // 1.判斷緩存池里面是否有同一個下載任務(wù) JKDownLoader *downLoader = self.downloadCache[url.path]; if (downLoader != nil) { NSLog(@"已經(jīng)在下載列表中,請不要重復(fù)下載"); return; } // 2.創(chuàng)建新的下載任務(wù) downLoader = [[JKDownLoader alloc]init]; // 3.將下載任務(wù)添加到緩存池 [self.downloadCache setValue:downLoader forKey:url.path]; __weak typeof(self) weakSelf = self; [downLoader jk_downloadWithUrl:url withDownProgress:progress completion:^(NSString * _Nonnull downFilePath) { // 1.將下載從緩存池移除 [weakSelf.downloadCache removeObjectForKey:url.path]; // 2.執(zhí)行調(diào)用方法的回調(diào) if (completion) { completion(downFilePath); } } fail:failed]; } -
(4)、下載暫停:暫停下載,從緩存池移除該url的path
#pragma mark 暫停某個文件下載 -(void)pauseloadWithUrl:(NSURL *)url{ // 1.通過url獲取下載任務(wù) JKDownLoader *downLoader = self.downloadCache[url.path]; // 2.暫停下載 if (downLoader == nil){ if (self.failed) { self.failed(@"已經(jīng)暫停下載,請不要重復(fù)點擊"); } return; } [downLoader pause]; // 3.從緩存池移除 [self.downloadCache removeObjectForKey:url.path]; }iOS 下載器完整的代碼請查看demo
-
到此下載完畢,下一篇會闡述 下載中 和 下載完成 文件夾里面文件的讀取,敬請關(guān)注









