您的位置:首页 > 移动开发 > IOS开发

iOS断点续传

2016-01-14 01:06 441 查看
基本思路:    

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

如果没有本地文件,下载

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

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

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

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

检查服务器文件

    获取服务器上的文件信息

    
//获取服务器上的文件大小和文件名

- (void)checkServerInfo:(NSURL *)url {

   NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

   request.HTTPMethod = @"HEAD";

   NSURLResponse *response = nil;

   [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];

   self.expectedContentLength = response.expectedContentLength;

   self.filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];

}


检查本地文件

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

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

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

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

- (long long)checkLocalInfo {

   NSFileManager *fileManger = [NSFileManager defaultManager];

   //检查文件是否已存在

   if (![fileManger fileExistsAtPath:self.filePath]) {

     return 0;

   }

   //获取本地文件的大小

   NSDictionary *fileAttrs = [fileManger attributesOfItemAtPath:self.filePath error:NULL];

   long long fileSize = fileAttrs.fileSize;

   if (fileSize > self.expectedContentLength) {

     [fileManger removeItemAtPath:self.filePath error:NULL];

     return 0;

   }

   return fileSize;

}


下载

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

long long fileSize = [self checkLocalInfo];

   if (fileSize == self.expectedContentLength) {

     NSLog(@"文件已经下载");

     return;

   }


从指定偏移处开始下载

range 取值

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

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

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

//从指定偏移处下载文件

- (void)downloadWithUrl:(NSURL *)url offset:(long long)offset {

   NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

   /*

    range 取值

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

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

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

    */

   [request setValue:[NSString stringWithFormat:@"bytes=%lld",offset] forHTTPHeaderField:@"range"];

   NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];

}


异步下载

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

异步下载

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

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

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

- (void)downloadWithUrl:(NSURL *)url offset:(long long)offset {

   [[NSOperationQueue new] addOperationWithBlock:^{

      NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

      /*

       range 取值

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

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

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

       */

     [request setValue:[NSString stringWithFormat:@"bytes=%lld-",offset] forHTTPHeaderField:@"range"];

      NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];

     //启动消息循环

     [[NSRunLoop currentRunLoop] run];

   }];

}


下载完成后的回调

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

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

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

定义block的属性

@property (nonatomic, copy) void(^successBlock)(NSString *path);

@property (nonatomic, copy) void(^processBlock)(float process);

@property (nonatomic, copy) void(^errorBlock)(NSError *error);


下载方法中给block属性赋值

- (void)downloadWithUrlString:(NSString *)urlStr success:(void (^)(NSString *))successBlock process:(void (^)(float))processBlock error:(void (^)(NSError *))errorBlock {

   self.successBlock = successBlock;

   self.processBlock = processBlock;

   self.errorBlock = errorBlock;


对应的位置调用回调方法

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

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {

   [self.stream write:data.bytes maxLength:data.length];

   self.currentContentLength += data.length;

   

   float process = self.currentContentLength * 1.0 / self.expectedContentLength;

   if (self.processBlock) {

      self.processBlock(process);

   }

}


URLConnection的下载完成方法中调用完成的回调,下载完成会回归主线程调用,在主线程更新界面

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {

   [self.stream close];


   if (self.successBlock) {

      dispatch_async(dispatch_get_main_queue(), ^{

         self.successBlock(self.filePath);

      });

   }

}


URLConnection的下载出错的方法中调用出错的回调,在那个线程执行由调用者决定

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {

   [self.stream close];

   

   if (self.errorBlock) {

      self.errorBlock(error);

   }

}


如果文件已经存在,也要调用完成的方法

   if (fileSize == self.expectedContentLength) {

     NSLog(@"文件已经下载");

      if (self.successBlock) {

         dispatch_async(dispatch_get_main_queue(), ^{

            self.successBlock(self.filePath);

         });

      }

      

     return;

   }


controller中调用

下载进度提示

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

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

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

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

//重写progress的set方法

- (void)setProgress:(float)progress {

   _progress = progress;

   [self setTitle:[NSString stringWithFormat:@"0.2f%%%",progress * 100] forState:UIControlStateNormal];

   [self setNeedsDisplay];

}


重写drawRect方法,根据progress画圆

#define kLINEWIDTH 5

- (void)drawRect:(CGRect)rect {

   UIBezierPath *path = [UIBezierPath bezierPath];

   CGPoint center = CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5);

   CGFloat radius = MIN(center.x, center.y) - kLINEWIDTH;

   CGFloat startA = -M_PI_2;

   CGFloat endA = startA + self.progress * 2 * M_PI;

   [path addArcWithCenter:center radius:radius startAngle:startA endAngle:endA clockwise:YES];

   

   path.lineCapStyle = kCGLineCapRound;

   path.lineWidth = kLINEWIDTH;

   [[UIColor orangeColor] setStroke];

   [path stroke];

}


controller中显示进度

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

process:^(float process) {

      dispatch_async(dispatch_get_main_queue(), ^{

       self.progressView.progress = process;

      });

      

   } 


在storyboard中设置自定义类的属性

IB

暂停下载

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

- (void)pause {

   [self.conn cancel];

}


代码重构

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

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

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

下载管理类和缓存池

创建下载的管理类DownloaderManger单例

static id instance = nil;

+ (instancetype)allocWithZone:(struct _NSZone *)zone {

   static dispatch_once_t onceToken;

   dispatch_once(&onceToken, ^{

      instance = [super allocWithZone:zone];

   });

   return instance;

}

+ (instancetype)sharedDownloaderManger {

   return [[self alloc] init];

}


下载,调用下载器的下载方法

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

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

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

- (void)pauseWithUrlString:(NSString *)urlStr {

   SMHDownloader *downloader = self.dictCache[urlStr];

   [downloader pause];

   [self.dictCache removeObjectForKey:urlStr];

}


- (NSMutableDictionary *)dictCache {

   if (_dictCache == nil) {

      _dictCache = [NSMutableDictionary dictionaryWithCapacity:5];

   }

   return _dictCache;

}


- (void)downloadWithUrlString:(NSString *)urlStr success:(void (^)(NSString *))successBlock process:(void (^)(float))processBlock error:(void (^)(NSError *))errorBlock {

   if (self.dictCache[urlStr]) {

      NSLog(@"正在下载");

     return;

   }

   SMHDownloader *downloader = [[SMHDownloader alloc] init];

   [self.dictCache setObject:downloader forKey:urlStr];

   [downloader downloadWithUrlString:urlStr success:^(NSString *path) {

      [self.dictCache removeObjectForKey:urlStr];

      if (successBlock) {

         successBlock(path);

      }

   } process:processBlock error:^(NSError *error) {

      [self.dictCache removeObjectForKey:urlStr];

      if (errorBlock) {

         errorBlock(error);

      }

   }];

}


Downloader 改成NSOpration


改成NSOpration的好处:

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

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

可以暂停正在下载的任务

- (void)main {

   NSURL *url = [NSURL URLWithString:self.urlStr];

   [self checkServerInfo:url];

   long long fileSize = [self checkLocalInfo];

   if (fileSize == self.expectedContentLength) {

     NSLog(@"文件已经下载");

      if (self.successBlock) {

         dispatch_async(dispatch_get_main_queue(), ^{

            self.successBlock(self.filePath);

         });

      }

      

     return;

   }

   self.currentContentLength = fileSize;

   [self downloadWithUrl:url offset:fileSize];


}


+ (instancetype)downloaderWithUrlString:(NSString *)urlStr success:(void (^)(NSString *))successBlock process:(void (^)(float))processBlock error:(void (^)(NSError *))errorBlock {

   SMHDownloader *downloader = [[self alloc] init];

   downloader.successBlock = successBlock;

   downloader.processBlock = processBlock;

   downloader.errorBlock = errorBlock;

   

   downloader.urlStr = urlStr;

   return downloader;

}


取消下载操作

- (void)pauseWithUrlString:(NSString *)urlStr {

   

   SMHDownloader *downloader = self.dictCache[urlStr];

   if (downloader == nil) {

      NSLog(@"没有此操作");

     return;

   }

   [downloader pause];

   [self.dictCache removeObjectForKey:urlStr];

}


下载操作还要取消正在执行的操作

在main方法的比较耗时的操作后面加上下面代码

   //取消正在下载的操作

   if (self.isCancelled) {

     return;

   }
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: