NSURLConnection下载
课程目标
- NSURLConnection下载是一个网络多线程的综合性演练项目
- 充分体会 NSURLConnection 开发中的细节
- 虽然 NSURLConnection 在 iOS 9.0 中已经被废弃,但是作为资深的 iOS 程序员,必须要了解 NSURLConnection 的细节
- 利用 HTTP 请求头的 Range 实现断点续传
- 利用 NSOutputStream 实现文件流拼接
- 自定义 NSOperation及操作缓存管理
- Block 的综合演练
- 利用 IB_DESIGNABLE 和 IBInspectable 实现在 Stroybaord 中自定义视图的实时渲染
-
NSURLSession 从 Xcode 6.0 到 Xcode 6.3.1 都存在内存问题,历时7个月,如下图所示
NSURLConnection 的历史
- iOS 2.0 推出的,至今有10多年的历史
- 苹果几乎没有对 NSURLConnection 做太大的改动
- sendAsynchronousRequest 方法是 iOS 5.0 之后,苹果推出的
- 在 iOS 5.0 之前,苹果的网络开发是处于黑暗时代
- 需要使用代理方法,还需要使用运行循环,才能够处理复杂的网络请求!
- 只提供了 启动 和 取消 两个方法,没有中间状态
使用异步方法下载
- (void)downloadWithURL:(NSURL *)url {
// 请求
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeout];
NSLog(@"start");
// 下载
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// 将文件写入磁盘
[data writeToFile:@"/Users/liufan/Desktop/123" atomically:YES];
NSLog(@"下载完成");
}];
}
问题:
- 没有进度跟进,用户体验不好
- 会出现内存峰值,如果文件太大,在真机上会闪退
解决办法
- 使用代理方法来解决下载进度跟进的问题
HEAD方法
HEAD
方法通常是用来在下载文件之前,获取远程服务器上的文件信息
- 与 GET 方法相比,同样能够拿到响应头,但是不返回数据实体
- 用户可以根据响应头信息,确定下一步操作
NSURL *url = [NSURL URLWithString:@"http://localhost/demo.json"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:10.0];
request.HTTPMethod = @"HEAD";
NSURLResponse *response = nil;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
NSLog(@"要下载文件的长度 %tu", response.expectedContentLength);
同步方法
- 同步方法是阻塞式的,通常只有
HEAD
方法才会使用同步方法 - 如果在开发中,看到参数的类型是
**
,就传入对象的地址
注意
-
NSURLConnectionDownloadDelegate
代理方法是为 Newsstand Kit’s(杂志包) 创建的下载服务的 - Newsstand 主要在国外使用比较广泛,国内极少
- 如果使用
NSURLConnectionDownloadDelegate
代理方法监听下载进度,能够监听到进度,但是:找不到下载的文件
示例代码如下:
- (void)downloadWithURL:(NSURL *)url {
// 请求
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeout];
[NSURLConnection connectionWithRequest:request delegate:self];
}
#pragma mark - NSURLConnectionDownloadDelegate
- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes {
NSLog(@"%f", (float)totalBytesWritten / expectedTotalBytes);
}
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL {
NSLog(@"%@", destinationURL);
}
跟踪下载进度
#pragma mark - NSURLConnectionDataDelegate
// 1. 接收到服务器响应
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(@"%@", response);
self.expectedContentLength = response.expectedContentLength;
self.fileSize = 0;
}
// 2. 接收到数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
self.fileSize += data.length;
float progress = (float)self.fileSize / self.expectedContentLength;
NSLog(@"%f", progress);
}
// 3. 接收完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"下载完成");
}
// 4. 网络错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%@", error);
}
拼接数据
- (NSMutableData *)fileData {
if (_fileData == nil) {
_fileData = [NSMutableData data];
}
return _fileData;
}
// 2. 接收到数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
self.fileSize += data.length;
float progress = (float)self.fileSize / self.expectedContentLength;
NSLog(@"%f", progress);
// 拼接数据
[self.fileData appendData:data];
}
// 3. 接收完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"下载完成");
[self.fileData writeToFile:@"/Users/liufan/Desktop/321" atomically:YES];
self.fileData = nil;
}
存在的问题
- 内存峰值依旧
意外发现:运行结果和 NSURLConnection 的异步方法的效果几乎一样!
NSFileHandle 拼接文件
-
NSFileManager
: 主要是做文件的删除,移动,复制,检查文件是否存在等操作,类似于 Finder -
NSFileHandle
: 文件句柄(指针)
,操纵,提示:凡是看到 Handle 这个单词,就表示对前面一个单词(File)的独立操作
- (void)writeData:(NSData *)data {
NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:self.targetPath];
if (fp == nil) {
[data writeToFile:self.targetPath atomically:YES];
} else {
[fp seekToEndOfFile];
[fp writeData:data];
[fp closeFile];
}
}
问题:文件会被重复追加
// 下载前删除文件
[[NSFileManager defaultManager] removeItemAtPath:self.targetPath error:NULL];
NSOutputStream 拼接文件
定义属性
// 1. 接收到服务器响应
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(@"%@", response);
self.expectedContentLength = response.expectedContentLength;
self.fileSize = 0;
self.targetPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
NSLog(@"%@", self.targetPath);
// 删除文件
[[NSFileManager defaultManager] removeItemAtPath:self.targetPath error:NULL];
// 打开文件流
self.fileStream = [[NSOutputStream alloc] initToFileAtPath:self.targetPath append:YES];
[self.fileStream open];
}
// 2. 接收到数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
self.fileSize += data.length;
float progress = (float)self.fileSize / self.expectedContentLength;
NSLog(@"%f", progress);
// 拼接数据
[self.fileStream write:data.bytes maxLength:data.length];
}
// 3. 接收完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"下载完成");
// 关闭流
[self.fileStream close];
}
// 4. 网络错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%@", error);
[self.fileStream close];
}
文件流操作方法
打开流 - 要对文件读写之前,首先需要打开流
- (void)open;
关闭流 - 对文件读写操作完成之后,需要关闭流
- (void)close;
将数据写入到流
- (NSInteger)write:(const uint8_t *)buffer maxLength:(NSUInteger)len;
断点续传
确认思路
- 检查服务器文件信息
- 检查本地文件
- 如果比服务器文件小,续传
- 如果比服务器文件大,重新下载
- 如果和服务器文件一样,下载完成
- 断点续传
代码实现
检查服务器文件信息
/// 检查服务器文件信息
- (void)remoteInfoWithURL:(NSURL *)url {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"HEAD";
NSURLResponse *response = nil;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
self.expectedContentLength = response.expectedContentLength;
self.targetPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
}
检查本地文件
/// 检查本地文件大小
- (long long)localFileSize {
NSFileManager *manager = [NSFileManager defaultManager];
long long fileSize = 0;
// 1. 文件是否存在
if ([manager fileExistsAtPath:self.targetPath]) {
fileSize = [[manager attributesOfItemAtPath:self.targetPath error:NULL] fileSize];
}
// 2. 判断是否大于服务器大小
if (fileSize > self.expectedContentLength) {
[manager removeItemAtPath:self.targetPath error:NULL];
fileSize = 0;
}
return fileSize;
}
断点续传
/// 从偏移位置下载文件
- (void)downloadWithURL:(NSURL *)url offset:(long long)offset {
self.fileSize = offset;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeout];
NSString *rangeStr = [NSString stringWithFormat:@"bytes=%lld-", offset];
[request setValue:rangeStr forHTTPHeaderField:@"Range"];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
[conn start];
}
修改代理方法
// 1. 接收到服务器响应
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// 打开文件流
self.fileStream = [[NSOutputStream alloc] initToFileAtPath:self.targetPath append:YES];
[self.fileStream open];
}
下载主方法
- (void)downloadWithURL:(NSURL *)url {
// 1. 检查服务器文件信息
[self remoteInfoWithURL:url];
// 2. 检查本地文件大小
long long fileSize = [self localFileSize];
if (fileSize == self.expectedContentLength) {
NSLog(@"下载完成");
return;
}
// 3. 从偏移位置下载文件
[self downloadWithURL:url offset:fileSize];
}
多线程
- 异步下载
- (void)downloadWithURL:(NSURL *)url {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 1. 检查服务器文件信息
[self remoteInfoWithURL:url];
// 2. 检查本地文件大小
long long fileSize = [self localFileSize];
if (fileSize == self.expectedContentLength) {
NSLog(@"下载完成");
return;
}
// 3. 从偏移位置下载文件
[self downloadWithURL:url offset:fileSize];
});
}
- 启动运行循环
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
[conn start];
// NSURLConnection 会在网络请求结束后,自动停止运行循环
[[NSRunLoop currentRunLoop] run];
NSLog(@"come here %@", [NSThread currentThread]);
完成回调
回调细节
-
进度回调,通常在异步执行
- 通常进度回调的频率非常高!如果界面上有很多文件,同时下载,又要更新 UI,可能会造成界面的卡顿
- 让进度回调,在异步执行,可以有选择的处理进度的显示,例如:只显示一个指示器!
- 有些时候,如果文件很小,调用方通常不关心下载进度!(SDWebImage)
- 异步回调,可以降低对主线程的压力
-
完成回调,通常在主线程执行
- 调用方不用考虑线程间通讯,直接更新UI即可
- 完成只有一次
增加类方法
/// 实例化下载操作
///
/// @param url 下载文件的URL
/// @param progress 进度回调
/// @param finised 完成回调
///
/// @return 下载操作
+ (instancetype)downloadWithURL:(NSURL *)url progress:(void (^)(float progress))progress finised:(void (^)(NSString *filePath, NSError *error))finised;
/// 开始下载
- (void)download;
利用属性记录block
- 如果本方法可以直接调用,就不需要使用属性记录
- 如果本方法不能直接调用,就需要使用属性记录,然后在需要的时候执行
定义 block 属性
/// 下载文件 URL
@property (nonatomic, strong) NSURL *url;
/// 进度回调
@property (nonatomic, copy) void (^progressBlock)(float);
/// 完成回调
@property (nonatomic, copy) void (^finishedBlock)(NSString *, NSError *);
类方法实现
+ (instancetype)downloadWithURL:(NSURL *)url progress:(void (^)(float))progress finised:(void (^)(NSString *, NSError *))finised {
HMDownloadOperation *d = [[HMDownloadOperation alloc] init];
// 记录属性
d.url = url;
d.progressBlock = progress;
d.finishedBlock = finised;
return d;
}
在视图控制器中准备块代码
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSURL *url = [NSURL URLWithString:@"http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.0.2.dmg"];
HMDownloadOperation *down = [HMDownloadOperation downloadWithURL:url progress:^(float progress) {
NSLog(@"%f %@", progress, [NSThread currentThread]);
} finised:^(NSString *filePath, NSError *error) {
NSLog(@"%@ %@ %@", filePath, error, [NSThread currentThread]);
}];
[down download];
}
进度回调
if (self.progressBlock) {
self.progressBlock(progress);
}
完成回调
dispatch_async(dispatch_get_main_queue(), ^{
self.finishedBlock(self.filePath, nil);
});
失败回调
dispatch_async(dispatch_get_main_queue(), ^{
self.finishedBlock(nil, error);
});
暂停下载
暂停下载
- (void)pause {
[self.conn cancel];
}
Cancels an asynchronous load of a request.
After this method is called, the connection makes no further delegate method calls. If you want to reattempt the connection, you should create a new connection object.取消一个异步请求,调用此方法后,
connection
不会再调用代理方法。如果要再次尝试连接,需要建立一个新的连接对象
下载进度视图
属性
IB_DESIGNABLE
@interface ProgressButton : UIButton
@property (nonatomic, assign) IBInspectable float progress;
@property (nonatomic, strong) IBInspectable UIColor *lineColor;
@property (nonatomic, assign) IBInspectable CGFloat lineWidth;
@end
代码实现
@implementation ProgressButton
- (void)setProgress:(float)progress {
_progress = progress;
[self setTitle:[NSString stringWithFormat:@"%.02f%%", progress * 100] forState:UIControlStateNormal];
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
CGPoint center = CGPointMake(rect.size.width * 0.5, rect.size.height * 0.5);
CGFloat r = (MIN(rect.size.width, rect.size.height) - self.lineWidth) * 0.5;
CGFloat startAngle = - M_PI_2;
CGFloat endAngle = self.progress * 2 * M_PI + startAngle;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:r startAngle:startAngle endAngle:endAngle clockwise:YES];
path.lineWidth = self.lineWidth;
path.lineCapStyle = kCGLineCapRound;
[self.lineColor setStroke];
[path stroke];
}
@end
Storyboard 技巧
在 SB 中直接设置自定义视图属性
下载管理器
单例
+ (instancetype)sharedDownloadManager {
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
移植下载方法
- (void)downloadWithURL:(NSURL *)url progress:(void (^)(float))progress finised:(void (^)(NSString *, NSError *))finised {
HMDownloadOperation *downloader = [HMDownloadOperation downloadWithURL:url progress:progress finised:finised];
[downloader download];
}
下载缓冲池
缓冲池属性
/// 下载缓冲池
@property (nonatomic, strong) NSMutableDictionary *downloaderCache;
// MARK: - 懒加载
- (NSMutableDictionary *)downloaderCache {
if (_downloaderCache == nil) {
_downloaderCache = [[NSMutableDictionary alloc] init];
}
return _downloaderCache;
}
修改下载方法
- (void)downloadWithURL:(NSURL *)url progress:(void (^)(float))progress finised:(void (^)(NSString *, NSError *))finised {
// 1. 判断下载操作缓冲池中是否存在下载操作
if (self.downloaderCache[url]) {
NSLog(@"正在玩命下载中...");
return;
}
// 2. 实例化下载操作
HMDownloadOperation *downloader = [HMDownloadOperation downloadWithURL:url progress:progress finised:finised];
// 3. 添加到下载操作缓冲池
[self.downloaderCache setObject:downloader forKey:url];
// 4. 开始下载
[downloader download];
}
下载完成后,将操作从缓冲池中删除
// 2. 实例化下载操作
HMDownloadOperation *downloader = [HMDownloadOperation downloadWithURL:url progress:progress finised:^(NSString *filePath, NSError *error) {
// 将操作从缓冲池中删除
[self.downloaderCache removeObjectForKey:url];
// 执行调用方准备的 finished
finised(filePath, error);
}];
NSOperation
使用 NSOperation 改造 HMDownloader
修改父类
@interface HMDownloadOperation : NSOperation
重写 main
方法
- 自定义操作,重写了main方法,在当操作被添加到队列的时候,会自动被执行
- 不要忘记自动释放池
- (void)main {
// 自定义操作千万不要忘记自动释放池
@autoreleasepool {
// 执行下载
[self download];
}
}
修改管理器代码
操作队列
@property (nonatomic, strong) NSOperationQueue *downloaderQueue;
- (NSOperationQueue *)downloaderQueue {
if (_downloaderQueue == nil) {
_downloaderQueue = [[NSOperationQueue alloc] init];
}
return _downloaderQueue;
}
修改开始下载代码
// 4. 开始下载
[self.downloaderQueue addOperation:downloader];
取消下载操作
- (void)pauserWithURL:(NSURL *)url {
// 1. 在缓冲池中查找下载操作
HMDownloadOperation *downloader = self.downloaderCache[url];
// 2. 判断是否存在下载操作
if (downloader == nil) {
NSLog(@"%@", self.downloaderQueue.operations);
return;
}
// 3. 暂停操作,操作队列会认为操作已经完成,会自动将操作从操作队列中删除
[downloader pause];
// 4. 将下载操作从缓冲池中删除
[self.downloaderCache removeObjectForKey:url];
}
重构步骤笔记
重构的目的
- 相同的代码不要出现两次
- 相同功能的代码可以及时抽取,以备日后复用,不要
重复创建轮子
重构的原则
- 明确
每一步的目标
- 小步走
- 测试(每一个改动都有可能出现错误)
抽取代码的步骤
- 新建方法
- 复制代码
- 根据代码调整参数和返回值
- 调整调用位置代码
- 测试
抽取类的步骤
- 示意图
抽取主方法
- 新建类
- 抽取主方法
- 在
.h
中定义方法接口,明确该方法是否适合被外部调用 - 在
.m
中增加方法实现
- 在
- 将主方法复制到新方法中
- 复制相关的
子
方法 - 复制相关属性
- 检查代码的
有效性
- 调整内部变量,让
NSURL
由调用方传递,保证代码的灵活性
- 调整内部变量,让
- 复制代理方法,
- 注释更新
UI
部分的代码 - 使用
#warning TODO
提醒自己此处有未完成的工作 - 这样做可以不影响重构的节奏
- 注释更新
- 调整视图控制器
测试
重构方法执行 - 调整视图控制器代码,删除被移走代码
- 再次测试,确保调整没有失误!
确认接口
- 确认重构的接口
- 需要
进度
回调 - 需要
完成&错误
回调
- 需要
- 定义类方法,传递回调参数
- 实现类方法,记录住回调 block
- 调整调用方法
- 增加
block
实现 - 测试
- 增加已经下载完成的回调
- 进度回调(100%)
- 完成回调(路径)
- 断言
- 暂停操作
- 测试,测试,测试!
新问题:如果连续点击,会重复下载,造成错乱!<br /><br />解决办法:建立一个下载管理器的单例,负责所有的文件下载,以及下载操作的缓存!
- 示意图
抽取下载管理器
- 建立单例
-
接管
下载操作- 定义接口方法
- 实现方法
- 替换方法
- 测试
- 操作缓存
- 暂停实现
- 最大并发数,
NSOperationQueue+NSOperation
block 小结
- block 是
C
语言的数据结构 - 是预先准备好的代码,在需要时执行,类似于匿名函数指针
- 可以被当作参数传递
- 在需要时,可以对
block
进行扩展 - 如果当前方法不执行
block
,需要使用属性
记录 -
block
属性需要使用copy
描述符 - 对于必须传递的
block
回调,可以使用断言