1. 程式人生 > >iOS 多工下載(支援離線)【轉】

iOS 多工下載(支援離線)【轉】

轉自:https://blog.csdn.net/jiuchabaikaishui/article/details/68485743

程式碼下載
程式碼下載地址

效果展示


分析
說到iOS中的下載,有很多方式可以實現,NSURLConnection(已經棄用)就不說了,AFNetworking也不說了。我使用的是NSURLSession,常用的有3個任務類,NSURLSessionDataTask、NSURLSessionDownloadTask、NSURLSessionUploadTask,它們都繼承自NSURLSessionTask。很明顯他們一個用於獲取資料一個用於下載另一個用於上傳的。首先我們肯定選擇使用NSURLSessionDownloadTask來做下載,那麼接下來就聊聊吧。

建立一個NSURLSession例項來管理網路任務
        //可以上傳下載HTTP和HTTPS的後臺任務(程式在後臺執行)。 在後臺時,將網路傳輸交給系統的單獨的一個程序,即使app掛起、推出甚至崩潰照樣在後臺執行。
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"QSPDownload"];
        _session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
1
2
3
2.新增下載任務

/**
 新增下載任務

 @param netPath 下載地址
 */
 - (void)addDownloadTast:(NSString *)netPath
{
    NSURLSessionDownloadTask *tast = [self.session downloadTaskWithURL:[NSURL URLWithString:netPath]];
    [(NSMutableArray *)self.downloadSources addObject:tast];
    //開始下載任務
    [task resume];
}
 
3.實現相關協議 
NSURLSessionDownloadTaskDelegate協議有如下3個方法:

這個方法在下載過程中反覆呼叫,用於獲知下載的狀態 
- (void)URLSession:(NSURLSession )session downloadTask:(NSURLSessionDownloadTask )downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite 
這個方法在下載完成之後呼叫,用於獲取下載後的檔案 
- (void)URLSession:(NSURLSession )session downloadTask:(NSURLSessionDownloadTask )downloadTask didFinishDownloadingToURL:(NSURL *)location 
這個方法在暫停後重新開始下載時呼叫,一般不操作這個代理 
- (void)URLSession:(NSURLSession )session downloadTask:(NSURLSessionDownloadTask )downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes

我們可以使用- (void)cancelByProducingResumeData:(void (^)(NSData * _Nullable resumeData))completionHandler這個方法來暫停任務,使用- (NSURLSessionDownloadTask )downloadTaskWithResumeData:(NSData )resumeData這個方法來重啟任務。可是沒有辦法獲取下載過程中的資料來實現離線下載,程式退出後就要重新下載了。

使用NSURLSessionDataTask實現離線下載
一、包裝一個QSPDownloadSource類來儲存每個下載任務的資料資源,並且實現NSCoding協議用於歸檔儲存資料,並通QSPDownloadSourceDelegate協議來監聽每個下載任務的過程

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSInteger, QSPDownloadSourceStyle) {
    QSPDownloadSourceStyleDown = 0,//下載
    QSPDownloadSourceStyleSuspend = 1,//暫停
    QSPDownloadSourceStyleStop = 2,//停止
    QSPDownloadSourceStyleFinished = 3,//完成
    QSPDownloadSourceStyleFail = 4//失敗
};

@class QSPDownloadSource;
@protocol QSPDownloadSourceDelegate <NSObject>
@optional
- (void)downloadSource:(QSPDownloadSource *)source changedStyle:(QSPDownloadSourceStyle)style;
- (void)downloadSource:(QSPDownloadSource *)source didWriteData:(NSData *)data totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
@end

@interface QSPDownloadSource : NSObject <NSCoding>
//地址路徑
@property (copy, nonatomic, readonly) NSString *netPath;
//本地路徑
@property (copy, nonatomic, readonly) NSString *location;
//下載狀態
@property (assign, nonatomic, readonly) QSPDownloadSourceStyle style;
//下載任務
@property (strong, nonatomic, readonly) NSURLSessionDataTask *task;
//檔名稱
@property (strong, nonatomic, readonly) NSString *fileName;
//已下載的位元組數
@property (assign, nonatomic, readonly) int64_t totalBytesWritten;
//檔案位元組數
@property (assign, nonatomic, readonly) int64_t totalBytesExpectedToWrite;
//是否離線下載
@property (assign, nonatomic, getter=isOffLine) BOOL offLine;
//代理
@property (weak, nonatomic) id<QSPDownloadSourceDelegate> delegate;

@end
 
二、使用單例QSPDownloadTool來管理所有下載任務 
1.單例化工具類,初始化、懶載入相關資料。

+ (instancetype)shareInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _shareInstance = [[self alloc] init];
    });

    return _shareInstance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _shareInstance = [super allocWithZone:zone];
        if (![[NSFileManager defaultManager] fileExistsAtPath:QSPDownloadTool_DownloadDataDocument_Path]) {
            [[NSFileManager defaultManager] createDirectoryAtPath:QSPDownloadTool_DownloadDataDocument_Path withIntermediateDirectories:YES attributes:nil error:nil];
        }
    });

    return _shareInstance;
}

- (NSURLSession *)session
{
    if (_session == nil) {
        //可以上傳下載HTTP和HTTPS的後臺任務(程式在後臺執行)。 在後臺時,將網路傳輸交給系統的單獨的一個程序,即使app掛起、推出甚至崩潰照樣在後臺執行。
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"QSPDownload"];
        _session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    }

    return _session;
}
 
2.設計QSPDownloadToolDelegate協議來監控下載工具類任務的完成

@class QSPDownloadTool;
@protocol QSPDownloadToolDelegate <NSObject>

- (void)downloadToolDidFinish:(QSPDownloadTool *)tool downloadSource:(QSPDownloadSource *)source;

@end
 
3.新增一系列控制下載的方法

/**
 新增下載任務

 @param netPath 下載地址
 @param offLine 是否離線下載該任務
 @return 下載任務資料模型
 */
- (QSPDownloadSource *)addDownloadTast:(NSString *)netPath andOffLine:(BOOL)offLine;

/**
 新增代理

 @param delegate 代理物件
 */
- (void)addDownloadToolDelegate:(id<QSPDownloadToolDelegate>)delegate;
/**
 移除代理

 @param delegate 代理物件
 */
- (void)removeDownloadToolDelegate:(id<QSPDownloadToolDelegate>)delegate;

/**
 暫停下載任務

 @param source 下載任務資料模型
 */
- (void)suspendDownload:(QSPDownloadSource *)source;
/**
 暫停所有下載任務
 */
- (void)suspendAllTask;

/**
 繼續下載任務

 @param source 下載任務資料模型
 */
- (void)continueDownload:(QSPDownloadSource *)source;
/**
 開啟所有下載任務
 */
- (void)startAllTask;
/**
 停止下載任務

 @param source 下載任務資料模型
 */
- (void)stopDownload:(QSPDownloadSource *)source;
/**
 停止所有下載任務
 */
- (void)stopAllTask;
 
說明: 
- 所有的下載任務資料儲存在陣列downloadSources中,如果新增的是離線任務,我們需要儲存到本地。所以增加一個儲存下載任務資料的方法:

- (void)saveDownloadSource
{
    NSMutableArray *mArr = [[NSMutableArray alloc] initWithCapacity:1];
    for (QSPDownloadSource *souce in self.downloadSources) {
        if (souce.isOffLine) {
            NSData *data = [NSKeyedArchiver archivedDataWithRootObject:souce];
            [mArr addObject:data];
        }
    }

    [mArr writeToFile:QSPDownloadTool_DownloadSources_Path atomically:YES];
}
 
關於QSPDownloadTool工具類,我設計的是能夠新增多個代理物件,代理物件儲存於陣列中,陣列對內部的資料都是強引用,所以會造成迴圈引用,為了解決這個問題,我設計一個代理中間類QSPDownloadToolDelegateObject,陣列強引用代理中間類物件,代理中間類物件弱引用代理物件。
@interface QSPDownloadToolDelegateObject : NSObject

@property (weak, nonatomic) id<QSPDownloadToolDelegate> delegate;

@end
 
4.重中之重,實現NSURLSessionDataDelegate協議記錄相關資料

#pragma mark - NSURLSessionDataDelegate代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    NSLog(@"%s", __FUNCTION__);
    dispatch_async(dispatch_get_main_queue(), ^{
        for (QSPDownloadSource *source in self.downloadSources) {
            if (source.task == dataTask) {
                source.totalBytesExpectedToWrite = source.totalBytesWritten + response.expectedContentLength;
            }
        }
    });

    // 允許處理伺服器的響應,才會繼續接收伺服器返回的資料
    completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    dispatch_async(dispatch_get_main_queue(), ^{
        for (QSPDownloadSource *source in self.downloadSources) {
            if (source.task == dataTask) {
                [source.fileHandle seekToEndOfFile];
                [source.fileHandle writeData:data];
                source.totalBytesWritten += data.length;
                if ([source.delegate respondsToSelector:@selector(downloadSource:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)]) {
                    [source.delegate downloadSource:source didWriteData:data totalBytesWritten:source.totalBytesWritten totalBytesExpectedToWrite:source.totalBytesExpectedToWrite];
                }
            }
        }
    });
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    if (error) {
        NSLog(@"%@", error);
        NSLog(@"%@", error.userInfo);
    }

    dispatch_async(dispatch_get_main_queue(), ^{
        QSPDownloadSource *currentSource = nil;
        for (QSPDownloadSource *source in self.downloadSources) {
            if (source.fileHandle) {
                [source.fileHandle closeFile];
                source.fileHandle = nil;
            }

            if (error) {
                if (source.task == task && source.style == QSPDownloadSourceStyleDown) {
                    source.style = QSPDownloadSourceStyleFail;
                    if (error.code == -997) {
                        [self continueDownload:source];
                    }
                }
            }
            else
            {
                if (source.task == task) {
                    currentSource = source;
                    break;
                }
            }
        }

        if (currentSource) {
            currentSource.style = QSPDownloadSourceStyleFinished;
            [(NSMutableArray *)self.downloadSources removeObject:currentSource];
            [self saveDownloadSource];
            for (QSPDownloadToolDelegateObject *delegateObj in self.delegateArr) {
                if ([delegateObj.delegate respondsToSelector:@selector(downloadToolDidFinish:downloadSource:)]) {
                    [delegateObj.delegate downloadToolDidFinish:self downloadSource:currentSource];
                }
            }
        }
    });
}
 
說明: 
- NSURLSessionDataDelegate代理方法為非同步呼叫,為了避免多執行緒對資源的爭奪和能夠重新整理UI,把代理方法中的操作都切入主執行緒。 
- 在(void)URLSession:(NSURLSession )session dataTask:(NSURLSessionDataTask )dataTask didReceiveData:(NSData *)data這個代理方法中,把資料寫入磁碟,防止記憶體過高,並記錄相關資料儲存於QSPDownloadSource物件中,還有就是在這裡需要回調QSPDownloadSourceDelegate協議的代理方法。 
- 在(void)URLSession:(NSURLSession )session task:(NSURLSessionTask )task didCompleteWithError:(NSError *)error這個代理方法中,我們需要對錯誤進行處理,移除掉完成的任務,並回調QSPDownloadToolDelegate的代理方法。

三、問題與補充 
- 計算下載檔案的大小,程式碼中的QSPDownloadTool_Limit值為1024

/**
 按位元組計算檔案大小

 @param tytes 位元組數
 @return 檔案大小字串
 */
+ (NSString *)calculationDataWithBytes:(int64_t)tytes
{
    NSString *result;
    double length;
    if (tytes > QSPDownloadTool_Limit) {
        length = tytes/QSPDownloadTool_Limit;
        if (length > QSPDownloadTool_Limit) {
            length /= QSPDownloadTool_Limit;
            if (length > QSPDownloadTool_Limit) {
                length /= QSPDownloadTool_Limit;
                if (length > QSPDownloadTool_Limit) {
                    length /= QSPDownloadTool_Limit;
                    result = [NSString stringWithFormat:@"%.2fTB", length];
                }
                else
                {
                    result = [NSString stringWithFormat:@"%.2fGB", length];
                }
            }
            else
            {
                result = [NSString stringWithFormat:@"%.2fMB", length];
            }
        }
        else
        {
            result = [NSString stringWithFormat:@"%.2fKB", length];
        }
    }
    else
    {
        result = [NSString stringWithFormat:@"%lliB", tytes];
    }

    return result;
}
 
計算下載速率:思路是這樣的,在獲得下載資料的代理方法中記錄多次回撥的時間差和多次回撥獲得的資料大小總和,就這樣資料大小除以時間就為下載速率了,具體實現請看工程中的程式碼。
--------------------- 
作者:酒茶白開水 
來源:CSDN 
原文:https://blog.csdn.net/jiuchabaikaishui/article/details/68485743 
版權宣告:本文為博主原創文章,轉載請附上博文連結!