zoukankan      html  css  js  c++  java
  • iOS之断点下载,使用NSURLSession简单封装

    最近公司需要做个文件管理的功能模块,刚交到博主手上时,头都大了。因为没做过这方面的东西,只好咬牙加班,并请教某位大神,指点了一下,清楚研究方向,找了网上大量资料,最后实现简单的封装。

    上代码:.h文件

    #import <Foundation/Foundation.h>
    
    @interface DocDownloader : NSObject
    
    /**
     *  创建断点续传管理对象,启动下载请求
     *
     *  @param url          文件资源地址
     *  @param targetPath   文件存放路径
     *  @param success      文件下载成功的回调块
     *  @param failure      文件下载失败的回调块
     *  @param progress     文件下载进度的回调块
     *
     *  @return 断点续传管理对象
     *
     */
    +(DocDownloader*)resumeManagerWithURL:(NSURL*)url 
                                  targetPath:(NSString*)targetPath
                                     success:(void (^)())success
                                     failure:(void (^)(NSError *error))failure
                                    progress:(void (^)(long long totalReceivedContentLength, long long totalContentLength))progress;
    
    /**
     *  启动断点续传下载请求(普通的静态下载链接或GET请求)
     */
    -(void)start;
    
    /**
     *  启动断点续传下载请求(POST请求)
     *  
     *  @param params  POST的内容
     */
    -(void)startWithParams:(NSString *)params;
    
    /**
     *  取消断点续传下载请求
     */
    -(void)cancel;

    .m文件

    #import "DocDownloader.h"
    
    typedef void (^completionBlock)();
    typedef void (^progressBlock)();
    
    @interface DocDownloader ()<NSURLSessionDelegate, NSURLSessionTaskDelegate>
    
    @property (nonatomic, strong) NSURLSession *session;    //注意一个session只能有一个请求任务
    @property(nonatomic, readwrite, retain) NSError *error; //请求出错
    @property(nonatomic, readwrite, copy) completionBlock completionBlock;
    @property(nonatomic, readwrite, copy) progressBlock progressBlock;
    
    @property (nonatomic, strong) NSURL *url;           //文件资源地址
    @property (nonatomic, strong) NSString *targetPath; //文件存放路径
    @property long long totalContentLength;             //文件总大小
    @property long long totalReceivedContentLength;     //已下载大小
    
    /**
     *  设置成功、失败回调block
     *
     *  @param success 成功回调block
     *  @param failure 失败回调block
     */
    - (void)setCompletionBlockWithSuccess:(void (^)())success
                                  failure:(void (^)(NSError *error))failure;
    
    /**
     *  设置进度回调block
     *
     *  @param progress
     */
    -(void)setProgressBlockWithProgress:(void (^)(long long totalReceivedContentLength, long long totalContentLength))progress;
    
    /**
     *  获取文件大小
     *  @param path 文件路径
     *  @return 文件大小
     *
     */
    - (long long)fileSizeForPath:(NSString *)path;
    
    @end
    
    @implementation DocDownloader
    
    /**
     *  设置成功、失败回调block
     *
     *  @param success 成功回调block
     *  @param failure 失败回调block
     */
    - (void)setCompletionBlockWithSuccess:(void (^)())success
                                  failure:(void (^)(NSError *error))failure{
        
        __weak typeof(self) weakSelf = self;
        self.completionBlock = ^ {
            
            dispatch_async(dispatch_get_main_queue(), ^{
                
                if (weakSelf.error) {
                    if (failure) {
                        failure(weakSelf.error);
                    }
                } else {
                    if (success) {
                        success();
                    }
                }
                
            });
        };
    }
    
    /**
     *  设置进度回调block
     *
     *  @param progress
     */
    -(void)setProgressBlockWithProgress:(void (^)(long long totalReceivedContentLength, long long totalContentLength))progress{
        
        __weak typeof(self) weakSelf = self;
        self.progressBlock = ^{
            
            dispatch_async(dispatch_get_main_queue(), ^{
                
                progress(weakSelf.totalReceivedContentLength, weakSelf.totalContentLength);
            });
        };
    }
    
    /**
     *  获取文件大小
     *  @param path 文件路径
     *  @return 文件大小
     *
     */
    - (long long)fileSizeForPath:(NSString *)path {
        
        long long fileSize = 0;
        NSFileManager *fileManager = [NSFileManager new]; // not thread safe
        if ([fileManager fileExistsAtPath:path]) {
            NSError *error = nil;
            NSDictionary *fileDict = [fileManager attributesOfItemAtPath:path error:&error];
            if (!error && fileDict) {
                fileSize = [fileDict fileSize];
            }
        }
        return fileSize;
    }
    
    /**
     *  创建断点续传管理对象,启动下载请求
     *
     *  @param url          文件资源地址
     *  @param targetPath   文件存放路径
     *  @param success      文件下载成功的回调块
     *  @param failure      文件下载失败的回调块
     *  @param progress     文件下载进度的回调块
     *
     *  @return 断点续传管理对象
     *
     */
    +(DocDownloader*)resumeManagerWithURL:(NSURL*)url
                                  targetPath:(NSString*)targetPath
                                     success:(void (^)())success
                                     failure:(void (^)(NSError *error))failure
                                    progress:(void (^)(long long totalReceivedContentLength, long long totalContentLength))progress{
        
        DocDownloader *manager = [[DocDownloader alloc]init];
        
        manager.url = url;
        manager.targetPath = targetPath;
        [manager setCompletionBlockWithSuccess:success failure:failure];
        [manager setProgressBlockWithProgress:progress];
        
        manager.totalContentLength = 0;
        manager.totalReceivedContentLength = 0;
        
        return manager;
    }
    
    /**
     *  启动断点续传下载请求(普通的静态下载链接或GET请求)
     */
    -(void)start{
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:self.url];
        long long downloadedBytes = self.totalReceivedContentLength = [self fileSizeForPath:self.targetPath];
        if (downloadedBytes > 0) {
            NSString *requestRange = [NSString stringWithFormat:@"bytes=%llu-", downloadedBytes];
            [request setValue:requestRange forHTTPHeaderField:@"Range"];
        }else{
            int fileDescriptor = open([self.targetPath UTF8String], O_CREAT | O_EXCL | O_RDWR, 0666);
            if (fileDescriptor > 0) {
                close(fileDescriptor);
            }
        }
        NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:queue];
        NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];
        [dataTask resume];
    }
    
    /**
     *  启动断点续传下载请求(POST请求)
     *
     *  @param params  POST的内容
     */
    -(void)startWithParams:(NSString *)params{
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:self.url];
        [request setHTTPMethod:@"POST"];
        [request setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]];
        long long downloadedBytes = self.totalReceivedContentLength = [self fileSizeForPath:self.targetPath];
        if (downloadedBytes > 0) {
            
            NSString *requestRange = [NSString stringWithFormat:@"bytes=%llu-", downloadedBytes];
            [request setValue:requestRange forHTTPHeaderField:@"Range"];
        }else{
            
            int fileDescriptor = open([self.targetPath UTF8String], O_CREAT | O_EXCL | O_RDWR, 0666);
            if (fileDescriptor > 0) {
                close(fileDescriptor);
            }
        }
        NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:queue];
        NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];
        [dataTask resume];
    }
    
    /**
     *  取消断点续传下载请求
     */
    -(void)cancel{
        if (self.session) {
            [self.session invalidateAndCancel];
            self.session = nil;
        }
    }
    
    #pragma mark -- NSURLSessionDelegate
    /* The last message a session delegate receives.  A session will only become
     * invalid because of a systemic error or when it has been
     * explicitly invalidated, in which case the error parameter will be nil.
     */
    - (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error{
        NSLog(@"didBecomeInvalidWithError");
    }
    
    #pragma mark -- NSURLSessionTaskDelegate
    /* Sent as the last message related to a specific task.  Error may be
     * nil, which implies that no error occurred and this task is complete.
     */
    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
    didCompleteWithError:(nullable NSError *)error{
        NSLog(@"didCompleteWithError");
        if (error == nil && self.error == nil) {
            self.completionBlock();
        }else if (error != nil){
            if (error.code != -999) {
                self.error = error;
                self.completionBlock();
            }
        }else if (self.error != nil){
            self.completionBlock();
        }
    }
    
    #pragma mark -- NSURLSessionDataDelegate
    /* Sent when data is available for the delegate to consume.  It is
     * assumed that the delegate will retain and not copy the data.  As
     * the data may be discontiguous, you should use
     * [NSData enumerateByteRangesUsingBlock:] to access it.
     */
    - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
        didReceiveData:(NSData *)data{
        //NSLog(@"dataLength = %lu",(unsigned long)data.length);
        //根据status code的不同,做相应的处理
        NSHTTPURLResponse *response = (NSHTTPURLResponse*)dataTask.response;
        NSLog(@"response = %@",response);
        if (response.statusCode == 200) {
            NSString *contentRange = [response.allHeaderFields valueForKey:@"Content-Length"];
            self.totalContentLength = [contentRange longLongValue];
        }else if (response.statusCode == 206){
            NSString *contentRange = [response.allHeaderFields valueForKey:@"Content-Range"];
            if ([contentRange hasPrefix:@"bytes"]) {
                NSArray *bytes = [contentRange componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" -/"]];
                if ([bytes count] == 4) {
                    self.totalContentLength = [[bytes objectAtIndex:3] longLongValue];
                }
            }
        }else if (response.statusCode == 416){
            NSString *contentRange = [response.allHeaderFields valueForKey:@"Content-Range"];
            if ([contentRange hasPrefix:@"bytes"]) {
                NSArray *bytes = [contentRange componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" -/"]];
                if ([bytes count] == 3) {
                    self.totalContentLength = [[bytes objectAtIndex:2] longLongValue];
                    if (self.totalReceivedContentLength == self.totalContentLength) {
                        //说明已下完,更新进度
                        self.progressBlock();
                    }else{
                        //416 Requested Range Not Satisfiable
                        self.error = [[NSError alloc]initWithDomain:[self.url absoluteString] code:416 userInfo:response.allHeaderFields];
                    }
                }
            }
            return;
        }else{
            //其他情况还没发现
            return;
        }
        
        NSFileManager *fileManager = [NSFileManager defaultManager];
        //向文件追加数据
        NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:self.targetPath];
        [fileHandle seekToEndOfFile]; //将节点跳到文件的末尾
        [fileHandle writeData:data];//追加写入数据
        if ([fileManager fileExistsAtPath:self.targetPath]) {
            self.totalReceivedContentLength = [[fileManager attributesOfItemAtPath:self.targetPath error:nil] fileSize];
            if (self.totalContentLength == self.totalReceivedContentLength) {
                NSLog(@"下载完了");
                //下载完了,停止请求
                [self cancel];
                self.completionBlock();
            }
        }
        [fileHandle closeFile];
        self.progressBlock();
    }

    使用步骤:

    1.

     [DocDownloader resumeManagerWithURL:[NSURL URLWithString:urlStr] targetPath:self.targetPath success:^{
            NSLog(@"WebRequestTypeDocDownload_success");
            //下载完成,可以写入一些完成之后的操作
        } failure:^(NSError *error) {
            NSLog(@"WebRequestTypeDocDownload_failure");
           //下载失败,可以做相应的提示
        } progress:^(long long totalReceivedContentLength, long long totalContentLength) {
           //回调totalReceivedContentLength和totalContentLength
           // 下载了多少:totalReceivedContentLength 
           // 文件总大小:  totalContentLength
           // 进度条可以这样表示:
           //progress = totalReceivedContentLength / totalContentLength
        }];

    2.启动下载(POST请求)

     [self.manager startWithParams:paramStr];

    3.暂停下载

    - (void)suspendWithCancel {
    
        [self.manager cancel];
    
        self.manager = nil;
    
    }

    那么问题来了,如果下载了一部分就暂停了,退出app,重新进来,文件数据呢???

    这个其实我们已经写入文件了,最好写入Documents目录下。首先判断第一次进入时,检查文件路径是否存在,若存在就需要计算出文件大小,并与我们知道的文件的总大小做比较。

    这里比较,可以分为两种情况:(这里以fileSize为文件计算出的大小,totalFileSize为文件的总大小)

    第一种文件没有加密,这个很好处理,1.如果0<fileSize<totalFileSize,说明已经下载了一部分,下载进度为fileSize。2.如果fileSize==totalFileSize,说明已经下载完了,标记状态。

    第二种文件下载完成后加密,加密之后的文件大小会比原来的大小大一些(由于博主的加密方式是sm4加密,不知道其他的加密方法会不会比原来的大一些),

    1.如果0<fileSize<totalFileSize,说明已经下载了一部分,下载进度为fileSize。2.如果fileSize>=totalFileSize,说明已经下载完了,标记状态。

  • 相关阅读:
    Spring Boot 2.3.0 正式发布!
    当互联网码农遇见国企老同学
    GitHub发布重大更新,关系到所有程序员!
    开发者被要求向破解者道歉,竟揪出“阿里云假员工”,网友:这人有前科
    等了整整12年!Linux QQ 终于更新了!
    我的电脑不联网,很安全,黑客:你还有风扇呢
    grpc的简单用例 (golang实现)
    grpc的简单用例 (C++实现)
    redis键过期 (redis 2.6及以上)
    安装folly库以及folly的ConcurrentHashMap的简单使用
  • 原文地址:https://www.cnblogs.com/DWdan/p/4962578.html
Copyright © 2011-2022 走看看