NSURLConnection的历史
iOS2.0推出的,至今有10多年的历史
苹果几乎没有对 NSURLConnection 做太大的改动
sendAsynchronousRequest 方法是 iOS5.0之后,苹果推出的
在 iOS5.0之前,苹果的网络开发是处于黑暗时代
需要使用 代理 方法,还需要使用运行循环,才能够处理复杂的网络请求
1、sendAsynchronousRequest异步下载
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// url 字符串
NSString *urlStr = @"http://localhost/图片浏览器.mp4";
// 添加百分号转义
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// 请求路径
NSURL *url = [NSURL URLWithString:urlStr];
// 请求对象
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSLog(@"开始下载");
// 发送异步请求
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// 将数据写入到文件中
[data writeToFile:@"/Users/pkxing/desktop/123.mp4" atomically:YES];
NSLog(@"下载完毕");
}];
}
上面下载大文件代码存在的问题:
* 没有进度跟进
* 出现内存峰值
2、代理方法下载—>错误的代理
# 能解决没有进度跟进的问题,但是是错误的代理
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// url 字符串
NSString *urlStr = @"http://localhost/图片浏览器.mp4";
// 添加百分号转义
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// 请求路径
NSURL *url = [NSURL URLWithString:urlStr];
// 请求对象
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSLog(@"开始下载");
// 发送请求
[NSURLConnection connectionWithRequest:request delegate:self];
}
#pragma mark - NSURLConnectionDownloadDelegate 代理方法
/**
* 下载完成后回调方法
*/
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL{
NSLog(@"destinationURL = %@",destinationURL);
}
/**
* 每当接收到服务器返回数据后的回调方法
*
* @param bytesWritten 本次下载的字节数
* @param totalBytesWritten 已经下载的字节数
* @param expectedTotalBytes 期望下载的字节数(总大小)
*/
- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long)expectedTotalBytes {
// NSLog(@"%lld--%lld---%lld",bytesWritten,totalBytesWritten,expectedTotalBytes);
// 小 long long / 大 long long = 0
CGFloat progress = (CGFloat)totalBytesWritten / expectedTotalBytes;
NSLog(@"progress = %f",progress);
}
> 使用NSURLConnectionDownloadDelegate代理方法问题:
* NSURLConnectionDownloadDelegate只适用于使用 NewsstandKit创建的NSURLConnection对象。对于Foundation框架创建的NSURLConnection对象,使用该代理下载完成后无法找到下载的文件。
* NewsstandKit.framework来支持newsstand类型的程序,就是在sprint board上看到在书架中的程序,NSURLConnectionDownloadDelegate用于刊物/电子杂志的下载。
3、代理方法下载—>正确的代理方法
// 文件总大小
@property(nonatomic,assign) long long expectedContentLenght;
// 当前接收文件大小
@property(nonatomic,assign) long long currentFileSize;
// 文件数据
@property(nonatomic,strong) NSMutableData *fileData;
- (NSMutableData *)fileData {
if (_fileData == nil) {
_fileData = [NSMutableData data];
}
return _fileData;
}
#pragma mark - NSURLConnectionDataDelegate 代理方法
/**
* 接收到服务器响应的时候调用(状态行和响应头)
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// 获得要下载文件总大小
self.expectedContentLenght = response.expectedContentLength;
// 设置当前接收文件大小为0
self.currentFileSize = 0;
}
/**
* 接收到服务器返回的数据时调用,可能会被调用多次
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
self.currentFileSize += data.length;
// 计算进度值
CGFloat progress = (CGFloat)self.currentFileSize / self.expectedContentLenght;
// 拼接数据
[self.fileData appendData:data];
NSLog(@"progress = %f",progress);
}
/**
* 网络请求结束调用(断开网络)
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"下载完成");
// 将数据写入到文件中
[self.fileData writeToFile:@"/Users/pkxing/desktop/1234.mp4" atomically:YES];
// 释放数据
self.fileData = nil;
}
/**
* 网络连接发生错误的时候调用(任何网络请求都有可能出现错误)
* 在实际开发中一定要进行出错处理
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"error = %@",error);
}
> 上面代码通过NSURLConnectionDataDelegate代理下载数据,解决了进度问题,但内存问题没有解决。
4、利用NSFileHandle拼接文件
/**
* 将数据写入文件
*/
- (void)writeData:(NSData *)data{
// NSFileHandle:Handle(句柄/文件指针)是针对前面一个单词(File)进行操作的对象
// 利用NSFileHandle可以对文件进行读写操作。
// NSFileManager:对文件的复制,删除,检查是否存在,检查文件大小...类似于Finder
// 创建文件句柄对象
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:@"/Users/pkxing/desktop/aaa.mp4"];
// 如果文件不存在,创建出来的句柄对象为 nil
if (fileHandle == nil) {
[data writeToFile:@"/Users/pkxing/desktop/aaa.mp4" atomically:YES];
} else {
// 将文件指针移动到后面
[fileHandle seekToEndOfFile];
// 写入数据
[fileHandle writeData:data];
// 关闭文件(在对文件进行操作时,一定要记得打开和关闭成对出现)
[fileHandle closeFile];
}
}
#pragma mark - NSURLConnectionDataDelegate 代理方法
/**
* 接收到服务器响应的时候调用(状态行和响应头)
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// 获得要下载文件总大小
self.expectedContentLenght = response.expectedContentLength;
// 设置当前接收文件大小为0
self.currentFileSize = 0;
}
/**
* 接收到服务器返回的数据时调用,可能会被调用多次
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
self.currentFileSize += data.length;
// 计算进度值
CGFloat progress = (CGFloat)self.currentFileSize / self.expectedContentLenght;
// 将数据写入文件
[self writeData:data];
}
5、利用NSOutputStream拼接文件
#pragma mark - NSURLConnectionDataDelegate 代理方法
/**
* 接收到服务器响应的时候调用(状态行和响应头)
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// 获得要下载文件总大小
self.expectedContentLenght = response.expectedContentLength;
// 设置当前接收文件大小为0
self.currentFileSize = 0;
// 根据文件名 创建输出流对象
self.fileStream = [NSOutputStream outputStreamToFileAtPath:@"/Users/pkxing/desktop/bbb.mp4" append:YES];
// 打开流
[self.fileStream open];
}
/**
* 接收到服务器返回的数据时调用,可能会被调用多次(所有的 data 的数据都是按顺序传递过来的)
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
self.currentFileSize += data.length;
// 计算进度值
CGFloat progress = (CGFloat)self.currentFileSize / self.expectedContentLenght;
// 拼接数据
[self.fileStream write:data.bytes maxLength:data.length];
}
/**
* 网络请求结束调用(断开网络)
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// 关闭流
[self.fileStream close];
}
/**
* 网络连接发生错误的时候调用(任何网络请求都有可能出现错误)
* 在实际开发中一定要进行出错处理
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
// 关闭流
[self.fileStream close];
}
6、大文件下载多线程—NSURLConnection
#warning 子线程中要手动开启runLoop,runLoop 是死循环
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// url 字符串
NSString *urlStr = @"http://localhost/图片浏览器.mp4";
// 添加百分号转义
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// 请求路径
NSURL *url = [NSURL URLWithString:urlStr];
// 请求对象
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSLog(@"开始下载=%@",[NSThread currentThread]);
// 发送请求
[NSURLConnection connectionWithRequest:request delegate:self];
// 启动 runLoop
[[NSRunLoop currentRunLoop] run];
NSLog(@"下载结束");
});
7、大文件下载—暂停下载
> 调用NSURLConnection的cancel方法即可暂停。
> 一旦调用了cancel方法暂停,下次重新下载需要重新NSURLConnection对象。
> 下载默认都是从零开始下载,如果上次下载的文件还存在,则下载的数据会拼接到文件后面。
#简单粗暴的方法---删除上一次没有下载完成的文件
// removeItemAtPath:文件存在,则删除,不存在则什么也不做。可以不用判断文件是否存在
[[NSFileManager defaultManager] removeItemAtPath:@"/Users/pkxing/desktop/bbb.mp4" error:NULL];
8、大文件下载—断点续传
1> 思路
1、检查服务器文件大小(HEAD请求)
2、检查本地是否存在文件
3、如果本地存在文件
> 如果小于服务器的文件,从当前文件大小开始下载
> 如果等于服务器的文件,下载完成
> 如果大于服务器的文件,直接删除,重新下载
2> 代码实现
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// url 字符串
NSString *urlStr = @"http://localhost/图片浏览器.mp4";
// 添加百分号转义
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// 请求路径
NSURL *url = [NSURL URLWithString:urlStr];
// 检查服务器文件信息
[self checkServerFileInfo:url];
// 检查本地文件信息
self.currentFileSize =[self checkLocalFileInfo];
// 文件大小相等
if (self.currentFileSize == self.expectedContentLenght) {
NSLog(@"下载完成");
return;
}
// 断点续传---一定不能使用缓存数据
// 请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
// 创建 range 头
NSString *range = [NSString stringWithFormat:@"bytes=%lld-",self.currentFileSize];
[request setValue:range forHTTPHeaderField:@"Range"];
// 建立连接,立即启动
self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
// 启动 runLoop
[[NSRunLoop currentRunLoop] run];
});
}
// 检查服务器的文件信息
- (void)checkServerFileInfo:(NSURL *)url{
// 创建请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 设置请求方法
request.HTTPMethod = @"HEAD";
NSURLResponse *response = nil;
// 发送同步请求(这里必须要用同步)
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
// 得到服务器响应
// 1> 目标文件大小
self.expectedContentLenght = response.expectedContentLength;
// 2> 保存文件路径
self.destinationPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
}
// 检查本地文件的信息
- (long long)checkLocalFileInfo{
// 获得文件管理对象
NSFileManager *fileManager = [NSFileManager defaultManager];
// 记录本地文件的大小
long long fileSize = 0;
// 判断文件是否存在
if([fileManager fileExistsAtPath:self.destinationPath]) {
// 文件存在,则获得文件信息
NSDictionary *attr = [fileManager attributesOfItemAtPath:self.destinationPath error:NULL];
// 直接从字典中获得文件大小
fileSize = attr.fileSize;
}
// 如果大于服务器文件大小,直接删除
if(fileSize > self.expectedContentLenght) {
[fileManager removeItemAtPath:self.destinationPath error:NULL];
fileSize = 0;
}
return fileSize;
}
9, 注意事项:
1,使用代理模式,GCD多线程下载,一定要开启 runloop,因为下载也是一个事件,需要不断的调用代理方法,所以开启子线程运行循环
2,NSTimer 也是需要开启 runloop
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// url 字符串
NSString *urlStr = @"http://soft1.xitongzhijia.net:808/201205/MicrosoftOffice2007_SP1_XiTongZhiJia.rar";
// 添加百分号转义
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// 请求路径
NSURL *url = [NSURL URLWithString:urlStr];
// 请求对象
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSLog(@"开始下载=%@",[NSThread currentThread]);
// 发送请求
[NSURLConnection connectionWithRequest:request delegate:self];
// 启动 runLoop
[[NSRunLoop currentRunLoop] run];
});