登录 立即注册
金钱:

Code4App-iOS开发-iOS 开源代码库-iOS代码实例搜索-iOS特效示例-iOS代码例子下载-Code4App.com

iOS 下载管理器 [复制链接]

2019-2-15 11:20
hellokenken 阅读:256 评论:0 赞:1
Tag:  

总体内容

在学习下面的知识前请在 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进行编码。

1
2
3
4
5
6
NSString *urlStr = @" 
 
// 在 iOS9 之后废弃了
// urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// iOS9 之后取代上面的 api
urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];

<2>、string 转 NSURL

1
NSURL *url = [NSURL URLWithString:urlStr];

<3>、创建 NSURLRequest 对象

1
NSURLRequest *request = [NSURLRequest requestWithURL:url];

<4>、NSURLConnection 下载 视频

1
2
3
4
5
6
7
8
9
10
11
12
// 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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)、内存偏高,会有最大峰值(一次性把数据写入),内存隐患

image.png

image.png


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

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

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

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

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

image.png

代理方法选择 `NSURLConnectionDataDelegate`

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

1
2
3
4
5
/** 要下载的文件总大小 */
@property(nonatomic,assign) long long exceptedContentLength;
 
/** 当前已经下载的文件总大小 */
@property(nonatomic,assign) long long currentDownContentLength;

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

(4)、常用的代理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
  保存下载视频的路径
*/
@property(nonatomic,strong) NSString *downFilePath;
/**
  保存视频数据
*/
@property(nonatomic,strong) NSMutableData *fileData;
-(NSMutableData *)fileData{
     if (!_fileData) {
          _fileData = [[NSMutableData alloc]init];
     }
    return _fileData;
}

(2)、代理中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 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中的异步效果一样,应该是苹果底层的实现方式

image.png

内存暴增

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

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

  • NSFileManager:主要的功能是创建目录、检查目录是否存在、遍历目录、删除文件

    拷贝文件、剪切文件等等,主要是针对文件的操作

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 把数据写入到磁盘的方法
-(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 属性

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

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

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

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

1
2
3
4
5
// 判断是否有空间可写
if ([self.fileStream hasSpaceAvailable]) {
 
     [self.fileStream write:data.bytes maxLength:data.length];
}

(4)、关闭文件流

1
2
3
4
5
6
接收到所有的数据加载完毕后会有一个通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
 
      NSLog(@"下载完毕");
      [self.fileStream close];
}

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

1
2
3
4
5
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)、将网络操作放在异步线程,异步的运行循环默认不启动,没有办法监听接下来的网络事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dispatch_async(dispatch_get_global_queue(00), ^{
 
    // 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)、定义一个保存下载线程的运行循环

1
@property(nonatomic,assign)CFRunLoopRef downloadRunloop;

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

1
2
3
4
5
6
7
8
9
10
11
12
/*
   CoreFoundation 框架 CFRunLoop
   CFRunloopStop() 停止指定的runloop
   CFRunloopGetCurrent() 获取当前的Runloop
   CFRunloopRun() 直接启动当前的运行循环
 */
  
//1、拿到当前的运行循环
self.downloadRunloop = CFRunLoopGetCurrent();
 
//2.启动当前的运行循环
CFRunLoopRun();

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

1
2
3
4
5
6
7
8
9
10
11
// 所有数据加载完毕--所有数据加载完毕,会一个通知!
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"完毕!%@",[NSThread currentThread]);
     
    //关闭文件流
    [self.fileStream close];
     
    //停止下载线程所在的runloop
    CFRunLoopStop(self.downloadRunloop);
}

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

image.png

Apache 服务器里面的数据

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

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

简单的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-(void)touchesBegan:(NSSet*)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

1
2
3
4
5
6
7
8
9