下载文件思路

下载文件


直接请求获取:

  • 这种方式会将数据全部接收回来,然后一次性存储到文件中,会出现内存峰值问题,也没有进度跟进
//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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值