iOS断点续传

基本思路:    

  • 判断本地文件,如果本地文件存在要判断文件的大小

    • 如果没有本地文件,下载

    • 如果本地文件存在,发送head请求获取服务器文件大小

      • 本地文件大小 == 服务器文件大小,不下载

      • 本地文件大小 < 服务器文件大小,从之前的位置开始下载

      • 本地文件大小 > 服务器文件大小,删除本地文件,重新下载


检查服务器文件

    获取服务器上的文件信息

    

 
 
  1. //获取服务器上的文件大小和文件名
  2. - (void)checkServerInfo:(NSURL *)url {
  3.    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
  4.    request.HTTPMethod = @"HEAD";
  5.    NSURLResponse *response = nil;
  6.    [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
  7.    self.expectedContentLength = response.expectedContentLength;
  8.    self.filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
  9. }


检查本地文件

  • 如果没有文件    返回0,    从头下载

  • 如果文件大小 > 服务器文件大小  返回0  删除本地文件   从头下载

  • 如果文件大小 == 服务器文件大小 返回文件大小  如果大小相等,不用下载

  • 如果文件大小 < 服务器文件大小  返回文件大小   从之前的位置开始下载

  •    
       
    1. - (long long)checkLocalInfo {
    2.    NSFileManager *fileManger = [NSFileManager defaultManager];
    3.    //检查文件是否已存在
    4.    if (![fileManger fileExistsAtPath:self.filePath]) {
    5.        return 0;
    6.    }
    7.    //获取本地文件的大小
    8.    NSDictionary *fileAttrs = [fileManger attributesOfItemAtPath:self.filePath error:NULL];
    9.    long long fileSize = fileAttrs.fileSize;
    10.    if (fileSize > self.expectedContentLength) {
    11.        [fileManger removeItemAtPath:self.filePath error:NULL];
    12.        return 0;
    13.    }
    14.    return fileSize;
    15. }


下载

  • 如果本地文件和服务器文件大小相等,不下载

  •    
       
    1. long long fileSize = [self checkLocalInfo];
    2.    if (fileSize == self.expectedContentLength) {
    3.        NSLog(@"文件已经下载");
    4.        return;
    5.    }
  • 从指定偏移处开始下载

  • range 取值

    • bytes=x-y x字节开始下载,下载到y字节

      bytes=x-  x字节开始下载,下载到最后

      bytes=-x   0字节开始下载,下载到x字节

  •    
       
    1. //从指定偏移处下载文件
    2. - (void)downloadWithUrl:(NSURL *)url offset:(long long)offset {
    3.    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    4.    /*
    5.     range 取值
    6.     bytes=x-y 从x字节开始下载,下载到y字节
    7.     bytes=x-  从x字节开始下载,下载到最后
    8.     bytes=-x   从0字节开始下载,下载到x字节
    9.     */
    10.    [request setValue:[NSString stringWithFormat:@"bytes=%lld",offset] forHTTPHeaderField:@"range"];
    11.    NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
    12. }


异步下载

  • 默认代理方法都在主线程上执行的,下载会卡死

  • 异步下载

    • NSURLConnection 的代理方法,想在子线程上执行的话必须开启消息循环

    • 把下载方法中的所有代码都放在异步队列中执行

    • 在指定位置处开启消息循环,消息循环的模式必须是default模式

    •      
           
      1. - (void)downloadWithUrl:(NSURL *)url offset:(long long)offset {
      2.    [[NSOperationQueue new] addOperationWithBlock:^{
      3.        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
      4.        /*
      5.         range 取值
      6.         bytes=x-y 从x字节开始下载,下载到y字节
      7.         bytes=x-  从x字节开始下载,下载到最后
      8.         bytes=-x   从0字节开始下载,下载到x字节
      9.         */
      10.        [request setValue:[NSString stringWithFormat:@"bytes=%lld-",offset] forHTTPHeaderField:@"range"];
      11.        NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
      12.        //启动消息循环
      13.        [[NSRunLoop currentRunLoop] run];
      14.    }];
      15. }


下载完成后的回调

  • 下载完成后或出错之后要在主界面做提示,现在所有的下载操作都封装在Downloader这个类中,学习SDWebImage中的做法,给下载操作传入需要的block,当下载完成或出错的时候调用

  • 修改Download二头文件中的下载方法,增加需要的block,进度,完成,出错的block

  • 修改downloader.m中的实现方法,因为具体的进度,完成,出错都是在URLConnection的代理方法实现的,所以传入block后需要定义属性接收

    • 定义block的属性

    •      
           
      1. @property (nonatomic, copy) void(^successBlock)(NSString *path);
      2. @property (nonatomic, copy) void(^processBlock)(float process);
      3. @property (nonatomic, copy) void(^errorBlock)(NSError *error);
    • 下载方法中给block属性赋值

    •      
           
      1. - (void)downloadWithUrlString:(NSString *)urlStr success:(void (^)(NSString *))successBlock process:(void (^)(float))processBlock error:(void (^)(NSError *))errorBlock {
      2.    self.successBlock = successBlock;
      3.    self.processBlock = processBlock;
      4.    self.errorBlock = errorBlock;
    • 对应的位置调用回调方法

    • URLConnection的下载方法中调用进度的回调,在当前子线程中执行

    •      
           
      1. - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
      2.    [self.stream write:data.bytes maxLength:data.length];
      3.    self.currentContentLength += data.length;
      4.    
      5.    float process = self.currentContentLength * 1.0 / self.expectedContentLength;
      6.    if (self.processBlock) {
      7.        self.processBlock(process);
      8.    }
      9. }
    • URLConnection的下载完成方法中调用完成的回调,下载完成会回归主线程调用,在主线程更新界面

    •      
           
      1. - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
      2.    [self.stream close];
      3.    if (self.successBlock) {
      4.        dispatch_async(dispatch_get_main_queue(), ^{
      5.            self.successBlock(self.filePath);
      6.        });
      7.    }
      8. }
    • URLConnection的下载出错的方法中调用出错的回调,在那个线程执行由调用者决定

    •      
           
      1. - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
      2.    [self.stream close];
      3.    
      4.    if (self.errorBlock) {
      5.        self.errorBlock(error);
      6.    }
      7. }
    • 如果文件已经存在,也要调用完成的方法

    •      
           
      1.    if (fileSize == self.expectedContentLength) {
      2.        NSLog(@"文件已经下载");
      3.        if (self.successBlock) {
      4.            dispatch_async(dispatch_get_main_queue(), ^{
      5.                self.successBlock(self.filePath);
      6.            });
      7.        }
      8.        
      9.        return;
      10.    }
    • controller中调用


下载进度提示

  • 界面上放置一个自定义按钮,设置大小

  • 创建按钮的自定义类 (按钮必须是custom的  系统默认的模式button在重绘时会按钮会跟被点击一样,会不断闪烁)

  • 定义一个progress的属性,把进度传过来

  • 每当给progress属性赋值的时候调用setneeddisplay 重绘

  •    
       
    1. //重写progress的set方法
    2. - (void)setProgress:(float)progress {
    3.    _progress = progress;
    4.    [self setTitle:[NSString stringWithFormat:@"0.2f%%%",progress * 100] forState:UIControlStateNormal];
    5.    [self setNeedsDisplay];
    6. }
  • 重写drawRect方法,根据progress画圆

  •    
       
    1. #define kLINEWIDTH 5
    2. - (void)drawRect:(CGRect)rect {
    3.    UIBezierPath *path = [UIBezierPath bezierPath];
    4.    CGPoint center = CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5);
    5.    CGFloat radius = MIN(center.x, center.y) - kLINEWIDTH;
    6.    CGFloat startA = -M_PI_2;
    7.    CGFloat endA = startA + self.progress * 2 * M_PI;
    8.    [path addArcWithCenter:center radius:radius startAngle:startA endAngle:endA clockwise:YES];
    9.    
    10.    path.lineCapStyle = kCGLineCapRound;
    11.    path.lineWidth = kLINEWIDTH;
    12.    [[UIColor orangeColor] setStroke];
    13.    [path stroke];
    14. }
  • controller中显示进度

    • controller 中在下载进度的回调中调用

    •      
           
      1. process:^(float process) {
      2.        dispatch_async(dispatch_get_main_queue(), ^{
      3.          self.progressView.progress = process;
      4.        });
      5.        
      6.    }
    • 在storyboard中设置自定义类的属性

    • IB



暂停下载

  • 暂停下载就是调用connection的cancel方法

  •    
       
    1. - (void)pause {
    2.    [self.conn cancel];
    3. }


代码重构

  • 多点击屏幕几次,这个时候会不停的下载同一个文件

  • 创建一个下载的单例的管理类

  • 通过管理类缓存下载操作,解决重复下载同一个文件

  • 下载管理类和缓存池

    • 创建下载的管理类DownloaderManger单例

    •      
           
      1. static id instance = nil;
      2. + (instancetype)allocWithZone:(struct _NSZone *)zone {
      3.    static dispatch_once_t onceToken;
      4.    dispatch_once(&onceToken, ^{
      5.        instance = [super allocWithZone:zone];
      6.    });
      7.    return instance;
      8. }
      9. + (instancetype)sharedDownloaderManger {
      10.    return [[self alloc] init];
      11. }
    • 下载,调用下载器的下载方法

    • 定义缓存池,当下载开始,把下载器缓存起来

    • 下载之前先判断缓存池中是否有此下载操作

    • 下载完成或失败后,从缓存池移除下载操作-----最终解决重复下载的问题

    •      
           
      1. - (void)pauseWithUrlString:(NSString *)urlStr {
      2.    SMHDownloader *downloader = self.dictCache[urlStr];
      3.    [downloader pause];
      4.    [self.dictCache removeObjectForKey:urlStr];
      5. }
      6. - (NSMutableDictionary *)dictCache {
      7.    if (_dictCache == nil) {
      8.        _dictCache = [NSMutableDictionary dictionaryWithCapacity:5];
      9.    }
      10.    return _dictCache;
      11. }
      12. - (void)downloadWithUrlString:(NSString *)urlStr success:(void (^)(NSString *))successBlock process:(void (^)(float))processBlock error:(void (^)(NSError *))errorBlock {
      13.    if (self.dictCache[urlStr]) {
      14.        NSLog(@"正在下载");
      15.        return;
      16.    }
      17.    SMHDownloader *downloader = [[SMHDownloader alloc] init];
      18.    [self.dictCache setObject:downloader forKey:urlStr];
      19.    [downloader downloadWithUrlString:urlStr success:^(NSString *path) {
      20.        [self.dictCache removeObjectForKey:urlStr];
      21.        if (successBlock) {
      22.            successBlock(path);
      23.        }
      24.    } process:processBlock error:^(NSError *error) {
      25.        [self.dictCache removeObjectForKey:urlStr];
      26.        if (errorBlock) {
      27.            errorBlock(error);
      28.        }
      29.    }];
      30. }


Downloader 改成NSOpration


  • 改成NSOpration的好处:

    • 可以设置最大并发数,限制下载文件的个数

    • 可以设置依赖,让一个下载在另一个下载后面执行

    • 可以暂停正在下载的任务

  
  
  1. - (void)main {
  2.    NSURL *url = [NSURL URLWithString:self.urlStr];
  3.    [self checkServerInfo:url];
  4.    long long fileSize = [self checkLocalInfo];
  5.    if (fileSize == self.expectedContentLength) {
  6.        NSLog(@"文件已经下载");
  7.        if (self.successBlock) {
  8.            dispatch_async(dispatch_get_main_queue(), ^{
  9.                self.successBlock(self.filePath);
  10.            });
  11.        }
  12.        
  13.        return;
  14.    }
  15.    self.currentContentLength = fileSize;
  16.    [self downloadWithUrl:url offset:fileSize];
  17. }
  18. + (instancetype)downloaderWithUrlString:(NSString *)urlStr success:(void (^)(NSString *))successBlock process:(void (^)(float))processBlock error:(void (^)(NSError *))errorBlock {
  19.    SMHDownloader *downloader = [[self alloc] init];
  20.    downloader.successBlock = successBlock;
  21.    downloader.processBlock = processBlock;
  22.    downloader.errorBlock = errorBlock;
  23.    
  24.    downloader.urlStr = urlStr;
  25.    return downloader;
  26. }
  • 取消下载操作
  •     
        
    1. - (void)pauseWithUrlString:(NSString *)urlStr {
    2.    
    3.    SMHDownloader *downloader = self.dictCache[urlStr];
    4.    if (downloader == nil) {
    5.        NSLog(@"没有此操作");
    6.        return;
    7.    }
    8.    [downloader pause];
    9.    [self.dictCache removeObjectForKey:urlStr];
    10. }
  • 下载操作还要取消正在执行的操作
  • 在main方法的比较耗时的操作后面加上下面代码
  •     
        
    1.    //取消正在下载的操作
    2.    if (self.isCancelled) {
    3.        return;
    4.    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值