下載文件思路

下載文件


直接請求獲取:

  • 這種方式會將數據全部接收回來,然後一次性存儲到文件中,會出現內存峯值問題,也沒有進度跟進
//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方法,連接對象就不能再使用,下次請求需要重新創建新的連接對象
    [[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 : NSOperation
/**
 *  創建下載操作
 *
 *  @param progressBlock 進度回調
 *  @param finishedBlock 完成回調
 *
 *  @return 下載操作
 */
+ (instancetype)downloadOperationWithURL:(NSURL *)URL progressBlock:(void (^)(CGFloat progress))progressBlock finishedBlock:(void (^)(NSString *destPath,NSError *error))finishedBlock;

/**
 *  暫停下載
 */
- (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);
/**
 *  要下載文件的URL
 */
@property (nonatomic, strong) NSURL *URL;
@end

@implementation YFDownloadOperation

/**
 *  創建下載操作
 *
 *  @param progressBlock 進度回調
 *  @param finishedBlock 完成回調
 *
 *  @return 下載操作 如果block不是在當前方法執行,需要使用屬性引着
 */
+ (instancetype)downloadOperationWithURL:(NSURL *)URL progressBlock:(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;

    // 記錄URL
    downloader.URL = URL;

    return downloader;
}

- (void)main {
    @autoreleasepool {
        [self download];
    }
}

/**
 *  下載URL指定的文件
 */
- (void)download{
    //dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 檢查服務器文件信息
        [self checkServerFileInfo:self.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:self.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;
/**
 *  下載隊列
 */
@property (nonatomic, strong) NSOperationQueue *downloadQueue;
@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 downloadOperationWithURL:URL progressBlock:progress finishedBlock:^(NSString *destPath, NSError *error) {
        // 將操作從緩存池中移除
        [self.operationCache removeObjectForKey:URL];
        // 完成回調
        finished(destPath,error);
    }];
    // 將操作添加到操作緩存池中
    [self.operationCache setObject:downloader forKey:URL];
    // 開始下載
//    [downloader download:URL];
    // 只有NSOperation的子類纔可以添加到NSOperationQueue中
    [self.downloadQueue addOperation:downloader];
}
/**
 *  暫停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;
}

- (NSOperationQueue *)downloadQueue {
    if (_downloadQueue == nil) {
        _downloadQueue = [[NSOperationQueue alloc] init];
        // 設置最大併發數
        _downloadQueue.maxConcurrentOperationCount = 2;
    }
    return _downloadQueue;
}
@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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章