05-文件下载

一、课程目标

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、回调细节
    1. 进度回调,通常在异步执行
      1. 通常进度回调的频率非常高!如果界面上有很多文件,同时下载,又要更新 UI,可能会造成界面的卡顿
      1. 让进度回调,在异步执行,可以有选择的处理进度的显示,例如:只显示一个指示器!
      1. 有些时候,如果文件很小,调用方通常不关心下载进度!(SDWebImage)
      1. 异步回调,可以降低对主线程的压力
    1. 完成回调,通常在主线程执行
      1. 调用方不用考虑线程间通讯,直接更新UI即可
      1. 完成只有一次

四、网络状态检测

#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

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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容