总体内容

在学习下面的知识前请在 mac电脑搭建一下 Apache 服务器

1、NSURLConncetion 下载

2、NSURLSession下载大文件

3、下载管理器(多文件下载)

一、NSURLConncetion 下载

1.1、我们先使用NSURLConncetion 下载一个视频试试,完整代码在demo中的 Test1ViewController

视频连接:@"http://images.ciotimes.com/2ittt-zm.mp4"

<1>、对视频链接进行编码

在iOS程序中,访问一些http/https的资源服务时,如果url中存在中文或者特殊字符时,会导致无法正常的访问到资源或服务,想要解决这个问题,需要对url进行编码。

NSString *urlStr = @"http://images.ciotimes.com/2ittt-zm.mp4";

// 在 iOS9 之后废弃了
// urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// iOS9 之后取代上面的 api
urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];

<2>、string 转 NSURL

NSURL *url = [NSURL URLWithString:urlStr];

<3>、创建 NSURLRequest 对象

NSURLRequest *request = [NSURLRequest requestWithURL:url];

<4>、NSURLConnection 下载 视频

// iOS9 之后废弃了
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
   completionHandler:^(NSURLResponse * _Nullable response,
   NSData * _Nullable data, NSError * _Nullable connectionError) {

      // 下载视频的名字
      NSString *videoName =  [urlStr lastPathComponent];
      // 下载到桌面的文件夹 JK视频下载器
      NSString *downPath = [NSString stringWithFormat:@"/Users/wangchong/Desktop/JK视频下载器/%@",videoName];
      // 将数据写入到硬盘
      [data writeToFile:downPath atomically:YES];

      NSLog(@"下载完成");
}];

提示:NSURLConnection 在iOS 2.0之后就有了,sendAsynchronousRequest这个方法是在 iOS5.0 之后出现的

完整的代码

// 1、对视频链接进行编码
NSString *urlStr = @"http://images.ciotimes.com/2ittt-zm.mp4";
// iOS9 之后的 api
urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];

// 2、string 转 NSURL
NSURL *url = [NSURL URLWithString:urlStr];

// 3、创建 NSURLRequest 对象
NSURLRequest *request = [NSURLRequest requestWithURL:url];

// 4、NSURLConnection 下载 视频
// iOS9 之后废弃了
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
    completionHandler:^(NSURLResponse * _Nullable response,
    NSData * _Nullable data, NSError * _Nullable connectionError) {

      // 下载视频的名字
      NSString *videoName =  [urlStr lastPathComponent];
      // 下载到桌面的文件夹 JK视频下载器
      NSString *downPath = [NSString stringWithFormat:@"/Users/wangchong/Desktop/JK视频下载器/%@",videoName];
      // 将数据写入到硬盘
      [data writeToFile:downPath atomically:YES];

      NSLog(@"下载完成");
}];

上面下载会出现的两个问题:

  • (1)、没有下载进度,会影响用户的体验

  • (2)、内存偏高,会有最大峰值(一次性把数据写入),内存隐患

608437dcc60be301f25552c68d6adee0.png

0ff162cf7bfac4bdb88757fe48704095.png

1.2、NSURLConnection 进度监听,完整代码在demo中的 Test2ViewController

(1)、在 1.1 里面我们使用的是 NSURLConnection 的block方法进行的下载,会有下载没有进度和出现峰值的问题,那么下面我们就使用 NSURLConnection 的代理方法来解决这些问题

  • 下载没有进度的解决办法:通过代理来解决

  • 进度跟进:在响应头中获取文件的总大小,在每次接收数据的时候计算数据的比例

(2)、代理方法选择 NSURLConnectionDataDelegate,其他两个的NSURLConnection代理方法都是不对的

bfc8b99d965d42a89c02f4233fbc8d99.png

代理方法选择 `NSURLConnectionDataDelegate`

(3)、定义一个记录总视频大小的属性和接收到的数据包或者下载的数据总大小

/** 要下载的文件总大小 */
@property(nonatomic,assign) long long exceptedContentLength;

/** 当前已经下载的文件总大小 */
@property(nonatomic,assign) long long currentDownContentLength;

提示:类型要选择 long long,系统使用的就是这个类型

(4)、常用的代理方法

// 1、接收服务器的响应 --- 状态和响应头做一些准备工作
// expectedContentLength : 文件的总大小
// suggestedFilename : 服务器建议保存的名字
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{

     // 记录文件的总大小
     self.exceptedContentLength = response.expectedContentLength;
     // 当前下载的文件大小初始化为 0
     self.currentDownContentLength = 0;
     NSLog(@"\nURL=%@\nMIMEType=%@\ntextEncodingName=%@\nsuggestedFilename=%@",response.URL,response.MIMEType,response.textEncodingName,response.suggestedFilename);
}

// 2、接收服务器的数据,由于数据是分块发送的,这个代理方法会被执行多次,因此我们也会拿到多个data
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{

     NSLog(@"接收到的数据长度=%tu",data.length);
     // 计算百分比
     // progress = (float)long long / long long
     float progress = (float)self.currentDownContentLength/self.exceptedContentLength;

     JKLog(@"下载的进度=%f",progress);
}

// 3、接收到所有的数据加载完毕后会有一个通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{

     NSLog(@"下载完毕");
}

// 4、下载错误的处理
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{

      NSLog(@"链接失败");
}

提示:计算百分比 progress = (float)long long / long long; 要记得转换类型,两个整数相除的结果是不会有小数的,转成 float就好

1.3、拼接数据然后写入磁盘(不可取,比 1.1  更严重),完整代码在demo中的 Test3ViewController

  • 由于在 1.1 中出现的 峰值 问题,在这里来解决一下,两种方式尝试

    第一种: 从服务器获取完 数据包 data 后一次性写入磁盘

    第二种:获取一个数据包就写入一次磁盘

(1)、定义视频下载到的路径以及数据的data

/**
  保存下载视频的路径
*/
@property(nonatomic,strong) NSString *downFilePath;
/**
  保存视频数据
*/
@property(nonatomic,strong) NSMutableData *fileData;

-(NSMutableData *)fileData{

     if (!_fileData) {

          _fileData = [[NSMutableData alloc]init];
     }
    return _fileData;
}

(2)、代理中的方法

// 1、接收服务器的响应 --- 状态和响应头做一些准备工作
// expectedContentLength : 文件的总大小
// suggestedFilename : 服务器建议保存的名字
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{

    // 记录文件的总大小
    self.exceptedContentLength = response.expectedContentLength;
    // 当前下载的文件大小初始化为 0
    self.currentDownContentLength = 0;

    // 创建下载的路径
    self.downFilePath = [@"/Users/wangchong/Desktop/JK视频下载器" stringByAppendingPathComponent:response.suggestedFilename];

}

// 2、接收服务器的数据,由于数据是分块发送的,这个代理方法会被执行多次,因此我们也会拿到多个data
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{

    // JKLog(@"接收到的数据长度=%tu",data.length);
    self.currentDownContentLength += data.length;

    // 计算百分比
    // progress = (float)long long / long long
    float progress = (float)self.currentDownContentLength/self.exceptedContentLength;

    JKLog(@"下载的进度=%f",progress);

    self.progressLabel.text = [NSString stringWithFormat:@"下载进度:%f",progress];

    // 拼接每次获取到服务器的数据包 data
    [self.fileData appendData:data];

}

// 3、接收到所有的数据加载完毕后会有一个通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{

     JKLog(@"下载完毕");

     // 数据获取完,写入磁盘
     [self.fileData writeToFile:self.downFilePath atomically:YES];
}

// 4、下载错误的处理
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{

      JKLog(@"链接失败");
}

分析:

第一种: 从服务器获取完 数据包 data 后一次性写入磁盘的问题:不仅仅会出现峰值的问题,由于 fileData是强引用无法释放,会造成内存暴增,由此可以看出和1.1中的异步效果一样,应该是苹果底层的实现方式

ca1416a0228b4e14516ada24a8abecfa.png

内存暴增

1.4、NSFileHandle数据包边下载边写入磁盘,完整代码在demo中的 Test4ViewController

提起NSFileHandle,我们老解释一下它与NSFileManager的区别

  • NSFileManager:主要的功能是创建目录、检查目录是否存在、遍历目录、删除文件、拷贝文件、剪切文件等等,主要是针对文件的操作

  • NSFileHandle:文件"句柄",对文件的操作,主要功能是:对同一个文件进行二进制读写

(1)、我们写一个写入数据的方法,如下

// 把数据写入到磁盘的方法
-(void)writeFileData:(NSData *)data{

     NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:self.downFilePath];

     // 如果文件不存在,直接x将数据写入磁盘
     if (fp == nil) {

           [data writeToFile:self.downFilePath atomically:YES];
     }else{

           // 如果存在,将data追加到现在文件的末尾
           [fp seekToEndOfFile];
           // 写入文件
           [fp writeData:data];
           // 关闭文件
           [fp closeFile];
      }
}

提示:通过测试,边下载边写入磁盘解决了峰值的问题

(2)、如何判断文件是否下载完成 ?

答:判断进度?判断完成通知?,判断时间?判断大小?这些都不太好,比较好的方式是使用MD5

MD5:

  • <1>.服务器对你下载的文件计算好一个MD5,将此 MD5 传给客户端

  • <2>.开始下载文件......

  • <3>.下载完成时,对下载的文件做一次MD5

  • <4>.比较服务器返回的MD5和我们自己计算的MD5,如果二者相等,就代表下载完成

1.5、NSOutputStream 拼接文件,完整代码在demo中的 Test5ViewController

(1)、创建一个保存文件的输出流 NSOutputStream 属性

/* 保存文件的输出流
   - (void)open; 写入之前,打开流
   - (void)close; 写入完毕之后,关闭流
 */
@property(nonatomic,strong)NSOutputStream *fileStream;

(2)、创建输出流并打开

// 创建输出流
self.fileStream = [[NSOutputStream alloc]initToFileAtPath:self.downFilePath append:YES];
[self.fileStream open];

(3)、将数据拼接起来,并判断是否可写如,一般情况下可写入,除非磁盘空间不足

// 判断是否有空间可写
if ([self.fileStream hasSpaceAvailable]) {

     [self.fileStream write:data.bytes maxLength:data.length];
}

(4)、关闭文件流

接收到所有的数据加载完毕后会有一个通知

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

      NSLog(@"下载完毕");
      [self.fileStream close];
}

(5)、把NSURLConncetion放到子线程,但是虽然写入的操作是在子线程,但是默认的connection 是在主线程工作,指定了代理的工作的队列之后,整个下载还是在主线程 。UI事件能够卡住下载

NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:self];

// 设置代理工作的操作 [[NSOperationQueue alloc]init] 默认创建一个异步并发队列
[connection setDelegateQueue:[[NSOperationQueue alloc]init]];
[connection start];

1.6、解决1.5中 NSURLConncetion的下载在主线程的问题,完整代码在demo中的 Test6ViewController

(1)、将网络操作放在异步线程,异步的运行循环默认不启动,没有办法监听接下来的网络事件

dispatch_async(dispatch_get_global_queue(0, 0), ^{

    // 1、对视频链接进行编码
    // 在iOS程序中,访问一些HTTP/HTTPS的资源服务时,如果url中存在中文或者特殊字符时,会导致无法正常的访问到资源或服务,想要解决这个问题,需要对url进行编码。
    NSString *urlStr = @"http://images.ciotimes.com/2ittt-zm.mp4";
    urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];

    // 2、string 转 NSURL
    NSURL *url = [NSURL URLWithString:urlStr];

    // 3、创建 NSURLRequest 对象
    NSURLRequest *request = [NSURLRequest requestWithURL:url];

    // 4、NSURLConnection 下载 视频
    /**
       By default, for the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.
       为了保证链接工作正常,调用线程的RunLoop,必须在默认的运行循环模式下
     */
    NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:self];

    // 设置代理工作的操作 [[NSOperationQueue alloc]init] 默认创建一个异步并发队列
    [connection setDelegateQueue:[[NSOperationQueue alloc]init]];

    [connection start];
});

分析:上面的代码是有很大的问题的,子线程执行完后会直接死掉,不会继续执行 start 后面的操作,也就是说没有办法下载;解决办法是给子线程创建 Runloop

(2)、定义一个保存下载线程的运行循环

@property(nonatomic,assign)CFRunLoopRef downloadRunloop;

(3)、在 [connection start];之后启动我们创建的子线程可以活下去的Runloop

/*
   CoreFoundation 框架 CFRunLoop
   CFRunloopStop() 停止指定的runloop
   CFRunloopGetCurrent() 获取当前的Runloop
   CFRunloopRun() 直接启动当前的运行循环
 */

//1、拿到当前的运行循环
self.downloadRunloop = CFRunLoopGetCurrent();

//2.启动当前的运行循环
CFRunLoopRun();

(4)、在下载完成后停止下载线程所在的runloop

// 所有数据加载完毕--所有数据加载完毕,会一个通知!
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"完毕!%@",[NSThread currentThread]);

    //关闭文件流
    [self.fileStream close];

    //停止下载线程所在的runloop
    CFRunLoopStop(self.downloadRunloop);
}

二、NSURLSession 下载大文件,以下测试我们使用Apache 服务器里面的数据

9392b2f845bb28daeba907917ae86633.png

Apache 服务器里面的数据

2.1、NSURLSession 简介 以及 简单使用,完整代码在JKNSURLSession中的 Test1ViewController

NSURLSession是在iOS 7.0(15年)的时候推出的,在最开始的时候也会出现峰值,后来解决后大家才重新使用NSURLSession,NSURLSession所有的任务都是session发起的,默认所有任务都是“挂起”的,需要resume执行。

简单的使用:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

     // 1.创建url: http://localhost/test.json
     NSURL *url = [NSURL URLWithString:@"http://localhost/test.json"];
     [self taskWithUrl:url];
}

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

     [[[NSURLSession sharedSession]dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

        // 反序列化
        id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];

         NSLog(@"%@",result);
     }] resume];
}

提示:NSURLSession 是一个单利,目的是使开发更容易,默认是不启动的,需要开发者调用  resume 启动 NSURLSession,如上面

2.2、NSURLSession 简单的下载,完整代码在JKNSURLSession中的 Test2ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

     // 1.创建url:http://localhost
     NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"];
     [self taskWithUrl:url];
}

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

    /*
      如果在回调方法中,不做任何处理,下载的文件会被删除
      下载是默认下载到tmp文件夹,系统会自动回收这个区域
      设计目的:
      1.通常从网络下载文件,什么样的格式文件最多?zip
      2.如果是zip包,下载之后要解压
      3.解压之后,原始的zip就不需要了。系统会自动帮你删除
     */

     [[[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {

           NSLog(@"%@",location);

     }]resume];
}

提示:location打印是:file:///Users/wangchong/Library/Developer/CoreSimulator/Devices/643379A0-0449-4FE2-AD19-71258BDDBAE6/data/Containers/Data/Application/E6F1AABA-BDBE-4191-A167-02D5DCD19D41/tmp/CFNetworkDownload_OaisFm.tmp,我们可以看到 tmp,临时存放下载文件的地方

2.3、文件解压缩,完整代码在JKNSURLSession中的 Test3ViewController

(1)、这里我们需要使用一个工具SSZipArchive,在demo里面有

62ab1b3d07e5378dad80f2821f103e58.png

SSZipArchive

SSZipArchive的功能:压缩文件 和 解压文件

(2)、解压我们服务器的一个文件到 Library/Caches里面

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    // 1.创建url:http://localhost
    NSURL *url = [NSURL URLWithString:@"http://localhost/ftp.docx.zip"];
    [self taskWithUrl:url];
}

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

    [[[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {

         NSLog(@"%@",location);
         // 文件解压目标路径,不能指定目标文件。因为我们解压文件会产生多个文件
         NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)lastObject];
         [SSZipArchive unzipFileAtPath:location.path toDestination:cachePath];

    }]resume];
}

83fd55bad33d5ee011a2d70afd135af9.png

解压文件

2.4、NSURLSession下载进度监听,完整代码在JKNSURLSession中的 Test4ViewController

(1)、创建一个 NSURLSession对象

// 全局的网络会话,管理所有的网络任务
@property(nonatomic,strong) NSURLSession *session;

-(NSURLSession *)session{

     if (!_session) {
          /**
           全局网络环境的一个配置
           比如:身份验证,浏览器类型以及缓存,超时,这些都会被记录在
          */
          NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
          _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
      }
     return _session;
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

       // 1.创建url:http://localhost
       NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"];
       //如果你要监听下载进度,必须使用代理。
       //如果你要更进下载进度,就不能block 。
       [[self.session downloadTaskWithURL:url]resume];
}

提示:

  • 如果你要监听下载进度,必须使用代理。

  • [NSURLSession sharedSession] 是全局的单例。整个系统都会用,也就是其他的应用程序也会用

  • 如果你要更进下载进度,就不能block 。

(2)、常用的代理方法(其中下载完成的方法是在iOS7.0之后必须要写的,在iOS7之前,下面的三个方法都必须写)

/**
   1、下载完成的方法
 */
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{

     NSLog(@"下载完成");
}

/**
  2、下载续传数据的方法
*/
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{


 }

/**
  3、下载进度的方法
  @param session 网络会话
  @param downloadTask 调用代理方式的下载任务
  @param bytesWritten 本次下载的字节数
  @param totalBytesWritten 已经下载的字节数
  @param totalBytesExpectedToWrite 期望下载的字节数 -- 文件的总大小
 */
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{

     float progress = (float)totalBytesWritten/totalBytesExpectedToWrite;
     NSLog(@"进度=%f",progress);
}

2.5、自定义progressview,完整代码在JKNSURLSession中的 Test5ViewController

(1)、自定义progressview我们选择继承于 UIButton,原因是:button可以设置文字,展示的时候比较方便,当然也可以使用其他的控件,比如lable,那么我们自定义一个类 JKProgressBtn 继承于UIButton,代码如下

  • JKProgressBtn.h里面的代码

#import 

NS_ASSUME_NONNULL_BEGIN

@interface JKProgressBtn : UIButton
/**
   表示进度的值
*/
@property(nonatomic,assign) float progress;

@end

NS_ASSUME_NONNULL_END
  • JKProgressBtn.m里面的代码

#import "JKProgressBtn.h"

@implementation JKProgressBtn

-(instancetype)initWithFrame:(CGRect)frame{

      self = [super initWithFrame:frame];

      if (self) {

         [self setTitleColor:[UIColor brownColor] forState:UIControlStateNormal];
      }

      return self;
}

-(void)setProgress:(float)progress{

      _progress = progress;

      // 进度的Label
      [self setTitle:[NSString stringWithFormat:@"%0.2f%%",_progress*100] forState:UIControlStateNormal];
      // 刷新视图
      [self setNeedsDisplay];
}

-(void)drawRect:(CGRect)rect{


      CGSize s = rect.size;
      // 圆心
      CGPoint center = CGPointMake(s.width*0.5, s.height*0.5);
      // 半径
      CGFloat r = (s.height > s.width) ? s.width*0.5:s.height*0.5;
      r = r - 5;
      // 其实角度
      CGFloat startAngle = -M_PI_2;
      // 结束角度
      CGFloat endAngle = self.progress*2*M_PI + startAngle;

      /**
         第1个参数:圆心
         第2个参数:半径
         第3个参数:起始角度
         第4个参数:结束角度
         第5个参数:YES:顺时针 / NO:逆时针
       */
      UIBezierPath *bezierPath = [UIBezierPath bezierPathWithArcCenter:center radius:r startAngle:startAngle endAngle:endAngle clockwise:YES];
      // 圆环的宽度
      bezierPath.lineWidth = 10.0;
      // 设置圆环的样式(圆形)
      bezierPath.lineCapStyle = kCGLineCapRound;
      // 给圆环添加颜色
      [[UIColor purpleColor]setStroke];

      // 绘制路径
      [bezierPath stroke];
}

@end

提示:UIBezierPath:贝塞尔曲线的起始角度是时钟的3点,也就是数学上x的正轴方向,故上面我们把起始角度设置为-M_PI_2,也就是 时钟的12点,同理数学上y的正轴方向,其他的参数在上面描述的很清楚

  • 解释一下: bezierPath.lineWidth = 10.0;,在贝塞尔曲线里面,半径决定后,圆环的宽度是以半径向外扩展的,所以才有上面的: r = r - 5;

(2)、JKProgressBtn 的使用,在NSURLSession下载进度的方法里面刷新JKProgressBtn的进度,如下:

1304fddc42fb4cb37a03ad2670a4c101.gif

自定义下载进度图

  • 先在控制器里面定义一个 JKProgressBtn属性并初始化添加到控制器

// 进度的View
@property(nonatomic,strong) JKProgressBtn *progressView;

-(JKProgressBtn *)progressView{

     if (!_progressView) {

          _progressView = [[JKProgressBtn alloc]initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-50, [UIScreen mainScreen].bounds.size.height/2-50, 100, 100)];
          _progressView.backgroundColor = [UIColor yellowColor];
     }

     return _progressView;
}

// 添加进度View
[self.view addSubview:self.progressView];
  • 在下载进度的方法里面设置主线程刷新 progressView的值

/**
  3、下载进度的方法
  @param session 网络会话
  @param downloadTask 调用代理方式的下载任务
  @param bytesWritten 本次下载的字节数
  @param totalBytesWritten 已经下载的字节数
  @param totalBytesExpectedToWrite 期望下载的字节数 -- 文件的总大小
*/
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{

       float progress = (float)totalBytesWritten/totalBytesExpectedToWrite;
       NSLog(@"进度=%f",progress);
       dispatch_async(dispatch_get_main_queue(), ^{

               self.progressView.progress = progress;
       });
}

提示:NSURLSession创建的下载是在子线程执行的,所以上面才在主线程刷新UI

2.6、断点续传,完整代码在JKNSURLSession中的 Test6ViewController

(1)、创建三个按钮,开始下载、暂停下载,继续下载

892d97051a10e3436696669a89c312df.png

创建三个按钮,**开始下载**、**暂停下载**,**继续下载**

(2)、创建一个全局的下载任务

/**
   设置一个全局的下载任务
 */
@property(nonatomic,strong) NSURLSessionDownloadTask *downloadTask;

(3)、开始下载、暂停下载,继续下载 三个方法对应的代码如下

#pragma mark 开始下载
-(void)startLoadTask{

       NSLog(@"开始下载");
       // 1.创建url:http://localhost
       NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"];
       self.downloadTask = [self.session downloadTaskWithURL:url];
       // 2、执行下载
       [self.downloadTask resume];
}

#pragma mark 暂停下载
-(void)pauseLoadTask{

       NSLog(@"暂停下载");

       [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {

           self.resumeData = resumeData;

           //释放任务
           self.downloadTask = nil;
       }];
}

#pragma mark 继续下载
-(void)resumeLoadTask{

       NSLog(@"继续下载");

      // 防止继续下载被执行两次,故下面把self.resumeData赋为nil
      if (self.resumeData == nil) return;

      self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
      self.resumeData = nil;
      [self.downloadTask resume];
}

提示:断点续传其实也就是在暂停下载的时候获取下载的resumeData,再次接着下载的时候,用resumeData再获取一个NSURLSessionDownloadTask,从而接着下载

2.7、NSURLSession 强引用 问题

(1)、NSURLSession是一个强引用,在下载完成的时候要进行释放,不管是是否支持不在当前界面下载,当所有的下载任务都完成后,需要进行释放 session,并赋nil,否则会造成内存泄漏

/**
   1、下载完成的方法
 */
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{

    NSLog(@"下载完成");
    [self.session finishTasksAndInvalidate];
    self.session = nil;
}

(2)、如果只支持当前界面下载的情况,退出当前界面,取消下载,session 赋nil

[self.session invalidateAndCancel];
self.session = nil;

2.8、完整代码请看 JKNSURLSession 中的 Test6ViewController

三、下载管理器  demo地址:JKDownloaderKit

3.1、下载思路

(1)、创建另两个文件夹:JKDownloadCompleted(下载完成的文件夹)和JKDownLoading(下载中的文件夹),在下载中的资源会存在JKDownLoading中,下载完成后会移动到JKDownloadCompleted里面

7413b16b8e697cccbbc43bfedf68b74d.png

创建另两个文件夹

(2)、先查看服务器上的文件大小

(3)、查看本地是否存在文件,如果存在如下

  • 如果文件小于服务器文件的大小,从本地文件长度开始下载(断点续传)

  • 如果文件等于服务器文件的大小,再把文件生成一个MD5与服务器对文件返回的MD5做对比,如果一样,代表下载完成

  • 如果文件大于服务器文件的大小,发生错误,直接删除文件,重新下载

(4)、如果本地不存在该文件,直接下载

  • 上传视频的思路:

在上传视频的时候,如果视频断开了(程序退出了),那么就要去服务器请求看看自己之前上传了多少,接着上传就好,和视频的下载原理是一样的,对比

  • 总体思路图

651aeff3f85ad64cf8d2b9e1f80390bb.png

下载与上传的思路图

(5)、在监听下载的方法中,当下载完成后做如下的操作

在没有 error 的情况下,文件下载是完毕了,但是不一定成功,分析如下

  • 判断, 本地缓存 == 文件总大小 (如果不相等,说明下载有问题,删除下载路径下的文件,重新下载;如果相等在验证文件内容的MD5值是否一样,一样的话才是真正的下载完成,否则下载是有问题的,删除下载路径下的文件,重新下载)

下载-文件完整性验证机制:验证文件的合法性, 下载数据是否完整可用

  • 服务器返回文件下载信息的同时, 会返回该文件内容的md5值

  • 本地下载完成后, 可以, 在本地已下载的文件的MD5值和服务器返回的进行对比;

  • 为了简单, 有的, 服务器返回的下载文件MD5值, 直接就是命名称为对应的文件名称

3.2、创建一个管理下载的类

命名下载的类为:JKDownLoader,继承于 NSObject,定义一个下载的方法

/**
  定义一个下载的方法
  (1)、先查看服务器上的文件大小
  (2)、查看本地是否存在文件,如果存在如下
         2.1、如果文件小于服务器文件的大小,从本地文件长度开始下载(断点续传)
         2.2、如果文件等于服务器文件的大小,再把文件生成一个MD5与服务器对文件返回的MD5做对比,如果一样,代表下载完成
         2.3、如果文件大于服务器文件的大小,发生错误,直接删除文件,重新下载
  (3)、如果本地不存在该文件,直接下载
  @param url 下载的url
*/
-(void)downloadWithUrl:(NSURL *)url{


}

3.3、从 服务器 获取下载文件的信息

  • 我们需要设置下载的总大小以及下载后存放的位置

/**
   文件的总大小
 */
@property(nonatomic,assign) long long expectdContentLength;

/**
  文件的下载路径
 */
@property(nonatomic,strong) NSString *downloadPath;
  • 获取文件信息的私有ipa

#pragma mark 私有方法
-(void)selectServerFileInfoWithUrl:(NSURL *)url{

    // 1.请求信息:我们只需要获取头部信息就好
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeOut];
    request.HTTPMethod = @"HEAD";

    // 2.建立网络连接
    NSURLResponse *response = nil;
    [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];

    NSLog(@"%@ %@ %lld",data,response,response.expectedContentLength);
    // 3.记录服务器的文件信息
    // 3.1、文件长度
    self.expectdContentLength = response.expectedContentLength;

    // 3.2、建议保存的文件名字,将在文件保存在tmp,系统会自动回收
    self.downloadPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
}
  • 提示:这里采用的是同步方法,因为我们需要根据文件的信息去操作下面的下载操作,不能使用异步

  • NSURLConnection:默认是在 主线程 进行操作,而NSURLSession 是在 子线程 进行操作

3.4、检查 本地 下载文件的信息

/**
   2.从本地检查要下载的文件信息(除了文件下载完,其他的情况都需要下载文件)
   @return YES:需要下载,NO:不需要下载
 */
-(BOOL)checkLocalFileInfo{

      long long fileSize = 0;
      // 1.判断文件是否存在
      if ([[NSFileManager defaultManager]fileExistsAtPath:self.downloadPath]) {

           // 获取本地存在文件大小
           NSDictionary *attributes = [[NSFileManager defaultManager]attributesOfItemAtPath:self.downloadPath error:NULL];
           NSLog(@"%@",attributes);
           fileSize = [attributes[NSFileSize] longLongValue];
       }

       // 2.根据文件大小来判断文件是否存在
       if(fileSize > self.expectdContentLength){

            // 文件异常,删除该文件
            [[NSFileManager defaultManager]removeItemAtPath:self.downloadPath error:NULL];
            fileSize = 0;
       }else if (fileSize == self.expectdContentLength) {
            // 文件已经下载完
            return NO;
       }

       return YES;
}

3.5、文件下载实现

(1)、定义一个属性保存下载的地址 URL

/**
   视频的下载地址 URL
 */
@property(nonatomic,strong) NSURL *downloadUrl;

(2)、视频下载从当前的字节开始下载,不管字节是不是0,都是检查过本地路径的字节,本地有的话,当前字节就不是0,也就是断点续传;没有的话就是0,也就是从头开始下载

  • 拓展一个 HTTP 属性 Range,下载会用到

Bytes = 0-499  : 从 0 到 499 的 500 个字节
Bytes = 500-999 : 从500-999的第二个500字节
Bytes = 500-  : 从500开始到以后所有的字节
Bytes = -500 最后500个字节
Bytes = 500-699,800-1299,1600-2000 同时指定多个范围

(3)、开始下载,这里先使用上面 一 中的 NSURLConncetion放在子线程,开启Runloop的代理方法来下载,把 NSURLConncetion 放在异步并发的队列,用文件流拼接写入路径,下面只展示部分代码,完整代码看 demo

/* 保存文件的输出流
   - (void)open; 写入之前,打开流
   - (void)close; 写入完毕之后,关闭流
 */
@property(nonatomic,strong)NSOutputStream *fileStream;

/*
   保存下载线程的运行循环,也就是下载任务的 runloop
 */
@property(nonatomic,assign)CFRunLoopRef downloadRunloop;

#pragma mark 下载文件
-(void)downloadFile{

     dispatch_async(dispatch_get_global_queue(0, 0), ^{

          // 1.建立请求
          NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:self.downloadUrl cachePolicy:1 timeoutInterval:MAXFLOAT];
          // 2.设置下载的字节范围,self.currentLength到之后所有的字节
          NSString *downloadRangeStr = [NSString stringWithFormat:@"bytes=%lld-",self.currentContentLength];
          // 3.设置请求头字段
          [request setValue:downloadRangeStr forHTTPHeaderField:@"Range"];
          // 4.开始网络连接
          NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];

          // 5.设置代理工作的操作 [[NSOperationQueue alloc]init] 默认创建一个异步并发队列
          [connection setDelegateQueue:[[NSOperationQueue alloc]init]];
          // 启动连接
          [connection start];

          //5.启动运行循环
          /*
            CoreFoundation 框架 CFRunLoop
            CFRunloopStop() 停止指定的runloop
            CFRunloopGetCurrent() 获取当前的Runloop
            CFRunloopRun() 直接启动当前的运行循环
          */

          // (1)、拿到当前的运行循环
          self.downloadRunloop = CFRunLoopGetCurrent();
          // (2)、启动当前的运行循环
         CFRunLoopRun();

    });

}

#pragma mark NSURLConnection的代理NSURLConnectionDataDelegate的方法
// 1、接收服务器的响应 --- 状态和响应头做一些准备工作
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{

     self.fileStream = [[NSOutputStream alloc]initToFileAtPath:self.downloadPath append:YES];
     [self.fileStream open];   
}
// 2、接收服务器的数据,由于数据是分块发送的,这个代理方法会被执行多次,因此我们也会拿到多个data
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
     // 接收数据,用输出流拼接,计算下载进度

    // 将数据拼接起来,并判断是否可写如,一般情况下可写入,除非磁盘空间不足
    if ([self.fileStream hasSpaceAvailable]) {

         [self.fileStream write:data.bytes maxLength:data.length];
    }

    // 当前长度拼接
    self.currentContentLength += data.length;

    // 计算百分比
    // progress = (float)long long / long long
    float progress = (float)self.currentContentLength/self.expectdContentLength;

    // 传送百分比
    if (self.progress) {
          self.progress(progress);
    }

    NSLog(@"%f %@",progress,[NSThread currentThread]);

}
// 3、接收到所有的数据下载完毕后会有一个通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
{
      NSLog(@"下载完毕");

      [self.fileStream close];

      // 下载完成的回调
      if (self.completion) {
          self.completion(self.downloadPath);
      }

     // 关闭当前下载完的 RunLoop
     CFRunLoopStop(self.downloadRunloop);
}

// 4、下载错误的处理
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{

     NSLog(@"连接失败:%@",error.localizedDescription);
     // 关闭流
     [self.fileStream close];

     // 关闭当前的 RunLoop
     CFRunLoopStop(self.downloadRunloop);
}

3.6、暂停下载操作

暂停下载操作直接调用:NSURLConnection  的 cancel 就好

3.7、多文件下载管理

我们创建一个下载管理器JKDownloaderManger,设置成单利,用来下载多个文件,同时创建下载缓存池,避免多次下载同一个文件

(1)、单利的实现(一个静态变量,三个方法,才是完整的单利)

static id instance;

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

      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
            instance = [super allocWithZone:zone];
      });

      return instance;
}

-(id)copy{

      return instance;
}

+(instancetype)shareDownloaderManger{

      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{

            instance = [[self alloc]init];
      });

      return instance;
}

(2)、创建字典缓存池

@property(nonatomic,strong) NSMutableDictionary *downloadCache;

(3)、下载方法的实现( 以NSURL的url.path 为键 )

-(void)jk_downloadWithUrl:(NSURL *)url withDownProgress:(downProgress)progress completion:(downCompletion)completion fail:(downFailed)failed
{
   // 1.判断缓存池里面是否有同一个下载任务
   JKDownLoader *downLoader = self.downloadCache[url.path];

   if (downLoader != nil) {
        NSLog(@"已经在下载列表中,请不要重复下载");
        return;
   }

   // 2.创建新的下载任务
   downLoader = [[JKDownLoader alloc]init];

   // 3.将下载任务添加到缓存池
   [self.downloadCache setValue:downLoader forKey:url.path];

    __weak typeof(self) weakSelf = self;
    [downLoader jk_downloadWithUrl:url withDownProgress:progress completion:^(NSString * _Nonnull downFilePath) {
           // 1.将下载从缓存池移除
           [weakSelf.downloadCache removeObjectForKey:url.path];
           // 2.执行调用方法的回调
           if (completion) {
                 completion(downFilePath);
           }
    } fail:failed];

}

(4)、下载暂停:暂停下载,从缓存池移除该url的path

#pragma mark 暂停某个文件下载
-(void)pauseloadWithUrl:(NSURL *)url{

    // 1.通过url获取下载任务
    JKDownLoader *downLoader = self.downloadCache[url.path];

    // 2.暂停下载
    if (downLoader == nil){

          if (self.failed) {
               self.failed(@"已经暂停下载,请不要重复点击");
          }
          return;
   }
   [downLoader pause];

   // 3.从缓存池移除
   [self.downloadCache removeObjectForKey:url.path];
}

iOS 下载器完整的代码请查看demo

到此下载完毕,下一篇会阐述 下载中 和 下载完成 文件夹里面文件的读取,敬请关注

作者:IIronMan

链接:https://www.jianshu.com/p/f637a262ee4a

本公众号转载内容已尽可能注明出处,如未能核实来源或转发内容图片有权利瑕疵的,请及时联系本公众号进行修改或删除【联系方式QQ : 3442093904  邮箱:support@cocoachina.com】。文章内容为作者独立观点,不代表本公众号立场。版权归原作者所有,如申请授权请联系作者,因文章侵权本公众号不承担任何法律及连带责任。

---END---

deb4afd9bdd112a5d8eb304a94d40800.png

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐