iOS 下载管理器(上)

简介: iOS 下载管理器

总体内容


一、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(@"下载完成");
}];

提示:NSURLConnectioniOS 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)、内存偏高,会有最大峰值(一次性把数据写入),内存隐患

image.png


image.png


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

  • (1)、在 1.1 里面我们使用的是 NSURLConnectionblock方法进行的下载,会有下载没有进度和出现峰值的问题,那么下面我们就使用 NSURLConnection 的代理方法来解决这些问题
  • 下载没有进度的解决办法:通过代理来解决
  • 进度跟进:在响应头中获取文件的总大小,在每次接收数据的时候计算数据的比例


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


image.png


  • (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中的异步效果一样,应该是苹果底层的实现方式


image.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)、如何判断文件是否下载完成 ?答:判断进度?判断完成通知?,判断时间?判断大小?这些都不太好,比较好的方式是使用MD5MD5:
  • <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);
}


目录
相关文章
|
Swift iOS开发 开发者
iOS - 跳转App Store下载 app 的两种方式
iOS - 跳转App Store下载 app 的两种方式
2223 0
iOS - 跳转App Store下载 app 的两种方式
|
存储 缓存 iOS开发
iOS 轻量化动态图像下载缓存框架实现
日常开发过程中,图片的下载会占用大量的带宽,图片的加载会消耗大量的性能和内存,正确的使用图片显得尤为重要。 同样也经常需要在各类型控件上读取网络图片和处理本地图片,例如:UIImageView、UIBtton、NSImageView、NSButton等等。
iOS 轻量化动态图像下载缓存框架实现
|
iOS开发
iOS下载文件保存到手机文件指定目录
iOS下载文件保存到手机文件指定目录
1094 0
|
Web App开发 弹性计算 Android开发
阿里云无影云桌面客户端下载Win/Mac/iOS/安卓/Web端均支持
阿里云无影客户端下载系统Win/Mac/iOS/安卓/Web端均支持
4636 0
阿里云无影云桌面客户端下载Win/Mac/iOS/安卓/Web端均支持
|
Linux iOS开发 开发者
WIN11自定义版本ios镜像下载教程
WIN11自定义版本ios镜像下载教程
WIN11自定义版本ios镜像下载教程
|
安全 数据安全/隐私保护 iOS开发
iMazing官网下载安装教程 2023最新版兼容Win和Mac的iOS设备管理软件
iMazing是一款功能强大的iOS设备管理软件,它可以帮助用户备份和管理他们的iPhone、iPad或iPod Touch上的数据。除此之外,它还可以将备份数据转移到新的设备中、管理应用程序、导入和导出媒体文件等。本文将详细介绍iMazing的功能和安全性,并教大家如何使用iMazing来恢复备份数据。
509 0
|
Web App开发 弹性计算 Android开发
阿里云无影客户端下载Windows/Mac/iOS/安卓/Web端操作系统均支持
阿里云无影云桌面客户端下载,无影客户端支持操作系统包括Windows、Mac、Web网页端、iOS客户端和安卓客户端
阿里云无影客户端下载Windows/Mac/iOS/安卓/Web端操作系统均支持
|
iOS开发 芯片 MacOS
macOS Big Sur 正式发布并已开放下载,支持原生运行 iOS 和 iPadOS App
macOS Big Sur 正式发布并已开放下载,支持原生运行 iOS 和 iPadOS App
218 0
|
移动开发 安全 JavaScript
关于ios低版本在app store下载软件时由于版本低导致不能下载的解决办法
关于ios低版本在app store下载软件时由于版本低导致不能下载的解决办法
590 0
关于ios低版本在app store下载软件时由于版本低导致不能下载的解决办法
|
移动开发 Java Linux
iOS上架app store下载步骤
iOS上架app store下载步骤
iOS上架app store下载步骤