1. 程式人生 > >下載檔案思路

下載檔案思路

下載檔案

直接請求獲取:

  • 這種方式會將資料全部接收回來,然後一次性儲存到檔案中,會出現記憶體峰值問題,也沒有進度跟進
//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方法,連線物件就不能再使用,下次請