美文网首页
05-文件下载

05-文件下载

作者: AlanGe | 来源:发表于2017-08-28 00:01 被阅读24次

    一、课程目标

    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);
    }
    

    相关文章

      网友评论

          本文标题:05-文件下载

          本文链接:https://www.haomeiwen.com/subject/wzzidxtx.html