iOS大文件斷點下載

廢話不多所,直接上代碼
github網(wǎng)址:https://github.com/SPStore/SPHTTPSessionManager

.h文件

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger, SPDownloadWay) {
    SPDownloadWayResume,    // 這種下載方式支持重啟app時繼續(xù)上一次的下載
    SPDownloadWayRestart    // 這種下載方式不支持重啟app時繼續(xù)上一次的下載
    
    // SPDownloadWayResume和SPDownloadWayRestart的下載方式具體異同點如下:
    /*
    1、SPDownloadWayResume和SPDownloadWayRestart均支持斷點下載
    2、SPDownloadWayResume支持重啟app繼續(xù)上一次下載,SPDownloadWayRestart不支持
    3、SPDownloadWayResume會自動判斷是否下載完成,并保存每時每刻下載的進度值,SPDownloadWayRestart沒有此功能
    4、SPDownloadWayResume支持任何時刻刪除已經(jīng)下載的文件數(shù)據(jù),SPDownloadWayRestart不支持在下載過程中刪除,只有下載完成時才能刪除
    5、SPDownloadWayResume不依賴于AFN,SPDownloadWayRestart依賴AFN
     
     通俗的講,SPDownloadWayResume和SPDownloadWayRestart的根本區(qū)別就是前者是沙盒模式,后者是內存模式
     */
    
};

NS_ASSUME_NONNULL_BEGIN
@interface SPHTTPSessionManager : NSObject

/** 單例對象 */
+ (instancetype)shareInstance;

/**
 *  get請求
 *
 *  @param urlString 請求地址
 *  @param params    參數(shù)字典
 *  @param success   請求成功回調的block
 *  @param failure   請求失敗回調的block
 */
- (void)GET:(NSString *)urlString
     params:(NSDictionary *)params
    success:(void (^)(id responseObject))success
    failure:(void (^)(NSError *error))failure;

/**
 *  post請求
 *
 *  @param urlString 請求地址
 *  @param params    參數(shù)字典
 *  @param success   請求成功回調的block
 *  @param failure   請求失敗回調的block
 */
- (void)POST:(NSString *)urlString
      params:(NSDictionary *)params
     success:(void (^)(id responseObject))success
     failure:(void (^)(NSError *error))failure;

/**
 *  下載
 *
 *  @param urlString 請求地址
 *  @param downloadProgressBlock  下載過程中回調的block
 *  @complete 下載完成回調的block
 */
- (NSURLSessionTask *)downloadWithURL:(NSString *)urlString
                                     progress:(void (^)(CGFloat progress))downloadProgressBlock
                                     complete:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler;


/*
 * 上傳  http://www.cnblogs.com/qingche/p/5489434.html
 *
 */
- (void)uploadWithURL:(NSString *)urlString
               params:(NSDictionary *)params
             fileData:(NSData *)filedata
                 name:(NSString *)name
             fileName:(NSString *)filename
             mimeType:(NSString *) mimeType
             progress:(void (^)(NSProgress *uploadProgress))uploadProgressBlock
              success:(void (^)(id responseObject))success
              failure:(void (^)(NSError *error))failure;

// 以下這些操作在外界也可以另外做到,比如啟動和暫停任務,外界在調用下載的方法時返回了一個task,開發(fā)者可以用該task去啟動和暫停任務,之所以將其封裝,一是:這個類能做到的盡量不讓開發(fā)者去做,二是:讓開發(fā)者完全面向我這個單例對象。開發(fā)者只需要做一些關于UI的事情

// 下載方式
@property (nonatomic, assign) SPDownloadWay downloadway;

/*
 *  啟動任務
 *
 */
- (void)resumeTask;
/* 
 *  暫停任務
 *
 */
- (void)suspendTask;
/*
 *  取消任務
 *
 */
- (void)cancelTask;

/*
 *  移除已經(jīng)下載好的文件數(shù)據(jù) 
 *
 */
- (BOOL)removeDownloadedData:(NSError * _Nullable __autoreleasing * _Nullable)error;

/** 是否正在下載,對于SPDownloadResume下載方式,該屬性來源于沙盒,對于SPDownloadRestart下載方式,該屬性來源于內存 */
@property (nonatomic, assign, readonly, getter=isDownloading) BOOL downloading;


// 以下兩個屬性只對SPDownloadResume下載方式奏效

/** 保存在沙盒中的進度值 */
@property (nonatomic, assign, readonly) CGFloat storedDownloadProgress;
/** 是否已經(jīng)下載完畢 */
@property (nonatomic, assign, readonly, getter=isDownloadCompleted) BOOL downloadCompleted;

@end
NS_ASSUME_NONNULL_END


@interface NSString (MD5)

@property (nullable, nonatomic, readonly) NSString *md5String;

@end

.m文件

#import "SPHTTPSessionManager.h"
#import "AFNetworking.h"

// 文件名,MD5加密
#define SPFileName self.fileURLString.md5String

// 文件的存放路徑(caches)
#define SPFileFullPath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:SPFileName]

// 存儲文件信息的路徑(caches)
#define SPFileInfoPath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"sp_fileInfo.info"]

// 文件的已下載長度
#define SPDownloadLength [[[NSFileManager defaultManager] attributesOfItemAtPath:SPFileFullPath error:nil][NSFileSize] integerValue]

@interface SPHTTPSessionManager() <NSURLSessionDelegate> {
    CGFloat _storedDownloadProgress;
    BOOL _downloadCompleted;
    BOOL _downloading;
}
/** session */
@property (nonatomic, strong) NSURLSession *session;
/** 寫文件的流對象 */
@property (nonatomic, strong) NSOutputStream *stream;
/** 文件的總長度 */
@property (nonatomic, assign) NSInteger totalLength;
/** 下載任務 */
@property (nonatomic, strong) NSURLSessionTask *task;
/** 文件的url */
@property (nonatomic, strong) NSString *fileURLString;
/** 下載過程中回調的block */
@property (nonatomic, copy) void (^downloadProgressBlock)(CGFloat progress);
/** 下載完成回調的block */
@property (nonatomic,copy) void (^completionHandler)(NSURLResponse *response, NSURL *URL, NSError *error);
/** 存儲文件信息的字典,該字典要寫入沙盒 */
@property (nonatomic, strong) NSMutableDictionary *fileInfoDictionry;

// ------------ 上面的額屬性是針對下載2,下面的屬性針對下載1 -------------
/** 下載1的文件url地址 */
@property (nonatomic, copy) NSString *downloadFromZero_UrlString;
/** 下載1完成后保存的文件路徑 */
@property (nonatomic, copy) NSString *downloadFromZero_filePath;
/** 下載1過程中回調的block */
@property (nonatomic, copy) void (^downloadFromZero_ProgressBlock)(CGFloat progress);
/** 下載1完成回調的block */
@property (nonatomic,copy) void (^downloadFromZero_completionHandler)(NSURLResponse *response, NSURL *URL, NSError *error);

@end

@implementation SPHTTPSessionManager

+ (instancetype)shareInstance {
    static SPHTTPSessionManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[self alloc] init];
    });
    return manager;
}


// get請求
- (void)GET:(NSString *)urlString params:(NSDictionary *)params
   success:(void (^)(id responseObject))success
   failure:(void (^)(NSError *error))failure {
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/plain",@"application/json",@"text/json",@"text/javascript",@"text/html", nil];
    
    [manager GET:urlString parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        
        if (success) {
            success(responseObject);
        }
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        if (failure) {
            failure(error);
        };
    }];
}

// post請求
- (void)POST:(NSString *)urlString params:(NSDictionary *)params
    success:(void (^)(id responseObject))success failure:(void (^)(NSError *error))failure{
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/plain",@"application/json",@"text/json",@"text/javascript",@"text/html", nil];
    
    [manager POST:urlString parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        if (success) {
            success(responseObject);
        }
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        if (failure) {
            failure(error);
        }
    }];
    
}

// 下載1(重啟app時從0開始開始)
- (NSURLSessionTask *)downloadFromZeroWithURL:(NSString *)urlString
                                     progress:(void (^)(CGFloat))downloadProgressBlock
                                     complete:(void (^)(NSURLResponse *, NSURL *, NSError *))completionHandler {
    
    self.downloadFromZero_UrlString = urlString;
    self.downloadFromZero_ProgressBlock = downloadProgressBlock;
    self.downloadFromZero_completionHandler = completionHandler;
  
    return self.task;
}

// 下載2(重啟app時從上一次的數(shù)據(jù)開始)
- (NSURLSessionTask *)downloadWithURL:(NSString *)urlString
                                                   progress:(void (^)(CGFloat))downloadProgressBlock
                                                   complete:(void (^)(NSURLResponse *, NSURL *, NSError *))completionHandler {
    if (self.downloadway == SPDownloadWayResume) {
        self.fileURLString = urlString;
        
        // 將block參數(shù)賦值給全局block變量
        self.downloadProgressBlock = downloadProgressBlock;
        self.completionHandler = completionHandler;
        
        [self downloadFromZeroWithURL:urlString progress:downloadProgressBlock complete:completionHandler];
        
        return self.task;
    } else {
        return [self downloadFromZeroWithURL:urlString progress:downloadProgressBlock complete:completionHandler];
    }
}


// 上傳
- (void)uploadWithURL:(NSString *)urlString
              params:(NSDictionary *)params
            fileData:(NSData *)filedata
                name:(NSString *)name
            fileName:(NSString *)filename
            mimeType:(NSString *) mimeType
            progress:(void (^)(NSProgress *uploadProgress))uploadProgressBlock
             success:(void (^)(id responseObject))success
             failure:(void (^)(NSError *error))failure {
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    
    [manager POST:urlString parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
        
        [formData appendPartWithFileData:filedata name:name fileName:filename mimeType:mimeType];
        
    } progress:^(NSProgress * _Nonnull uploadProgress) {
        if (uploadProgressBlock) {
            uploadProgressBlock(uploadProgress);
        }
        
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        if (success) {
            success(responseObject);
        }
        
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        if (failure) {
            failure(error);
        }
    }];
}

// 啟動任務
- (void)resumeTask {
    [self.task resume];
}

// 暫停任務
- (void)suspendTask {
    [self.task suspend];
}

// 取消任務
- (void)cancelTask {
    [self.task cancel];
}

// 刪除
- (BOOL)removeDownloadedData:(NSError * _Nullable __autoreleasing * _Nullable)error {
    if (self.downloadway == SPDownloadWayResume) {
        if (SPDownloadLength) {
            
            BOOL isDirectory = NO;
            
            NSFileManager *manager = [NSFileManager defaultManager];
            // 刪除已經(jīng)下載好的文件
            if ([manager fileExistsAtPath:SPFileFullPath isDirectory:&isDirectory] && [manager fileExistsAtPath:SPFileInfoPath isDirectory:&isDirectory]) {
                // 移除
                BOOL removeFileSuccess = [manager removeItemAtPath:SPFileFullPath error:error];
                BOOL removeFileLengthSuccess = [manager removeItemAtPath:SPFileInfoPath error:error];
                if (removeFileSuccess && removeFileLengthSuccess) { // 移除成功

                    [self.task cancel];
                    self.task = nil;
                    return YES;
                } else {
                    NSLog(@"移除文件失敗");
                    return NO;
                }
                
            } else {
                NSLog(@"沒找到文件路徑");
                return NO;
            }
        } else {
            NSLog(@"沒有需要刪除的數(shù)據(jù)");
            return NO;
        }
    }
    else {
        if (!_downloading) { // 說明沒有正在下載(下載1)
            BOOL isDirectory = NO;
            
            NSFileManager *manager = [NSFileManager defaultManager];
            // 刪除已經(jīng)下載好的文件
            if ([manager fileExistsAtPath:self.downloadFromZero_filePath isDirectory:&isDirectory]) {
                // 移除
                BOOL removeSuccess = [manager removeItemAtPath:self.downloadFromZero_filePath error:error];
                if (removeSuccess) {
                    [self.task cancel];
                    self.task = nil;
                    return YES;
                } else {
                    NSLog(@"移除失敗");
                    return NO;
                }
                return YES;
            } else {
                NSLog(@"沒找到文件路徑");
                return NO;
            }
        }
        else { // 正在下載
            NSLog(@"****** ‘SPDownloadWayRestart‘不支持在下載過程中刪除");
           return NO;
        }
    }
    
}

// 從沙盒中獲取下載的進度值
- (CGFloat)storedDownloadProgress {
    _storedDownloadProgress = [self.fileInfoDictionry[@"downloadProgress"] floatValue];
    return _storedDownloadProgress;
}

// 從沙盒中獲取下載是否完畢的標識
- (BOOL)isDownloadCompleted {
    _downloadCompleted = self.fileInfoDictionry[@"downloadCompleted"];
    return _downloadCompleted;
}

// 從沙盒中獲取是否正在下載的標識
- (BOOL)isDownloading {
    _downloading = self.fileInfoDictionry[@"downloading"];
    return _downloading;
}

- (NSOutputStream *)stream {
    if (!_stream) {
        _stream = [NSOutputStream outputStreamToFileAtPath:SPFileFullPath append:YES];
    }
    return _stream;
}

- (NSURLSession *)session {
    if (!_session) {
        _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
    }
    return _session;
}

- (NSURLSessionTask *)task {
    if (!_task) {
        
        if (self.downloadway == SPDownloadWayResume) {
            // 取出文件的總長度
            NSInteger totalLength = [self.fileInfoDictionry[SPFileName] integerValue];
            if (totalLength && SPDownloadLength == totalLength) {
                NSLog(@"文件已經(jīng)下載完成了");
                return nil;
            }
            // 創(chuàng)建請求
            NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.fileURLString]];
            
            // 設置請求頭
            // Range : bytes=xxx-xxx
            NSString *range = [NSString stringWithFormat:@"bytes=%zd-", SPDownloadLength];
            [request setValue:range forHTTPHeaderField:@"Range"];
            
            // 創(chuàng)建一個Data任務
            _task = [self.session dataTaskWithRequest:request];
        }
        else {
            AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
            
            NSURL *urlpath = [NSURL URLWithString:self.downloadFromZero_UrlString];
            NSURLRequest *request = [NSURLRequest requestWithURL:urlpath];
            
            _task = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
                
                _downloading = YES;
                
                CGFloat progress = 1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount;
                
                if (self.downloadFromZero_ProgressBlock) {
                    self.downloadFromZero_ProgressBlock(progress);
                }
                
            } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
                
                NSString *cachesPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
                
                NSURL *fileURL = [NSURL fileURLWithPath:cachesPath];
                
                return fileURL;
                
            } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
                
                _downloading = NO;
                self.downloadFromZero_filePath = filePath.path;
                
                if (self.downloadFromZero_completionHandler) {
                    self.downloadFromZero_completionHandler(response,filePath,error);
                }
            }];
        }
    }
    return _task;
        
}

- (NSMutableDictionary *)fileInfoDictionry {
    if (_fileInfoDictionry == nil) {
        //  通過文件文件路徑初始化字典,第一次取出來的必為空,因為此時還沒有寫進沙盒
       _fileInfoDictionry = [NSMutableDictionary dictionaryWithContentsOfFile:SPFileInfoPath];
        if (_fileInfoDictionry == nil) {
            _fileInfoDictionry = [NSMutableDictionary dictionary];
        }
    }
    return _fileInfoDictionry;
}


#pragma mark - <NSURLSessionDataDelegate>
/**
 * 1.接收到響應
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    
    // 打開流
    [self.stream open];
    
    // 獲得服務器這次請求 返回數(shù)據(jù)的總長度
    self.totalLength = [response.allHeaderFields[@"Content-Length"] integerValue] + SPDownloadLength;
    
    // 存儲總長度
    self.fileInfoDictionry[SPFileName] = @(self.totalLength);
    [self.fileInfoDictionry writeToFile:SPFileInfoPath atomically:YES];
    
    // 接收這個請求,允許接收服務器的數(shù)據(jù)
    completionHandler(NSURLSessionResponseAllow);
}

/**
 * 2.接收到服務器返回的數(shù)據(jù)(這個方法可能會被調用N次)
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    
    // 寫入數(shù)據(jù),不需要指定寫入到哪個路徑,因為stream在創(chuàng)建的那一刻就紀錄好存儲路徑
    [self.stream write:data.bytes maxLength:data.length];
    
    // 回調block
    if (self.downloadProgressBlock) {

        // 獲取進度值
        CGFloat progress = 1.0 * SPDownloadLength / self.totalLength;
        //NSLog(@"++++++%f",progress);
        self.downloadProgressBlock(progress);

        self.fileInfoDictionry[@"downloadProgress"] = @(progress);  // 進度值
        self.fileInfoDictionry[@"downloading"] = @(YES); // 正在下載的標識
        [self.fileInfoDictionry writeToFile:SPFileInfoPath atomically:YES];

    }
}

/**
 * 3.請求完畢(成功\失敗)
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    if (self.completionHandler) {
        self.completionHandler(task.response,[NSURL fileURLWithPath:SPFileFullPath],error);
        // 關閉流
        [self.stream close];
        self.stream = nil;
        
        // 清除任務
        self.task = nil;
        
        if (!error) {

            self.fileInfoDictionry[@"downloadCompleted"] = @(YES);  // 下載完成的標識
            self.fileInfoDictionry[@"downloading"] = @(NO); // 正在下載的標識
            [self.fileInfoDictionry writeToFile:SPFileInfoPath atomically:YES];
        } else {
            if ([error.domain isEqualToString:@"NSURLErrorDomain"] && error.code == -999) {
                return;
            }
        }
    }
    
}

@end

#import <CommonCrypto/CommonDigest.h>

@implementation NSString (MD5)

- (NSString *)md5String {
    
    const char *string = self.UTF8String;
    int length = (int)strlen(string);
    unsigned char bytes[CC_MD5_DIGEST_LENGTH];
    CC_MD5(string, length, bytes);
    return [self stringFromBytes:bytes length:CC_MD5_DIGEST_LENGTH];
}

- (NSString *)stringFromBytes:(unsigned char *)bytes length:(NSInteger)length {
    
    NSMutableString *mutableString = @"".mutableCopy;
    for (int i = 0; i < length; i++)
        [mutableString appendFormat:@"%02x", bytes[i]];
    return [NSString stringWithString:mutableString];
}

@end

之所以不用大家普遍使用的URLSessionDownloadTask,是因為它幾乎無法做到殺死app后繼續(xù)下載,盡管有個resumeData,也只能實現(xiàn)后臺或者暫停后繼續(xù)下載。

github網(wǎng)址:https://github.com/SPStore/SPHTTPSessionManager

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容