一、课程目标
1、NSURLConnection下载是一个网络多线程的综合性演练项目
2、充分体会 NSURLConnection 开发中的细节
3、虽然 NSURLConnection 在 ios9.0中已经被废弃,但是作为资深的iOS程序员。必须要了解 NSURLConnection 的细节。
4、利用 HTTP 请求头的 Range 实现断点续传
5、利用 NSOutputStream 实现文件流拼接
6、自定义 NSOperation 及操作缓存管理
7、利用IB_DESIGNABLE和IBInspectable实现在stroyboard中自定义视图实时渲染
二、NSURLConnection的历史
1、iOS2.0推出的,至今有10多年的历史。
2、苹果几乎没有对 NSURLConnection 做太大的改动。h
3、sendAsynchronousRequest 方法是 iOS5.0之后,苹果推出的。
4、在 iOS5.0之前,苹果的网络开发是处于黑暗时代。
5、需要使用 代理 方法,还需要使用运行循环,才能够处理复杂的网络请求。
三、下载大文件
1、异步下载sendAsynchronousRequest
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// url 字符串
NSString *urlStr = @"http://localhost/图片浏览器.mp4";
// 添加百分号转义,把字符串按照utf8格式转换
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 {
CGFloat progress = (CGFloat)totalBytesWritten / expectedTotalBytes;
NSLog(@"progress = %f",progress);
}
1、上面实现能解决没有进度跟进的问题,但是是错误的代理。
2、NSURLConnectionDownloadDelegate 只适用于使用NewsstandKit框架创建的 NSURLConnection 对象。
3、对于Foundation框架创建的NSURLConnection对象,使用该代理下载完成后无法找到下载的文件4、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 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、多线程下载
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(@"下载结束");
});
注意点:子线程中要手动开启runLoop,runLoop是死循环。下载完毕后,系统会自动关闭子线程开启的runLoop。
7、暂停下载
- 1、如何暂停下载?
- 调用 NSURLConnection 的cancel方法即可暂停。
- 2、暂停下载后注意点?
- 一旦调用了cancel方法暂停,下次下载需要重新创建NSURLConnection对象。
- 默认每一次下载都是从零开始,如果上次下载的文件还存在,则下载的数据会拼接到文件后面。
- 简单粗暴的方法---删除上一次没有下载完成的文件
// removeItemAtPath:文件存在,则删除,不存在则什么也不做。可以不用判断文件是否存在
[[NSFileManager defaultManager] removeItemAtPath:@"/Users/pkxing/desktop/bbb.mp4" error:NULL];
8、断点续传
8.1、思路
- 检查服务器文件大小(HEAD请求)
- 检查本地是否存在文件
- 如果本地存在文件
- 如果小于服务器的文件,从当前文件大小开始下载
- 如果等于服务器的文件,下载完成
- 如果大于服务器的文件,直接删除,重新下载
8.2、HTTP HEAD方法
HEAD 方法通常是用来在下载文件之前,获取远程服务器上的文件信息
- 与 GET 方法相比,同样能够拿到响应头,但是不返回数据实体
- 用户可以根据响应头信息,确定下一步操作
同步方法
- 同步方法是阻塞式的,通常只有 HEAD 方法才会使用同步方法
- 如果在开发中,看到参数的类型是 **,就传入对象的地址
8.2、获得服务器的文件信息
- (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];
}
8.3、获得本地文件的信息
- (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;
}
8.4、代码实现
- (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];
});
}
8.5、设置Range头
- Range 用于设置获取服务器数据的范围
- 示例
值 说明
bytes=500- 从500字节以后的所有字节
bytes=0-499 从0到499的头500个字节
bytes=500-999 从500到999的第二个500字节
bytes=-500 最后500个字节
bytes=500-599,800-899 同时指定几个范围
- Range小结
- 用于分隔
- 前面的数字表示起始字节数
- 后面的数组表示截止字节数,没有表示到末尾
- 用于分组,可以一次指定多个Range,不过很少用
- 用于分隔
9、下载进度视图
- 1、使用按钮绘制进度圆,并显示下载进度
- (void)setProgress:(CGFloat)progress {
_progress = progress;
[self setTitle:[NSString stringWithFormat:@"%.02f%%",progress * 100] forState:UIControlStateNormal];
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
CGFloat width = rect.size.width;
CGFloat height = rect.size.height;
CGPoint center = CGPointMake(width * 0.5, height * 0.5);
CGFloat radius = (MIN(width, height) - self.lineWidth) * 0.5;
CGFloat startAngle = -M_PI_2;
CGFloat endAngle = 2 * M_PI * self.progress + startAngle;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
path.lineWidth = self.lineWidth;
[self.lineColor set];
[path stroke];
}
- 2、下载进度IB技巧
IB_DESIGNABLE
@interface HMProgressButton : UIButton
// 进度值
@property(nonatomic,assign) IBInspectable CGFloat progress;
// 线宽
@property(nonatomic,assign) IBInspectable CGFloat lineWidth;
// 线的颜色
@property(nonatomic,copy) IBInspectable UIColor *lineColor;
@end
// IB_DESIGNABLE:表示这个类可以在 IB 中设计
// IBInspectable:表示这个属性可以在 IB 中设计
// IB:interface builder 即界面构建者
10、下载方法抽取
10.1、抽取思路
- 1、新建类
- 2、抽取主方法-在.h 中定义,明确方法是否适合被调用
- 3、复制主方法
- 4、复制相关的"子"方法
- 5、复制相关的属性
- 6、检查代码的有效性
- 7、复制代理方法
- 8、调整控制器代码,测试重构方法是否正确执行
- 9、调整控制器代码, 删除控制器被拷走的代码
- 10、再次测试,确保调整没有失误。
- 11、确认重构的接口
- — 需要回调:进度回调,完成&错误回调
- 12、定义类方法,传递回调参数
- 13、实现类方法,记录主 block
- 14、调整调用方法
- 15、增加 block 的实现
- 16、测试
- 17、如果之前已经下载完成,直接回调,进度回调和完成回调
- 18、暂停操作
- 19、测试,测试,测试
- 20、发现问题:如果连续点击,会重复下载,造成错乱!
- 21、解决方法:建立一个下载管理器的单列,负责所有文件的下载,以及下载操作的缓存。
- 22、建立单例
- 23、接管下载操作
- 1> 定义接口方法
- 2> 实现接口方法
- 3> 替换方法
- 4> 测试
- 24、下载操作缓存
- 25、暂停实现
- 26、最大并发数、NSOperationQueue + NSOperation
10.2、下载管理器
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface HMDownloadManager : NSObject
// 全局入口
+(instancetype)sharedManager;
/**
* 下载 URL 对应的文件
* @param url 文件路径
* @param progress 进度回调
* @param finished 完成回调
*/
-(void)downloadWithUrl:(NSURL *)url progress:(void (^)(CGFloat progress))progress finished:(void (^)(NSString *filePath,NSError *error))finished;
@end
#import "HMDownLoadOperation.h"
@implementation HMDownloadManager
+(instancetype)sharedManager{
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (void)downloadWithUrl:(NSURL *)url progress:(void (^)(CGFloat))progress finished:(void (^)(NSString *, NSError *))finished {
// 实例化下载操作对象
HMDownLoadOperation *downloader = [HMDownLoadOperation downloadOperation:progress finished:finished];
// 开始下载
[downloader download:url];
}
@end
10.3、实现下载操作缓存
#import "HMDownloadManager.h"
#import "HMDownLoadOperation.h"
@interface HMDownloadManager()
// 操作缓存池
@property(nonatomic,strong) NSMutableDictionary *operationCache;
@end
@implementation HMDownloadManager
+(instancetype)sharedManager{
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (void)downloadWithUrl:(NSURL *)url progress:(void (^)(CGFloat))progress finished:(void (^)(NSString *, NSError *))finished {
// 判断缓冲池中是否存在,有就直接返回
if (self.operationCache[url]) {
NSLog(@"正在玩命下载中....稍安勿躁");
return;
}
// 实例化下载操作对象
HMDownLoadOperation *downloader = [HMDownLoadOperation downloadOperation:progress finished:^(NSString *filePath, NSError *error) {
// 从缓存池中删除下载操作
// self:是单例,没有释放的需要,保存在静态区,跟随程序的销毁而销毁
//,这里可以不用 weak也不会有循环引用问题
[self.operationCache removeObjectForKey:url];
// 回调调用方的 block
finished(filePath,error);
}];
// 将操作对象添加到缓存池中
[self.operationCache setObject:downloader forKey:url];
// 开始下载
[downloader download:url];
}
- (void)pause:(NSURL *)url {
// 根据 url 从缓存池中获得对应的下载操作
HMDownLoadOperation *downloader = self.operationCache[url];
if (downloader == nil) {
NSLog(@"没有对应的下载操作要暂停");
return;
}
// 暂停下载
[downloader pause];
// 从缓冲池中移除操作
[self.operationCache removeObjectForKey:url];
}
#pragma mark - 懒加载操作缓冲池
- (NSMutableDictionary *)operationCache {
if (_operationCache == nil) {
_operationCache = [NSMutableDictionary dictionary];
}
return _operationCache;
}
@end
10.4、自定义下载操作
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface HMDownLoadOperation : NSOperation
+(instancetype)downloadOperation:(void (^)(CGFloat progress))progress finished:(void (^)(NSString *filePath,NSError *error))finished;
/**
* 要下载文件的 url
*/
@property(nonatomic,strong) NSURL *url;
/**
* 根据指定的 url 下载文件
*/
-(void)download:(NSURL *)url;
/**
* 暂停下载
*/
- (void)pause;
@end
#import "HMDownLoadOperation.h"
@interface HMDownLoadOperation()<NSURLConnectionDataDelegate>
// 请求链接对象
@property(nonatomic,strong) NSURLConnection *connection;
// 文件输出流
@property(nonatomic,strong) NSOutputStream *fileStream;
// 保存下载文的件路径
@property(nonatomic,copy) NSString *destinationPath;
// 文件总大小
@property(nonatomic,assign) long long expectedContentLenght;
// 当前接收文件大小
@property(nonatomic,assign) long long currentFileSize;
// 下载进度回调
@property(nonatomic,copy) void (^progressBlock)(CGFloat progress);
// 下载完成&出错回调
@property(nonatomic,copy) void (^finishedBlock)(NSString *filePath,NSError *error);
@end
@implementation HMDownLoadOperation
/**
* 返回一个下载操作对象
*
* @param progress 进度回调
* @param finished 完成回调
* 提示:block 如果不在当前方法执行,则要使用一个属性保存。
*/
+(instancetype)downloadOperation:(void (^)(CGFloat progress))progress finished:(void (^)(NSString *filePath,NSError *error))finished{
// 断言 必须传人完成回调 block,进度回调 progress 可选
NSAssert(finished != nil, @"必须传人完成回调block");
HMDownLoadOperation *downloader = [[self alloc] init];
// 记录block
downloader.progressBlock = progress;
downloader.finishedBlock = finished;
return downloader;
}
- (void)main {
@autoreleasepool {
[self download:self.url];
}
}
/**
* 根据指定的 url 下载文件
*/
-(void)download:(NSURL *)url{
// 检查服务器文件信息
[self checkServerFileInfo:url];
// 检查本地文件信息
self.currentFileSize =[self checkLocalFileInfo];
NSLog(@"----%@",[NSThread currentThread]);
// 文件大小相等
if (self.currentFileSize == self.expectedContentLenght) {
// 判断是否有进度回调
if(self.progressBlock){
self.progressBlock(1);
}
// 主线程回调
dispatch_async(dispatch_get_main_queue(), ^{
self.finishedBlock(self.destinationPath,nil);
});
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)pause{
[self.connection cancel];
}
#pragma mark - 私有方法
// 检查服务器的文件信息
- (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;
}
#pragma mark - NSURLConnectionDataDelegate 代理方法
/**
* 接收到服务器响应的时候调用(状态行和响应头)
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// NSLog(@"response = %@",response);
// 根据文件名 创建输出流对象
self.fileStream = [NSOutputStream outputStreamToFileAtPath:self.destinationPath append:YES];
// 打开流
[self.fileStream open];
}
/**
* 接收到服务器返回的数据时调用,可能会被调用多次(所有的 data 的数据都是按顺序传递过来的)
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
self.currentFileSize += data.length;
// 计算进度值
CGFloat progress = (CGFloat)self.currentFileSize / self.expectedContentLenght;
NSLog(@"接收到数据 = %f",progress);
// 传递进度值给进度视图 -- 异步线程回调
if (self.progressBlock != nil) {
self.progressBlock(progress);
}
// 拼接数据
[self.fileStream write:data.bytes maxLength:data.length];
}
/**
* 网络请求结束调用(断开网络)
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"下载完成");
// 关闭流
[self.fileStream close];
// 主线程回调
dispatch_async(dispatch_get_main_queue(), ^{
self.finishedBlock(self.destinationPath,nil);
});
}
/**
* 网络连接发生错误的时候调用(任何网络请求都有可能出现错误)
* 在实际开发中一定要进行出错处理
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"error = %@",error);
// 关闭流
[self.fileStream close];
// 主线程回调
dispatch_async(dispatch_get_main_queue(), ^{
self.finishedBlock(nil,error);
});
}
@end
10.5、实现并发控制
#import "HMDownloadManager.h"
#import "HMDownLoadOperation.h"
@interface HMDownloadManager()
// 全局下载队列
@property(nonatomic,strong) NSOperationQueue *queue;
// 操作缓存池
@property(nonatomic,strong) NSMutableDictionary *operationCache;
@end
@implementation HMDownloadManager
+(instancetype)sharedManager{
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (void)downloadWithUrl:(NSURL *)url progress:(void (^)(CGFloat))progress finished:(void (^)(NSString *, NSError *))finished {
// 判断缓冲池中是否存在,有就直接返回
if (self.operationCache[url]) {
NSLog(@"正在玩命下载中....稍安勿躁");
return;
}
// 实例化下载操作对象
HMDownLoadOperation *downloader = [HMDownLoadOperation downloadOperation:progress finished:^(NSString *filePath, NSError *error) {
// 从缓存池中删除下载操作
// self:是单例,没有释放的需要,保存在静态区,跟随程序的销毁而销毁
//,这里可以不用 weak也不会有循环引用问题
[self.operationCache removeObjectForKey:url];
// 回调调用方的 block
finished(filePath,error);
}];
// 将操作对象添加到缓存池中
[self.operationCache setObject:downloader forKey:url];
// 设置下载 url
downloader.url = url;
// 将下载操作添加到队列中
[self.queue addOperation:downloader];
}
- (void)pause:(NSURL *)url {
// 根据 url 从缓存池中获得对应的下载操作
HMDownLoadOperation *downloader = self.operationCache[url];
if (downloader == nil) {
NSLog(@"没有对应的下载操作要暂停");
return;
}
// 暂停下载
[downloader pause];
// 从缓冲池中移除操作
[self.operationCache removeObjectForKey:url];
}
#pragma mark - 懒加载操作缓冲池
- (NSMutableDictionary *)operationCache {
if (_operationCache == nil) {
_operationCache = [NSMutableDictionary dictionary];
}
return _operationCache;
}
#pragma mark - 懒加操作队列
- (NSOperationQueue *)queue {
if (_queue == nil) {
_queue = [[NSOperationQueue alloc] init];
// 设置最大并发输
_queue.maxConcurrentOperationCount = 2;
}
return _queue;
}
10.6、回调细节
-
- 进度回调,通常在异步执行
- 通常进度回调的频率非常高!如果界面上有很多文件,同时下载,又要更新 UI,可能会造成界面的卡顿
- 让进度回调,在异步执行,可以有选择的处理进度的显示,例如:只显示一个指示器!
- 有些时候,如果文件很小,调用方通常不关心下载进度!(SDWebImage)
- 异步回调,可以降低对主线程的压力
-
- 完成回调,通常在主线程执行
- 调用方不用考虑线程间通讯,直接更新UI即可
- 完成只有一次
四、网络状态检测
#import "ViewController.h"
#import "Reachability.h"
@interface ViewController ()
@property(nonatomic,strong) Reachability *reachablityManager;
@end
@implementation ViewController
- (Reachability *)reachablityManager {
if (_reachablityManager == nil) {
// 如果指定主机名,找一个不容易‘当机’的服务器
// 在实际开发中,替换成公司的服务器主机名就行了
_reachablityManager = [Reachability reachabilityWithHostName:@"baidu.com"];
}
return _reachablityManager;
}
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(checkNetworkState) name:kReachabilityChangedNotification object:nil];
// 开始检测
[self.reachablityManager startNotifier];
}
- (void)dealloc {
// 移除通知
[[NSNotificationCenter defaultCenter ] removeObserver:self];
// 停止检测网络状态
[self.reachablityManager stopNotifier];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self checkNetworkState];
}
#pragma mark - 网络状态检测
-(void)checkNetworkState{
switch (self.reachablityManager.currentReachabilityStatus) {
case NotReachable: // 没有联网
NSLog(@"没有联网");
break;
case ReachableViaWiFi: // wifi
NSLog(@"不用花钱,尽管使用");
break;
case ReachableViaWWAN: // 2/3/4G
NSLog(@"要花钱,谨慎使用,土豪除外");
default:
break;
}
}
@end
五、ASI
5.0、ASI简介
什么是ASI
全称是ASIHTTPRequest,外号“HTTP终结者”,功能十分强大。
基于底层的CFNetwork框架,运行效率很高。可惜作者早已停止更新。
很多公司的旧项目里面都残留着它的身影,以前的很多iOS项目都是ASI + SBJson
会不会用ASI,可以算是检验是否为老牌iOS程序员的标准之一
ASI的github地址
https://github.com/pokeb/asi-http-requestASI的使用参考
http://www.cnblogs.com/dotey/archive/2011/05/10/2041966.html
http://www.oschina.net/question/54100_36184
5.1、ASI同步请求
#pragma mark -
- (void)syncDemo {
/**
问题:
1. 只要是网络访问,就有可能出错!
2. 超时时长!、
3. 多线程!
*/
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 1. url
NSURL *url = [NSURL URLWithString:@"http://192.168.31.2/videos.json"];
// 2. 请求
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
// 修改网络请求超时时长
// 默认的网络请求超时时长 10 秒,苹果官方的是 60 秒,SDWebImage 是 15 秒,AFN 是 60 秒
request.timeOutSeconds = 2.0;
// 这种方法在开发中很少用,因为不能指定时长,不能处理错误,只能根据data是否存在,判断网络请求是否出错!
// NSData *data = [NSData dataWithContentsOfURL:url];
// 3. 同步启动请求,会阻塞当前线程
[request startSynchronous];
// 出错处理
if (request.error) {
NSLog(@"%@", request.error);
return;
}
// 4. 就能够拿到响应的结果
NSLog(@"%@ %@", request.responseData, [NSThread currentThread]);
// 5. 如果返回的内容确实是字符串,可以使用 responseString
NSLog(@"%@ %@", request.responseString, [NSThread currentThread]);
// NSString *str = [[NSString alloc] initWithData:request.responseData encoding:NSUTF8StringEncoding];
// NSLog(@"%@", str);
});
}
5.2、ASI异步请求
-
在 ASI 中,异步请求,有三种回调方式
- 代理
- Block
- 自定义回调方法
1、代理监听回调
#pragma mark 通过代理来监听网络请求
- (void)asyncDemo {
// 1. url
NSURL *url = [NSURL URLWithString:@"http://192.168.31.2/videos.json"];
// 2. request
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
// 设置代理
request.delegate = self;
// 3. 启动异步
[request startAsynchronous];
}
#pragma mark 代理方法
// 开发多线程框架的时候,有一个细节
// 耗时的操作,框架来做,在后台线程,回调方法在主线程做,使用框架的人,不需要关心线程间通讯
- (void)requestStarted:(ASIHTTPRequest *)request {
NSLog(@"%s", __FUNCTION__);
}
- (void)request:(ASIHTTPRequest *)request didReceiveResponseHeaders:(NSDictionary *)responseHeaders {
NSLog(@"%s %@", __FUNCTION__, responseHeaders);
}
- (void)requestFinished:(ASIHTTPRequest *)request {
NSLog(@"%s %@ %@", __FUNCTION__, request.responseString, [NSThread currentThread]);
}
- (void)requestFailed:(ASIHTTPRequest *)request {
NSLog(@"失败 %@", request.error);
}
// 此方法知道就行,一旦实现了这个方法,那么在 requestFinished 方法中,就得不到最终的结果了!
//- (void)request:(ASIHTTPRequest *)request didReceiveData:(NSData *)data {
// NSLog(@"%s %@", __FUNCTION__, data);
//}
- 2、block 监听回调
#pragma mark 通过块代码来监听网络请求
- (void)asyncBlockDemo {
// 1. url
NSURL *url = [NSURL URLWithString:@"http://192.168.31.2/videos.json"];
// 2. 请求
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
// 设置代理
request.delegate = self;
// 2.1 块代码回调
// 开始
[request setStartedBlock:^{
NSLog(@"start");
}];
// 接收到响应头
[request setHeadersReceivedBlock:^(NSDictionary *responseHeaders) {
NSLog(@"block - %@", responseHeaders);
}];
// 接收到字节(下载)
// request setBytesReceivedBlock:^(unsigned long long size, unsigned long long total) {
//
// }
// 接收到数据,和代理方法一样,一旦设置,在网络完成时,就没有办法获得结果
// 实现这个方法,就意味着程序员自己处理每次接收到的二进制数据!
// [request setDataReceivedBlock:^(NSData *data) {
// NSLog(@"%@", data);
// }];
// 简单的网络访问
__weak typeof(request) weakRequest = request;
[request setCompletionBlock:^{
NSLog(@"block - %@", weakRequest.responseString);
}];
// 访问出错
[request setFailedBlock:^{
NSLog(@"block - %@", weakRequest.error);
}];
// 3. 发起异步
[request startAsynchronous];
}
- 3、自定义网络监听回调方法
#pragma mark 自行指定网络监听方法(知道就行)
- (void)asyncSelectorDemo {
// 1. url
NSURL *url = [NSURL URLWithString:@"http://192.168.31.2/videos.json"];
// 2. 请求
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
// 指定监听方法 - 接收到服务器的响应头方法没有指定,如果程序中实现,会同样会被调用!
// 开始的方法
[request setDidStartSelector:@selector(start:)];
// 完成的监听
[request setDidFinishSelector:@selector(finished:)];
// 失败的监听
[request setDidFailSelector:@selector(failed:)];
// 需要注意的,以上方法是在修改代理监听的执行方法
// 需要指定代理
request.delegate = self;
// 3. 启动请求
[request startAsynchronous];
}
- (void)start:(ASIHTTPRequest *)request {
NSLog(@"%s %@", __FUNCTION__, request);
}
- (void)finished:(ASIHTTPRequest *)request {
NSLog(@"%s %@", __FUNCTION__, request);
}
- (void)failed:(ASIHTTPRequest *)request {
NSLog(@"%s %@", __FUNCTION__, request);
}
5.3、ASI常用POST相关方法
#pragma mark - 登录
- (void)postLogin{
// 请求 url
NSURL *url = [NSURL URLWithString:@"http://localhost/login.php"];
// 创建请求对象
// 如果使用 post 请求,一般使用 ASIFormDataRequest
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
// 设置请求体
[request setPostValue:@"zhangsan" forKey:@"username"];
[request setPostValue:@"zhang" forKey:@"password"];
// 设置完成回调
__weak typeof(request) weakRequest = request;
[request setCompletionBlock:^{
NSLog(@"%@",weakRequest.responseString);
}];
// 发送异步请求
[request startAsynchronous];
}
#pragma mark - 上传 JSON 数据
- (void)postJson{
// 请求 url
NSURL *url = [NSURL URLWithString:@"http://localhost/post/postjson.php"];
// 创建请求对象 POST JSON
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
// ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
// 设置请求方法
[request setRequestMethod:@"POST"];
// 设置二进制数据
NSDictionary *dict = @{@"productId":@"123"};
// 序列化
NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:NULL];
// 设置请求体
[request setPostBody:[NSMutableData dataWithData:data]];
// 设置完成回调
__weak typeof(request) weakRequest = request;
[request setCompletionBlock:^{
NSLog(@"%@",weakRequest.responseString);
}];
// 发送异步请求
[request startAsynchronous];
}
5.4、ASI上传文件
#pragma mark - 上传文件
-(void)upload{
// url 是负责上传文件的脚本
NSURL *url = [NSURL URLWithString:@"http://localhost/post/upload.php"];
// 上传文件
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];;
// 设置上传的文件
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"other.png" ofType:nil];
/**
参数:
1、本地文件的路径
2、上传脚本中的字段名
*/
//[request addFile:filePath forKey:@"userfile"];
/**
参数:
1、本地文件的路径
2、保存到服务器的文件名
3、mine-Type
4、上传脚本中的字段名
*/
[request addFile:filePath withFileName:@"abc.png" andContentType:@"image/png" forKey:@"userfile"];
// 设置完成回调
__weak typeof(request) weakRequest = request;
[request setCompletionBlock:^{
NSLog(@"%@",weakRequest.responseString);
}];
// 发送异步请求
[request startAsynchronous];
}
5.5、ASI下载文件
- (void)download{
NSString *urlStr = @"http://localhost/1234视频.mp4";
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// 请求
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:urlStr]];
// 下载需要指定下载的路径(缓存路径)
NSString *cacheDir = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.mp4"];
NSLog(@"%@",cacheDir);
// 设置保存下载文件的目标路径
[request setDownloadDestinationPath:cacheDir];
// 断点续传
[request setAllowResumeForFileDownloads:YES];
// 需要设置临时文件
NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"aaa.mp4"];
[request setTemporaryFileDownloadPath:tempPath];
// 设置下载代理 -- 跟进下载进度
request.downloadProgressDelegate = self;
// 设置完成回调
__weak typeof(request) weakRequest = request;
[request setCompletionBlock:^{
NSLog(@"%@",weakRequest.responseString);
}];
// 发送异步请求
[request startAsynchronous];
}
/**
* 获取进度方法
*/
- (void)setProgress:(float)newProgress {
NSLog(@"%f",newProgress);
}