iOS 基础--大文件的下载(断点续传)

古人学问无遗力,少壮工夫老始成!<佛烈托斯>

准备写一个下载的基础总结,发现 简·友 【 xx_cc】 这篇总结写的很好了大家可以一起看看,我分享其中我也用过的方法分析一个下载的小Demo!哎,暂时不干 iOS 抽时间和大家一起学习,有错误地方还请大家指正!GitHub


效果图:


大文件下载.gif
  • 使用NSURLSessionDataTask、NSURLSessionDataDelegate下载大文件思路:

1、下载的时候直接把下载的文件放到沙盒中,可以避免零时变量占用较大的内存同时不会因为中断下载而导致前面下载的内容丢失,这里直接用输出流将数据不断的放到指定沙盒位置!
2、为了实现断点续传,创建下载任务的时候会根据 URL 去找对象的沙盒位置中有没有该文件

  • 如果 有 判断下载多少了,如果是全部下载完成就不需要再去下载,要是下了一部分就设置从已经下载好了之后开始下载。
  • 如果没有的话,直接开始下载并且把文件的总大小记录到沙盒中,以供相应的判断

3、这里存储位置我们可以自己决定,但是文件的名字主要是要和 URL 一一对应,我这里使用下载地址 MD5 转化之后的字符串 加上自己的后缀(类型) 作为文件名。这样每次去比较的的时候就是使用 URL 进行相应的转换即可!

  • 补充一个流的概念

Stream 翻译成为流,它是对我们读写文件的一个抽象,是把文件的内容,一小段一小段的读出或 写入,来到达这样的效果

  • NSStream
    NSStream 是Cocoa平台下对流这个概念的实现类, NSInputStream 和 NSOutputStream 则是它的两个子类,分别对应了读文件和 写文件。
  • NSInputStream
    NSInputStream 对应的是读文件,所以要记住它是要将文件的内容读到内存(你声明的一段buffer)里
  • NSOutputStream
    NSOutputStream 对应的是写文件,它是要将已存在的内存(buffer)里的数据写入文件

代码部分:

1、遵循代理及相关的属性

@interface PP_DownLoad ()<NSURLSessionDataDelegate>

@property(nonatomic,strong)NSOutputStream *stream;// 输出流(对应写入文件)
@property(nonatomic,assign)NSInteger totalLength;// 文件总大小
@property(nonatomic,assign)NSInteger currentLength;// 已经下载大小
@property(nonatomic,strong)NSURLSession *session;
@property(nonatomic,strong)NSURLSessionDataTask *dataTask;
@property (nonatomic, strong) NSString *fileName; // 文件保存名字
@property (nonatomic, strong) NSString *urlString; // 下载路径
@property (nonatomic, strong) NSString *fileLengthName; // 存储文件长度的名字

@end```

2、根据URL创建 任务
```code
-(NSURLSessionDataTask *)dataTaskWithUrlStr:(NSString *)urlString
{
    /*
     * 先去看看已经下载了多少,然后设置从已经下载之后的开始下载!
     */
    if (_dataTask == nil) {
        self.urlString = urlString;
        self.currentLength = [self getCurrent];
        NSURL *url =[NSURL URLWithString:urlString];
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        NSString *range =[NSString stringWithFormat:@"bytes=%zd-",self.currentLength];// %zd表示 size_t类型进行输出
        [request setValue:range forHTTPHeaderField:@"Range"];
        self.dataTask = [self.session dataTaskWithRequest:request];
        
    }
    return self.dataTask;
}```
3、 计算对应下载的文件大小
```code
-(NSInteger )getCurrent
{
    NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [caches stringByAppendingPathComponent:self.fileName];
    NSFileManager *manager = [NSFileManager defaultManager];
    NSDictionary *dict = [manager attributesOfItemAtPath:filePath error:nil];
    return [dict[@"NSFileSize"] integerValue];
}```
4、确定存储文件名称  用下载地址 MD5 转化之后的字符串  加上自己的后缀 作为文件名
```code
- (NSString *)fileName
{
    NSArray *prefix_Suffix = [_urlString componentsSeparatedByString:@"."];
    _fileName = [[self getMD5String:_urlString] stringByAppendingFormat:@".%@",[prefix_Suffix lastObject]];
    
    return _fileName;
}
- (NSString *)fileLengthName
{
    return [NSString stringWithFormat:@"%@.txt",self.fileName];
}```

5、 把字符串转化成 MD5字符串 去掉特殊的标记
```code
- (NSString *)getMD5String:(NSString *)string
{
    // 转成 C 语言的字符串
    const char *mdData = [string UTF8String];
    unsigned char result[CC_MD5_DIGEST_LENGTH];
   
    CC_MD5(mdData, (CC_LONG)strlen(mdData), result);
    
    // 化成 OC 可变 字符串
    NSMutableString *mdString  = [NSMutableString new];
    for (int i =0 ; i < CC_MD5_DIGEST_LENGTH; i++)
    {
        [mdString appendFormat:@"%02X",result[i]];
    }
    return mdString;
}```

##### 下载部分:
1、 设置代理
```code
-(NSURLSession *)session
{
    if (_session == nil) {
        // 使用代理方法请求
        /**
         参数一:配置信息
         参数二:代理
         参数三:控制代理方法在那个队列中调用
         遵守代理:NSURLSessionDataDelegate
         */
        _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}```

2、设置文件总数
```code
-(void)saveTotal:(NSInteger )length
{
    NSLog(@"开始存储文件大小");
    NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [caches stringByAppendingPathComponent:self.fileLengthName];
    // 把下载文件的总大小  存在沙盒的缓存里面
    
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    [dict setObject:@(length) forKey:self.fileLengthName];
    [dict writeToFile:filePath atomically:YES];
}```

####下载代理部分
- 接收到服务器响应的时候调用
```code
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    // 拿到文件总大小 获得的是当次请求的数据大小,当我们关闭程序以后重新运行,开下载请求的数据是不同的 ,所以要加上之前已经下载过的内容
    NSLog(@"接收到服务器响应");
    self.totalLength = response.expectedContentLength + self.currentLength;
    
    // 把文件总大小保存的沙盒 没有必要每次都存储一次,只有当第一次接收到响应,self.currentLength为零时,存储文件总大小就可以了
    if (self.currentLength == 0) {
        [self saveTotal:self.totalLength];
    }
    NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
    NSString *filePath = [caches stringByAppendingPathComponent:self.fileName];
    NSLog(@"%@",filePath);
    
    // 创建输出流 如果没有文件会创建文件,YES:会往后面进行追加
    NSOutputStream *stream = [[NSOutputStream alloc] initToFileAtPath:filePath append:YES];
    [stream open];
    self.stream = stream;
    //NSLog(@"didReceiveResponse 接受到服务器响应");
    completionHandler(NSURLSessionResponseAllow);
    
    // 调用自己定义下载类的代理方法  供外界获取下载情况(这儿用了代理和 Block 两个方法一样的目的,就是练练手省的生疏了)
    [self.delegate pp_DownLoad:self startFilePath:self.fileName hasLoadLength:self.currentLength totalLength:self.totalLength];
    self.startBlock(self.fileName,self.currentLength,self.totalLength);
}```
- 接收到服务器返回数据时调用,会调用多次
```code
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    self.currentLength += data.length;
    // 输出流 写数据
    [self.stream write:data.bytes maxLength:data.length];
    //NSLog(@"下载了百分比---->%f %%",1.0 * self.currentLength / self.totalLength * 100);
    NSLog(@"didReceiveData 接受到服务器返回数据");
    // 回调代理方法
    [self.delegate pp_DownLoad:self progressCurrent:self.currentLength totalLength:self.totalLength];
    self.progressBlock(self.currentLength,self.totalLength);
}```
- 当请求完成之后调用,如果请求失败error有值
```code
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    // 关闭stream
    [self.stream close];
    self.stream = nil;
    NSLog(@"didCompleteWithError 请求完成");
    // 下载完成回调
    [self.delegate pp_DownLoad:self didCompleteWithError:error];
    self.competeBlock(error);
}```

##### 下载执行 :

1、 开始下载
```code
- (void)startDownLoad:(NSString *)downLoadUrl
{
    [[self dataTaskWithUrlStr:downLoadUrl] resume];
}```
1.1 开始下载包含 Block 回调的方法
```code
- (void)startDownLoad:(NSString *)downLoadUrl
       WithStartBlock:(StartLoadBlock)startBlock
        progressBlock:(ProgressBlock)progressBlock
     didCompleteBlock:(CompleteBlock)competeBlock
{
    [[self dataTaskWithUrlStr:downLoadUrl] resume];
    self.startBlock = startBlock;
    self.progressBlock = progressBlock;
    self.competeBlock = competeBlock;
}```
2、暂停下载
```code
- (void)stopDownLoad
{
    [self.dataTask suspend];
}```

-----------
补充在.h中的自定义的协议和 Block
```code
@protocol PP_DownLoadDelegate <NSObject>

// 开始下载
- (void)pp_DownLoad:(PP_DownLoad *)pp_DownLoad
      startFilePath:(NSString *)filePath
      hasLoadLength:(NSInteger)hasLoadLength
        totalLength:(NSInteger)totalLength;

// 获取下载进度
- (void)pp_DownLoad:(PP_DownLoad *)pp_DownLoad
    progressCurrent:(NSInteger)currentLength
        totalLength:(NSInteger)totalLength;
// 下载完成
- (void)pp_DownLoad:(PP_DownLoad *)pp_DownLoad didCompleteWithError:(NSError *)error;
@end

@property (nonatomic, copy) StartLoadBlock startBlock ; // 开始下载回调
@property (nonatomic, copy) ProgressBlock progressBlock ; // 更新数据回调
@property (nonatomic, copy) CompleteBlock competeBlock ;// 下载完成回调```

---------
先写到这儿,慢慢补充!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容