下載檔案思路
阿新 • • 發佈:2019-02-02
下載檔案
直接請求獲取:
- 這種方式會將資料全部接收回來,然後一次性儲存到檔案中,會出現記憶體峰值問題,也沒有進度跟進
//ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
//ViewController.h
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
/*
1、會有記憶體峰值。
2、沒有進度跟進
*/
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 下載檔案的URL
NSString *URLString = @"http://192.168.2.23/12設定資料和frame.mp4";
// 百分號編碼(如果使用get方法請求的 url 字串中,包含中文或空格等特殊字元,需要新增百分號轉義)
URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// URL
NSURL *url = [NSURL URLWithString:URLString];
// Request
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 傳送請求-->非同步下載
// 這個方法會將資料從網路接收過來之後,在記憶體中拼接,再執行block中的檔案儲存,不能解決出現記憶體峰值問題.
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
//會將資料先快取到記憶體中,然後再寫入到檔案中
NSLog(@"%@---%zd",response,data.length);
// 將檔案資料寫到檔案中
[data writeToFile:@"/Users/shenzhenios/Desktop/abc.mp4" atomically:YES];
}];
@end
代理方法簡單版:
使用代理方法的方式(簡單版)
這裡是一個簡單版本,實現了下載進度跟進,但是還沒有解決記憶體峰值問題.
//ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
//ViewController.m
#import "ViewController.h"
/*
注意: <NSURLConnectionDownloadDelegate>是錯誤的代理方法,裡面的代理方法也可以顯示下載進度,但是下載檔案無
法找到,一般用在NSURLConnectionDownloadDelegate代理是使用在Newsstand Kit’s建立的NSURLConnection物件上
注:Newsstand Kit’s 是用來下載報刊,電子雜誌等資料使用的框架
*/
//正確的代理
@interface ViewController () <NSURLConnectionDataDelegate>
/**
* 要下載檔案的總大小
*/
@property (nonatomic, assign) long long expectedContentLength;
/**
* 當前已經接收檔案的大小
*/
@property (nonatomic, assign) long long currentFileSize;
/**
* 用來拼接檔案資料
*/
@property (nonatomic, strong) NSMutableData *fileData;
/**
* 儲存下載檔案的路徑
*/
@property (nonatomic, copy) NSString *destPath;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
/*
1、會有記憶體峰值。
*/
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 下載檔案的URL
NSString *URLString = @"http://192.168.2.23/12設定資料和frame.mp4";
// 百分號編碼(如果使用get方法請求的 url 字串中,包含中文或空格等特殊字元,需要新增百分號轉義)
URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// URL
NSURL *url = [NSURL URLWithString:URLString];
// Request
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 建立一個URLConnection物件,並立即載入url指定的資料,並指明瞭代理.
[NSURLConnection connectionWithRequest:request delegate:self];
}
#pragma mark - NSURLConnectionDataDelegate 代理方法
/**
* 接收到伺服器響應時呼叫(呼叫一次)
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// 獲得要下載檔案的大小
self.expectedContentLength = response.expectedContentLength;
// 獲得伺服器建議儲存的檔名
self.destPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
NSLog(@"%@", self.destPath);
}
/**
* 接收到伺服器返回的資料就呼叫 (有可能會呼叫多次)
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// 累加檔案大小
self.currentFileSize += data.length;
// 計算進度值
CGFloat progress = (CGFloat)self.currentFileSize / self.expectedContentLength;
// 將資料拼接起來
[self.fileData appendData:data];
NSLog(@"progress =%f", progress);
}
/**
* 請求完畢之後呼叫(呼叫一次)
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"%s", __FUNCTION__);
// 將檔案資料寫入沙盒
[self.fileData writeToFile:self.destPath atomically:YES];
// 清空資料
self.fileData = nil;
}
/**
* 請求失敗/出錯時呼叫 (一定要對錯誤進行處理)
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%s", __FUNCTION__);
// 清空資料
self.fileData = nil;
}
#pragma mark - 懶載入資料
- (NSMutableData *)fileData {
if (_fileData == nil) {
_fileData = [[NSMutableData alloc] init];
}
return _fileData;
}
@end
代理方法簡單版的改進:
- 利用NSFileHandle拼接檔案,實現一塊一塊的寫入資料,解決記憶體峰值問題.
- 還存在問題就是該程式碼反覆執行多次,檔案會不斷累加,不斷變大.
//ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
//ViewController.m
#import "ViewController.h"
@interface ViewController () <NSURLConnectionDataDelegate>
/**
* 要下載檔案的總大小
*/
@property (nonatomic, assign) long long expectedContentLength;
/**
* 當前已經接收檔案的大小
*/
@property (nonatomic, assign) long long currentFileSize;
/**
* 儲存下載檔案的路徑
*/
@property (nonatomic, copy) NSString *destPath;
/**
* 檔案指標
*/
@property (nonatomic, strong) NSFileHandle *fp;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 下載檔案的URL
NSString *URLString = @"http://192.168.30.79/117檔案操作之字串與二進位制";
// 百分號編碼
URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// URL
NSURL *url = [NSURL URLWithString:URLString];
// Request
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 建立一個URLConnection物件,並立即載入url執行的資料
[NSURLConnection connectionWithRequest:request delegate:self];
}
/**
NSFileHandle:Handle(控制代碼/檔案指標) 一般是對Handle前一單詞物件的處理,利用NSFileHandle可以對檔案進行讀寫操作
NSFileManager: 管理檔案,檢查檔案是否存在,複製檔案,檢視檔案屬性...NSFileManager類似Finder
*/
#pragma mark - NSURLConnectionDataDelegate 代理方法
/**
* 接收到伺服器響應時呼叫
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// 獲得要下載檔案的大小
self.expectedContentLength = response.expectedContentLength;
// 獲得伺服器建議儲存的檔名
self.destPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
NSLog(@"%@", self.destPath);
}
/**
* 接收到伺服器返回的資料就呼叫 (有可能會呼叫多次)
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// 累加檔案大小
self.currentFileSize += data.length;
// 計算進度值
CGFloat progress = (CGFloat)self.currentFileSize / self.expectedContentLength;
// 拼接資料
[self writeData:data];
NSLog(@"progress =%f", progress);
}
/**
* 將資料寫入檔案中
*/
- (void)writeData:(NSData *)data {
if (self.fp == nil) {
[data writeToFile:self.destPath atomically:YES];
} else {
// 將檔案指標移動到檔案末尾
[self.fp seekToEndOfFile];
// 利用檔案指標寫入資料,預設是從檔案開頭拼接資料
[self.fp writeData:data];
}
}
/**
* 請求完畢之後呼叫
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"%s", __FUNCTION__);
// 檔案指標在使用完畢之後要關閉 (必須要有)
[self.fp closeFile];
}
/**
* 請求失敗/出錯時呼叫 (一定要對錯誤進行處理)
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%s", __FUNCTION__);
// 檔案指標在使用完畢之後要關閉 (必須要有)
[self.fp closeFile];
}
#pragma mark - 懶載入資料
- (NSFileHandle *)fp {
// 建立檔案指標物件
// fileHandleForReadingAtPath:以只讀的方式建立檔案指標物件
// fileHandleForWritingAtPath:以寫入的方式建立檔案指標物件
// 如果檔案不存在,則fp為nil
if (_fp == nil) {
//這裡只是單純的獲取指定路徑的檔案的檔案指標物件,並沒有建立檔案物件,因此,在檔案還沒有建立時試圖獲取檔案指標物件,返回值為nil
_fp = [NSFileHandle fileHandleForWritingAtPath:self.destPath];
}
return _fp;
}
@end
代理方法方式二:
- 利用NSOutputStream拼接檔案
- 還存在問題就是該程式碼反覆執行多次,檔案會不斷累加,不斷變大.
//ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
//ViewController.m
#import "ViewController.h"
@interface ViewController () <NSURLConnectionDataDelegate>
/**
* 要下載檔案的總大小
*/
@property (nonatomic, assign) long long expectedContentLength;
/**
* 當前已經接收檔案的大小
*/
@property (nonatomic, assign) long long currentFileSize;
/**
* 儲存下載檔案的路徑
*/
@property (nonatomic, copy) NSString *destPath;
/**
* 檔案輸出流
*/
@property (nonatomic, strong) NSOutputStream *fileStream;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 下載檔案的URL
NSString *URLString = @"http://192.168.30.79/117檔案操作之字串與二進位制.mp4";
// 百分號編碼
URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// URL
NSURL *url = [NSURL URLWithString:URLString];
// Request
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 建立一個URLConnection物件,並立即載入url執行的資料
[NSURLConnection connectionWithRequest:request delegate:self];
}
#pragma mark - NSURLConnectionDataDelegate 代理方法
/**
* 接收到伺服器響應時呼叫
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// 獲得要下載檔案的大小
self.expectedContentLength = response.expectedContentLength;
// 獲得伺服器建議儲存的檔名
self.destPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
NSLog(@"%@", self.destPath);
// 建立檔案輸出流 引數1:檔案路徑 引數2 YES:已追加形式輸出
self.fileStream = [NSOutputStream outputStreamToFileAtPath:self.destPath append:YES];
// 開啟流 --> 寫入資料之前,必須先開啟流
[self.fileStream open];
}
/**
* 接收到伺服器返回的資料就呼叫 (有可能會呼叫多次)
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// 累加檔案大小
self.currentFileSize += data.length;
// 計算進度值
CGFloat progress = (CGFloat)self.currentFileSize / self.expectedContentLength;
NSLog(@"progress =%f", progress);
// 寫入資料
[self.fileStream write:data.bytes maxLength:data.length];
}
/**
* 請求完畢之後呼叫
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"%s", __FUNCTION__);
// 關閉流:開啟流和關閉必須成對出現
[self.fileStream close];
}
/**
* 請求失敗/出錯時呼叫 (一定要對錯誤進行處理)
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%s", __FUNCTION__);
// 關閉流 :開啟流和關閉必須成對出現
[self.fileStream close];
}
@end
用子執行緒下載
- 用子執行緒下載
- 實現斷點續傳
- 存在問題:下載過程中不斷點選,會使下載檔案產生混亂,顯示的進度產生混亂.
//ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
//ViewController.m
#import "ViewController.h"
@interface ViewController () <NSURLConnectionDataDelegate>
/**
* 要下載檔案的總大小
*/
@property (nonatomic, assign) long long expectedContentLength;
/**
* 當前已經接收檔案的大小
*/
@property (nonatomic, assign) long long currentFileSize;
/**
* 儲存下載檔案的路徑
*/
@property (nonatomic, copy) NSString *destPath;
/**
* 檔案輸出流
*/
@property (nonatomic, strong) NSOutputStream *fileStream;
/**
* 連線物件
*/
@property (nonatomic, strong) NSURLConnection *connection;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
/**
* 暫停
* 這是一個按鈕的連線
*/
- (IBAction)pause {
// 一旦呼叫cancel方法,連線物件就不能再使用,下次請求需要重新建立新的連線物件進行下載.
[self.connection cancel];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 下載檔案的URL
NSString *URLString = @"http://192.168.2.23/12設定資料和frame.mp4";
// 百分號編碼
URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// URL
NSURL *url = [NSURL URLWithString:URLString];
// 檢查伺服器檔案資訊
[self checkServerFileInfo:url];
// 檢查本地檔案資訊
self.currentFileSize = [self checkLocalFileInfo];
// 如果本地檔案大小相等伺服器檔案大小
if (self.currentFileSize == self.expectedContentLength) {
NSLog(@"下載完成");
return;
}
// 告訴伺服器從指定的位置開始下載檔案
// Request:斷點續傳快取策略必須是直接從伺服器載入,請求物件必須是可變的。
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15.0];
// 設定從伺服器獲取檔案的位置(斷點續傳的位置)
NSString *range = [NSString stringWithFormat:@"bytes=%lld-",self.currentFileSize];
[request setValue:range forHTTPHeaderField:@"Range"];
// 建立一個URLConnection物件,並立即載入url執行的資料(第二次請求,用代理獲取檔案)
self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
// RunLoop:監聽事件。網路請求本身是一個事件,需要RunLoop來監聽並響應.定時器也是一個事件。
// 子執行緒的Runloop預設是不開啟,需要手動開啟RunLoop
// 當前網路請求結束之後,系統會預設關閉當前執行緒的RunLoop.
// 另一種開啟執行緒的方式:CFRunLoopRun();
[[NSRunLoop currentRunLoop] run];
NSLog(@"come here");
});
}
/**
* 檢查伺服器檔案資訊
*/
- (void)checkServerFileInfo:(NSURL *)url{
// 使用同步請求獲得伺服器檔案資訊(第一次請求,獲取伺服器檔案資訊)
// 請求物件
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 設定請求方法,獲取下載內容的頭資訊
// HEAD請求只用在下載檔案之前,獲得伺服器檔案資訊。
request.HTTPMethod = @"HEAD";
// 響應物件
NSURLResponse *response = nil;
// 傳送同步請求 --> 兩個** 就是傳遞物件的地址
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
// NSLog(@"response = %@",response);
// 獲得要下載檔案的大小
self.expectedContentLength = response.expectedContentLength;
// 獲得伺服器建議儲存的檔名
self.destPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
// NSLog(@"%@", self.destPath);
}
/**
* 檢查本地檔案資訊
*/
- (long long)checkLocalFileInfo {
// 獲得檔案管理者物件
NSFileManager *fileMangager = [NSFileManager defaultManager];
/*
檢查本地檔案資訊(斷點續傳邏輯思路)
> 如果本地沒有檔案,則從零開始下載
> 本地檔案的大小比伺服器檔案還大,刪除本地檔案從零開始下載。
> 本地檔案小於伺服器檔案,從本地檔案大小的位置開始下載。
> 本地檔案等於伺服器檔案,提示下載完成。
*/
long long fileSize = 0;
//判斷檔案是否存在
if ([fileMangager fileExistsAtPath:self.destPath]) {
// 存在,獲得本地檔案資訊
NSDictionary *fileAttributes = [fileMangager attributesOfItemAtPath:self.destPath error:NULL];
// NSLog(@"fileAttributes = %@",fileAttributes );
// 獲得檔案大小
// fileAttributes[NSFileSize] longLongValue
fileSize = [fileAttributes fileSize]; // 10 M
}
// 本地檔案跟伺服器檔案進行比較
if(fileSize > self.expectedContentLength) {
// 刪除本地檔案
[fileMangager removeItemAtPath:self.destPath error:NULL];
// 從零開始下載
fileSize = 0;
}
return fileSize;
}
#pragma mark - NSURLConnectionDataDelegate 代理方法
/**
* 接收到伺服器響應時呼叫
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// 建立檔案輸出流 引數1:檔案路徑 引數2 YES:已追加形式輸出
self.fileStream = [NSOutputStream outputStreamToFileAtPath:self.destPath append:YES];
// 開啟流 --> 寫入資料之前,必須先開啟流
[self.fileStream open];
}
/**
* 接收到伺服器返回的資料就呼叫 (有可能會呼叫多次)
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// NSLog(@"%@", [NSThread currentThread]);
// 累加檔案大小
self.currentFileSize += data.length;
// 計算進度值
CGFloat progress = (CGFloat)self.currentFileSize / self.expectedContentLength;
[NSThread sleepForTimeInterval:0.1];
NSLog(@"progress =%f", progress);
// 寫入資料
[self.fileStream write:data.bytes maxLength:data.length];
}
/**
* 請求完畢之後呼叫
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"%s", __FUNCTION__);
// 關閉流:開啟流和關閉必須成對出現
[self.fileStream close];
}
/**
* 請求失敗/出錯時呼叫 (一定要對錯誤進行處理)
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%s", __FUNCTION__);
// 關閉流 :開啟流和關閉必須成對出現
[self.fileStream close];
}
@end
使用單例下載管理器來管理下載
- 用子執行緒下載
- 實現斷點續傳
- 解決下載過程中不斷點選產生的檔案混亂問題和進度混亂問題.
- 存在問題:沒有限制開啟的最大執行緒數,當有大量檔案下載時,會按照檔案的數量開啟相應的執行緒數.
//ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
//ViewController.m
#import "ViewController.h"
#import "YFDownloadManager.h"
#import "YFProgressButton.h"
@interface ViewController ()
/**
* 進度按鈕
* 顯示進度的按鈕的連線
*/
@property (nonatomic, weak) IBOutlet YFProgressButton *progressBtn;
/**
* 下載操作
*/
//@property (nonatomic, strong) YFDownloadOperation *downloader;
@property (nonatomic, strong) NSURL *url;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
/**
* 暫停
* 按鈕的連線
*/
- (IBAction)pause {
// 一旦呼叫cancel方法,連線物件就不能再使用,下次請求需要重新建立新的連線物件
[[YFDownloadManager sharedManager] pause:self.url];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 下載檔案的URL
NSString *URLString = @"http://192.168.2.23/12設定資料和frame.mp4";
// 百分號編碼
URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// URL
NSURL *url = [NSURL URLWithString:URLString];
self.url = url;
// 利用單例接管下載操作
[[YFDownloadManager sharedManager] downloadWithURL:url progress:^(CGFloat progress) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"progress = %f",progress);
self.progressBtn.progress = progress;
});
} finished:^(NSString *destPath, NSError *error) {
NSLog(@"下載完成 destPath = %@,error = %@",destPath,error);
}];
}
@end
//單例檔案
//YFSingleton.h
// 標頭檔案使用的巨集
// ## 表示拼接前後兩個字串
#define YFSingleton_h(name) + (instancetype)shared##name;
#if __has_feature(objc_arc) // 是arc環境
#define YFSingleton_m(name) + (instancetype)shared##name {\
return [[self alloc] init];\
}\
\
+ (instancetype)allocWithZone:(struct _NSZone *)zone {\
static dispatch_once_t onceToken;\
static id instance = nil;\
dispatch_once(&onceToken, ^{ \
instance = [super allocWithZone:zone];\
});\
return instance;\
}\
\
- (id)copyWithZone:(nullable NSZone *)zone {\
return self;\
}
#else // MRC環境
#define YFSingleton_m(name) + (instancetype)shared##name {\
return [[self alloc] init];\
}\
\
+ (instancetype)allocWithZone:(struct _NSZone *)zone {\
static dispatch_once_t onceToken;\
static id instance = nil;\
dispatch_once(&onceToken, ^{ \
instance = [super allocWithZone:zone];\
});\
return instance;\
}\
\
- (id)copyWithZone:(nullable NSZone *)zone {\
return self;\
}\
- (oneway void)release {\
\
}\
\
- (instancetype)autorelease {\
return self;\
}\
\
- (instancetype)retain {\
return self;\
}\
\
- (NSUInteger)retainCount {\
return 1;\
}
#endif
//下載操作檔案
//YFDownloadOperation.h
#import <UIKit/UIKit.h>
@interface YFDownloadOperation : NSObject
/**
* 建立下載操作
*
* @param progressBlock 進度回撥
* @param finishedBlock 完成回撥
*
* @return 下載操作
*/
+ (instancetype)downloadOperation:(void (^)(CGFloat progress))progressBlock finishedBlock:(void (^)(NSString *destPath,NSError *error))finishedBlock;
/**
* 下載URL指定的檔案
*/
- (void)download:(NSURL *)URL;
/**
* 暫停下載
*/
- (void)pause;
/**
* 根據URL獲得檔案的下載進度
*/
+ (CGFloat)progressWithURL:(NSURL *)URL;
@end
//YFDownloadOperation.m
#import "YFDownloadOperation.h"
@interface YFDownloadOperation() <NSURLConnectionDataDelegate>
/**
* 要下載檔案的總大小
*/
@property (nonatomic, assign) long long expectedContentLength;
/**
* 當前已經接收檔案的大小
*/
@property (nonatomic, assign) long long currentFileSize;
/**
* 儲存下載檔案的路徑
*/
@property (nonatomic, copy) NSString *destPath;
/**
* 檔案輸出流
*/
@property (nonatomic, strong) NSOutputStream *fileStream;
/**
* 連線物件
*/
@property (nonatomic, strong) NSURLConnection *connection;
/**
* 進度回撥block
*/
@property (nonatomic, copy) void (^progressBlock)(CGFloat progress);
/**
* 完成回撥
*/
@property (nonatomic, copy) void (^finishedBlock)(NSString *destPath,NSError *error);
@end
@implementation YFDownloadOperation
/**
* 建立下載操作
*
* @param progressBlock 進度回撥
* @param finishedBlock 完成回撥
*
* @return 下載操作 如果block不是在當前方法執行,需要使用屬性引著
*/
+ (instancetype)downloadOperation:(void (^)(CGFloat progress))progressBlock finishedBlock:(void (^)(NSString *destPath,NSError *error))finishedBlock {
// 使用斷言
NSAssert(finishedBlock != nil, @"必須傳人完成回撥");
// 建立下載操作
YFDownloadOperation *downloader = [[YFDownloadOperation alloc] init];
// 記錄block
downloader.progressBlock = progressBlock;
downloader.finishedBlock = finishedBlock;
return downloader;
}
/**
* 下載URL指定的檔案
*/
- (void)download:(NSURL *)URL {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 下載檔案的URL
NSString *URLString = @"http://192.168.2.23/12設定資料和frame.mp4";
// 百分號編碼
URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// URL
NSURL *url = [NSURL URLWithString:URLString];
// 檢查伺服器檔案資訊
[self checkServerFileInfo:url];
// 檢查本地檔案資訊
self.currentFileSize = [self checkLocalFileInfo];
// 如果本地檔案大小相等伺服器檔案大小
if (self.currentFileSize == self.expectedContentLength) {
// 完成回撥
dispatch_async(dispatch_get_main_queue(), ^{
self.finishedBlock(self.destPath,nil);
});
// 進度回撥
if(self.progressBlock) {
self.progressBlock(1);
}
return;
}
// 告訴伺服器從指定的位置開始下載檔案
// Request:斷點續傳快取策略必須是直接從伺服器載入,請求物件必須是可變的。
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:15.0];
// 設定Range
NSString *range = [NSString stringWithFormat:@"bytes=%lld-",self.currentFileSize];
[request setValue:range forHTTPHeaderField:@"Range"];
// 建立一個URLConnection物件,並立即載入url執行的資料
self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
// RunLoop:監聽事件。網路請求本身也是一個事件。定時器本身也是一個。
// 子執行緒的Runloop預設是不開啟,需要手動開啟RunLoop
// CFRunLoopRun();
// 當前網路請求結束之後,系統會預設關閉當前執行緒的RunLoop.
[[NSRunLoop currentRunLoop] run];
NSLog(@"come here");
});
}
/**
* 暫停下載
*/
- (void)pause {
[self.connection cancel];
}
/**
* 根據URL獲得檔案的下載進度
*/
+ (CGFloat)progressWithURL:(NSURL *)URL {
YFDownloadOperation *downloader = [[YFDownloadOperation alloc] init];
[downloader checkServerFileInfo:URL];
downloader.currentFileSize = [downloader checkLocalFileInfo];
return (CGFloat)downloader.currentFileSize / downloader.expectedContentLength;
}
#pragma mark - 私有方法
/**
* 檢查伺服器檔案資訊
*/
- (void)checkServerFileInfo:(NSURL *)url{
// 使用同步請求獲得伺服器檔案資訊
// 請求物件
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 設定請求方法
// HEAD請求只用在下載檔案之前,獲取記錄檔案資訊的資料包文頭部內容,目的是獲得伺服器檔案資訊。
request.HTTPMethod = @"HEAD";
// 響應物件,用來儲存從伺服器返回的內容
NSURLResponse *response = nil;
// 傳送同步請求 --> 兩個** 就是傳遞物件的地址
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
// NSLog(@"response = %@",response);
// 獲得要下載檔案的大小
self.expectedContentLength = response.expectedContentLength;
// 獲得伺服器建議儲存的檔名
self.destPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
NSLog(@"%@", self.destPath);
}
/**
* 檢查本地檔案資訊
*/
- (long long)checkLocalFileInfo {
// 獲得檔案管理者物件
NSFileManager *fileMangager = [NSFileManager defaultManager];
/*
檢查本地檔案資訊
> 如果本地沒有檔案,則從零開始下載
> 本地檔案的大小比伺服器檔案還大,刪除本地檔案從零開始下載。
> 本地檔案小於伺服器檔案,從本地檔案大小的位置開始下載。
> 本地檔案等於伺服器檔案,提示下載完成。
*/
long long fileSize = 0;
if ([fileMangager fileExistsAtPath:self.destPath]) {
// 存在,獲得本地檔案資訊
NSDictionary *fileAttributes = [fileMangager attributesOfItemAtPath:self.destPath error:NULL];
// NSLog(@"fileAttributes = %@",fileAttributes );
// 獲得檔案大小
// fileAttributes[NSFileSize] longLongValue
fileSize = [fileAttributes fileSize]; // 10 M
}
// 本地檔案跟伺服器檔案進行比較
if(fileSize > self.expectedContentLength) {
// 刪除本地檔案
[fileMangager removeItemAtPath:self.destPath error:NULL];
// 從零開始下載
fileSize = 0;
}
return fileSize;
}
#pragma mark - NSURLConnectionDataDelegate 代理方法
/**
* 接收到伺服器響應時呼叫
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// 建立檔案輸出流 引數1:檔案路徑 引數2 YES:已追加形式輸出
self.fileStream = [NSOutputStream outputStreamToFileAtPath:self.destPath append:YES];
// 開啟流 --> 寫入資料之前,必須先開啟流
[self.fileStream open];
}
/**
* 接收到伺服器返回的資料就呼叫 (有可能會呼叫多次)
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// NSLog(@"%@", [NSThread currentThread]);
// 累加檔案大小
self.currentFileSize += data.length;
// 計算進度值
CGFloat progress = (CGFloat)self.currentFileSize / self.expectedContentLength;
[NSThread sleepForTimeInterval:0.1];
// NSLog(@"progress =%f", progress);
// 寫入資料
[self.fileStream write:data.bytes maxLength:data.length];
if (self.progressBlock) {
self.progressBlock(progress);
}
}
/**
* 請求完畢之後呼叫
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"%s", __FUNCTION__);
// 關閉流:開啟流和關閉必須成對出現
[self.fileStream close];
// 主執行緒回撥
dispatch_async(dispatch_get_main_queue(), ^{
self.finishedBlock(self.destPath,nil);
});
}
/**
* 請求失敗/出錯時呼叫 (一定要對錯誤進行錯誤)
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%s", __FUNCTION__);
// 關閉流 :開啟流和關閉必須成對出現
[self.fileStream close];
// 主執行緒回撥
dispatch_async(dispatch_get_main_queue(), ^{
self.finishedBlock(nil,error);
});
}
@end
//單例下載管理器
//YFDownloadManager.h
#import <UIKit/UIKit.h>
#import "YFSingleton.h"
@interface YFDownloadManager : NSObject
YFSingleton_h(Manager)
/**
* 下載URL指定的檔案
*
* @param URL 檔案路徑
* @param progress 進度回撥
* @param finished 完成回撥
*/
- (void)downloadWithURL:(NSURL *)URL progress:(void (^)(CGFloat progress))progress finished:(void (^)(NSString *destPath,NSError *error))finished;
/**
* 暫停URL指定的下載操作
*/
- (void)pause:(NSURL *)URL;
@end
//YFDownloadManager.m
#import "YFDownloadManager.h"
#import "YFDownloadOperation.h"
@interface YFDownloadManager()
/**
* 操作快取池
*/
@property (nonatomic, strong) NSMutableDictionary *operationCache;
@end
@implementation YFDownloadManager
YFSingleton_m(Manager)
/**
* 下載URL指定的檔案
*
* @param URL 檔案路徑
* @param progress 進度回撥
* @param finished 完成回撥
*/
- (void)downloadWithURL:(NSURL *)URL progress:(void (^)(CGFloat progress))progress finished:(void (^)(NSString *destPath,NSError *error))finished {
if (URL == nil) return;
// 判斷是否存在對應的下載操作
if(self.operationCache[URL] != nil) {
NSLog(@"正在拼命下載中...稍安勿躁...");
return;
}
// 利用類方法建立下載操作
YFDownloadOperation *downloader = [YFDownloadOperation downloadOperation:progress finishedBlock:^(NSString *destPath, NSError *error) {
// 將操作從快取池中移除
[self.operationCache removeObjectForKey:URL];
// 完成回撥
finished(destPath,error);
}];
// 將操作新增到操作快取池中
[self.operationCache setObject:downloader forKey:URL];
// 開始下載
[downloader download:URL];
}
/**
* 暫停URL指定的下載操作
*/
- (void)pause:(NSURL *)URL {
// 根據URL獲得下載操作
YFDownloadOperation *downloader = self.operationCache[URL];
// 暫停下載
[downloader pause];
// 將操作從快取池中移除
[self.operationCache removeObjectForKey:URL];
}
#pragma mark - 懶載入資料
- (NSMutableDictionary *)operationCache {
if (_operationCache == nil) {
_operationCache = [[NSMutableDictionary alloc] init];
}
return _operationCache;
}
@end
//顯示進度按鈕檔案
//YFProgressButton.h
#import <UIKit/UIKit.h>
// IB_DESIGNABLE:表示這個類可以在IB中設定
// IBInspectable:表示這個屬性可以在IB中設值
// IB:interface builder 介面構建者
IB_DESIGNABLE
@interface YFProgressButton : UIButton
/**
* 進度值
*/
@property (nonatomic, assign) IBInspectable CGFloat progress;
/**
* 線寬
*/
@property (nonatomic, assign) IBInspectable CGFloat lineWidth;
/**
* 線的顏色
*/
@property (nonatomic, strong) IBInspectable UIColor *lineColor;
@end
//YFProgressButton.m
#import "YFProgressButton.h"
@implementation YFProgressButton
- (void)setProgress:(CGFloat)progress {
_progress = progress;
// 設定按鈕標題
[self setTitle:[NSString stringWithFormat:@"%.2f%%",progress * 100] forState:UIControlStateNormal];
// 通知重繪
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
// 圓心
CGPoint center = CGPointMake(rect.size.width * 0.5, rect.size.height * 0.5);
// 起始角度
CGFloat startAngle = - M_PI_2;
// 結束角度
CGFloat endAngle = 2 * M_PI * self.progress + startAngle;
// 半價
CGFloat raduis = (MIN(rect.size.width, rect.size.height) - self.lineWidth) * 0.5;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:raduis startAngle:startAngle endAngle:endAngle clockwise:YES];
// 設定線寬
path.lineWidth = self.lineWidth;
// 設定顏色
[self.lineColor set];
// 渲染
[path stroke];
}
@end
完美版
//主控制器檔案
//ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
//ViewController.m
#import "ViewController.h"
#import "YFDownloadManager.h"
#import "YFProgressButton.h"
@interface ViewController ()
/**
* 進度按鈕
*/
@property (nonatomic, weak) IBOutlet YFProgressButton *progressBtn;
/**
* 下載操作
*/
//@property (nonatomic, strong) YFDownloadOperation *downloader;
@property (nonatomic, strong) NSURL *url;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
/**
* 暫停
* 暫停按鈕連線
*/
- (IBAction)pause {
// After this method is called, the connection makes no further delegate method calls. If you want to reattempt the connection, you should create a new connection object.
// 一旦呼叫cancel方法,連線物件就不能再使用,下次請