2018年12月05日 16:09:00 weixin_34101784 閱讀數:5
https://blog.csdn.net/weixin_34101784/article/details/87569604
斷點續傳
demog.gif
斷點續傳的原理是在HTTP1.1協議(RFC2616)中定義了斷點續傳相關的HTTP頭的Range和Content-Range字段,支持只請求資源的一部分。
Range:可以請求文件資源的一個或者多個子范圍。
例如:
表示頭500個字節:bytes=0-499;
表示第二個500字節:bytes=500-999;
表示最后500個字節:bytes=-500;
表示500字節以后的范圍:bytes=500- ;
第一個和最后一個字節:bytes=0-0,-1;
同時指定幾個范圍:bytes=500-600,601-999;
Content-Range:字段說明服務器返回了文件的某個范圍及文件的總長度。這時Content-Length字段就不是整個文件的大小了,而是對應文件這個范圍的字節數,這一點一定要注意。一般格式,Content-Range: bytes 500-999/1000
NSURlSessionDownloadTask
iOS可以使用NSURlSessionDownloadTask來實現下載的斷點續傳功能,它提供了resumeData來實現斷點續傳功能,不需要在httpheader里設置Range了。網上關於NSURlSessionDownloadTask實現斷點續傳下載的代碼有很多,這里總結下自己遇到的問題。
下載暫停和恢復
NSURlSessionDownloadTask有兩種實現暫停的方法:
- suspend:直接調suspend方法可以使task暫停下載,恢復下載可以調用resume方法,;
- cancelByProducingResumeData::這個方法會取消task,從block里會得到一個resumeData。resumeData是用來恢復下載的。由於之前的task已經被取消了,方法downloadTaskWithResumeData:可以使用resumeData來獲取一個新的NSURlSessionDownloadTask來繼續下載。
在使用suspend時碰到的問題:
如果一個task正在下載,調用suspend暫停task后,然后com+R重啟應用,雖然在session的getTasksWithCompletionHandler:的block里能夠獲取到task,但是重新resume的時候有時候會失敗,有時候成功,不知道為什么。所以建議使用cancelByProducingResumeData來實現暫停功能。
把resumeData轉成字符串打印看一下:
-
<?xml version="1.0" encoding="UTF-8"?>
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-
<plist version="1.0">
-
<dict>
-
<key>NSURLSessionDownloadURL</key>
-
<string>http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.2.4.dmg</string>
-
<key>NSURLSessionResumeBytesReceived</key>
-
<integer>8297040</integer>
-
<key>NSURLSessionResumeCurrentRequest</key>
-
<data>
-
...
-
</data>
-
<key>NSURLSessionResumeInfoTempFileName</key>
-
<string>CFNetworkDownload_sFZ0E8.tmp</string>
-
<key>NSURLSessionResumeInfoVersion</key>
-
<integer>4</integer>
-
<key>NSURLSessionResumeOriginalRequest</key>
-
<data>
-
...
-
</data>
-
<key>NSURLSessionResumeServerDownloadDate</key>
-
<string>Thu, 12 May 2016 02:41:27 GMT</string>
-
</dict>
-
</plist>
可以看出,resumeData實質是一個plist文件,里面包含了一些下載的信息,NSURLSessionResumeBytesReceived對應的是已經下載的字節數,NSURLSessionResumeInfoTempFileName對應的是下載的臨時文件的名字。
如果下載出現錯誤,文檔里是這樣描述的
When any task completes, the NSURLSession object calls the delegate’s URLSession:task:didCompleteWithError: method with either an error object or nil (if the task completed successfully). If the download task can be resumed, the NSError object’s userInfo dictionary contains a value for the NSURLSessionDownloadTaskResumeData key. Your app should pass this value to call downloadTaskWithResumeData: or downloadTaskWithResumeData:completionHandler: to create a new download task that continues the existing download. If the task can’t be resumed, your app should create a new download task and restart the transaction from the beginning. In either case, if the transfer failed for any reason other than a server error, go to step 3 (creating and resuming task objects).
出錯時會調用URLSession:task:didCompleteWithError:獲得error對象,如果下載是可以恢復的,可以使用error.userInfo[NSURLSessionDownloadTaskResumeData]來獲取resumeData恢復下載。有兩種情況獲取resumeData:
- task調用cancelByProducingResumeData:之后會調用URLSession:task:didCompleteWithError:來獲取resumeData,
- 還有user主動kill應用后系統會取消下載中的task,重新啟動時創建session后也可以在URLSession:task:didCompleteWithError:里獲取到resumeData。
NSURLSessionDownloadTask在后台下載
NSURLSessionDownloadTask是支持后台下載的。
downloading_files_in_the_background
Downloading Content in the Background
When downloading files, apps should use an NSURLSession object to start the downloads so that the system can take control of the download process in case the app is suspended or terminated. When you configure an NSURLSession object for background transfers, the system manages those transfers in a separate process and reports status back to your app in the usual way. If your app is terminated while transfers are ongoing, the system continues the transfers in the background and launches your app (as appropriate) when the transfers finish or when one or more tasks need your app’s attention.
Once configured, your NSURLSession object seamlessly hands off upload and download tasks to the system at appropriate times. If tasks finish while your app is still running (either in the foreground or the background), the session object notifies its delegate in the usual way. If tasks have not yet finished and the system terminates your app, the system automatically continues managing the tasks in the background. If the user terminates your app, the system cancels any pending tasks.
文檔里提到了app的幾種情況:
- 如果app在后台,但是is still running(比如按home鍵把app切到后台),會正常調用session的代理方法;
- 如果app被系統terminate了(比如app在后台時間過長可能會被系統強制殺掉),系統會繼續在后台管理session的task,當task完成時系統會啟動app,此時只要使用相同的id創建session就會回調代理方法了;
- 如果使用戶主動kill了app,系統會取消session的task,重新啟動時,用相同的id創建session,會調用代理方法URLSession:task:didCompleteWithError:,可以獲取到resumeData來恢復下載。
在后台下載完成時的處理
下載任務在后台完成后:
如果app在后台,但是沒有被系統teminate,系統會resume 應用並且調用UIApplicationDelegate的代理方法application:handleEventsForBackgroundURLSession:completionHandler:。之后會調用session的代理方法。
如果app在后台時候被系統terminate了,當下載task完成時,系統會在后台重啟應用並調用UIApplicationDelegate的代理方法application:handleEventsForBackgroundURLSession:completionHandler:。方法里獲得的identifier是之前創建session時的identifier。需要用這個idetifier重新創建session,之后會調用session的代理方法。
調用這個方法獲得的completionHandler可以讓系統知道您的應用程序的用戶界面已更新,並且可以拍攝新的快照。一般在session的代理方法URLSessionDidFinishEventsForBackgroundURLSession:中調用。
在下載過程中用戶主動kill app
用戶主動kill app會導致下載task取消,當應用再次啟動時,需要使用之前創建session的identifier重新創建session,之后會調用URLSession: task: didCompleteWithError:方法,從error中可以獲取resumeData來恢復下載。
系統終止了app,重啟時獲取下載中的task
比如應用退到后台,因為內存問題被系統teminate,這時候下載任務不會取消,系統會繼續管理下載task,此時若重新打開應用,可以使用相同的identifier創建session,然后通過
-
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
-
-
}];
這個方法來獲取task。如果在模擬器上正在進行下載任務,然后com+R重新運行程序也可以使用這個方法獲取之前創建的下載task。
如果task在下載過程中用戶主動kill app,task會被取消,再重新啟動應用時。上面的這個方法會獲取到取消的task,它的state是NSURLSessionTaskStateCompleted,不能使用這個task繼續請求了,可以從session的代理方法里獲取resumeData重新創建task來繼續請求。
iOS 大文件下載、斷點續傳、后台下載 —— HERO博客
https://blog.csdn.net/hero_wqb/article/details/80407478
2018年05月23日 14:40:02 hero_wqb 閱讀數:6681
版權聲明:轉載請注明出處。 https://blog.csdn.net/hero_wqb/article/details/80407478
本篇簡述一下實現文件下載功能,包含大文件下載,后台下載,殺死進程,重新啟動時繼續下載,設置下載並發數,監聽網絡改變等,並在最后附有Demo。
下載功能的實現:
使用的網絡連接的類為NSURLSession。該類用以替代NSURLConnection,在iOS7時推出,至此iOS系統才有了后台傳輸。在初始化NSURLSession前,需要先創建NSURLSessionConfiguration,可以理解為是NSURLSession需要的一個配置。NSURLSessionConfiguration有三種模式:
1. default:可以使用緩存的Cache、Cookie、鑒權。
2. ephemeral,僅內存緩存,不使用緩存的Cache、Cookie、鑒權。
3. background,支持后台傳輸,需要一個identifier標識,用來重新連接session對象。
創建后台模式NSURLSessionConfiguration:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"HWDownloadBackgroundSessionIdentifier"];
創建NSURLSession,設置配信息、代理、代理線程:
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
在實現下載前,還需要了解一個很重要的類,NSURLSessionTask,無論下載多少文件,我們只需要初始化一個NSURLSession即可,而每個task對應一個任務,需要通過task才能實現下載,NSURLSessionTask是一個基類,有四個子類:
1. NSURLSessionDataTask:下載時,內容以NSData對象返回,需要我們不斷寫入文件,但不支持后台傳輸,切換后台會終止下載,回到前台時在協議方法中輸出error,下面貼一下用NSURLSessionDataTask實現斷點續傳的核心代碼:
-
// 遵守協議
-
<NSURLSessionDataDelegate>
-
// 創建NSMutableURLRequest
-
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
-
[request setValue:[NSString stringWithFormat:@"bytes=%zd-", tmpFileSize] forHTTPHeaderField:@"Range"];
-
// 創建NSURLSessionDataTask
-
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
-
// 開始、繼續下載
-
[dataTask resume];
-
// 暫停下載
-
[dataTask suspend];
-
// 取消下載
-
[dataTask cancel];
-
-
// 接收到服務器響應
-
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
-
{
-
// 更新文件的總大小
-
totalFileSize = response.expectedContentLength + tmpFileSize;
-
-
// 創建輸出流
-
NSOutputStream *stream = [[NSOutputStream alloc] initToFileAtPath:fullPath append:YES];
-
[stream open];
-
-
// 允許處理服務器的響應,繼續接收數據
-
completionHandler(NSURLSessionResponseAllow);
-
}
-
-
// 接收到服務器返回數據,會被調用多次
-
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
-
{
-
// 寫入數據
-
[stream write:data.bytes maxLength:data.length];
-
-
// 當前下載大小
-
tmpFileSize += data.length;
-
-
// 進度
-
self.progressView.progress = 1.0 * tmpFileSize / totalFileSize;
-
}
-
-
// 當請求完成之后調用,如果錯誤,那么error有值
-
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
-
{
-
[stream close];
-
}
-
-
- (void)dealloc
-
{
-
[session invalidateAndCancel];
-
}
有幾點需要注意,調用cancel方法會立即進入-URLSession: task: didCompleteWithError這個回調;調用suspend方法,即使任務已經暫停,但達到超時時長,也會進入這個回調,可以通過error進行判斷;當一個任務調用了resume方法,但還未開始接受數據,這時調用suspend方法是無效的。也可以通過cancel方法實現暫停,只是每次需要重新創建NSURLSessionDataTask。
2. NSURLSessionUploadTask:繼承自NSURLSessionDataTask,內容以NSData對象返回,協議方法中可以查看請求時上傳內容的過程,支持后台傳輸。
3. NSURLSessionStreamTask:建立了一個TCP/IP連接,替代NSInputStream/NSOutputStream,新的API可異步讀寫,自動通過HTTP代理連接遠程服務器。
4. NSURLSessionDownloadTask:筆者推薦使用該task實現文件下載,斷點續傳系統幫我們做了,資源會下載到一個臨時文件,下載完成需將文件移動至想要的路徑,系統會刪除臨時路勁文件,暫停時,系統會返回NSData對象,恢復下載時用這個data創建task,支持后台傳輸,下面重點介紹一下NSURLSessionDownloadTask的使用:
創建NSURLSessionDownloadTask,有兩種方式,后面會講解NSData在哪里獲取,其中需要注意一點,在iOS 10.0和iOS 10.1系統中,使用downloadTaskWithResumeData:會發生數據錯誤問題,需要進行額外處理,具體可以在Demo中查看:
-
// 根據NSData對象創建,可以繼續上次進度下載
-
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithResumeData:resumeData];
-
-
// 根據NSURLRequesta對象創建,開啟新的下載
-
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:model.url]]];
開始、繼續下載用NSURLSessionTask的resume方法,暫停下載用下面方法,這里拿到回調的NSData,保存,可以通過它來創建task實現繼續下載:
-
[downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
-
model.resumeData = resumeData;
-
}];
遵守協議,實現相應協議方法:
NSURLSessionDownloadDelegate:
-
/**
-
接收到服務器返回數據,會被調用多次,可獲取文件大小,進度,計算速度等
-
-
@param bytesWritten 當次寫入文件大小
-
@param totalBytesWritten 已寫入文件大小
-
@param totalBytesExpectedToWrite 文件總大小
-
*/
-
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
-
{
-
// 計算進度
-
model.progress = 1.0 * totalBytesWritten / totalBytesExpectedToWrite;
-
}
-
-
// 下載完成
-
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
-
{
-
// 移動文件,原路徑文件由系統自動刪除
-
[[NSFileManager defaultManager] moveItemAtPath:[location path] toPath:localPath error:nil];
-
}
NSURLSessionTaskDelegate,注意調用cancel、cancelByProducingResumeData:方法也會調用:
-
// 請求完成,有錯誤時,error有值
-
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;
后台下載:
到這里,已經可以通過NSURLSessionDownloadTask實現斷點續傳了,下面介紹如何實現后台下載,其實非常簡單,一共三步:
1. 創建NSURLSession時,需要創建后台模式NSURLSessionConfiguration,上面已經介紹過了。
2. 在AppDelegate中實現下面方法,並定義變量保存completionHandler代碼塊:
-
// 應用處於后台,所有下載任務完成調用
-
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
-
{
-
_backgroundSessionCompletionHandler = completionHandler;
-
}
3. 在下載類中實現下面NSURLSessionDelegate協議方法,其實就是先執行完task的協議,保存數據、刷新界面之后再執行在AppDelegate中保存的代碼塊:
-
// 應用處於后台,所有下載任務完成及NSURLSession協議調用之后調用
-
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
-
{
-
dispatch_async(dispatch_get_main_queue(), ^{
-
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
-
if (appDelegate.backgroundSessionCompletionHandler) {
-
void (^completionHandler)(void) = appDelegate.backgroundSessionCompletionHandler;
-
appDelegate.backgroundSessionCompletionHandler = nil;
-
-
// 執行block,系統后台生成快照,釋放阻止應用掛起的斷言
-
completionHandler();
-
}
-
});
-
}
程序終止,再次啟動繼續下載:
后台下載實現之后,再看一下如何實現進程殺死后,再次啟動時繼續下載,在應用程序被殺掉時,系統會自動保存應用下載session信息,重新啟動應用時,如果創建和之前相同identifier的session,系統會找到對應的session數據,並響應-URLSession: task: didCompleteWithError:方法,打印error輸出如下:
error: Error Domain=NSURLErrorDomain Code=-999 "(null)" UserInfo={NSURLErrorBackgroundTaskCancelledReasonKey=0, NSErrorFailingURLStringKey=https://www.apple.com/105/media/cn/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/films/feature/iphone-x-feature-cn-20170912_1280x720h.mp4, NSErrorFailingURLKey=https://www.apple.com/105/media/cn/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/films/feature/iphone-x-feature-cn-20170912_1280x720h.mp4, NSURLSessionDownloadTaskResumeData=<CFData 0x7ff401097c00 [0x104cb6bb0]>{length = 6176, capacity = 6176, bytes = 0x3c3f786d6c2076657273696f6e3d2231 ... 2f706c6973743e0a}}
可以看到,有幾點有用的信息:
1)error.localizedDescription為"(null)",打印結果為"The operation couldn't be completed. (NSURLErrorDomain error -999.)"。
2)[error.userInfo objectForkey:NSURLErrorBackgroundTaskCancelledReasonKey]有值。
3)返回了NSURLSessionDownloadTaskResumeData。
綜上進程殺死后,再次啟動繼續下載的思路就是,重啟時,創建相同identifier的session,在-URLSession: task: didCompleteWithError:方法中拿到resumeData,用resumeData創建task,就可以恢復下載。
再說明一下,另外一種不可取的思路,在appDelegate中進程殺死時會調用-applicationWillTerminate:方法,在這里task調用cancelByProducingResumeData:方法暫停正在下載的任務,但是這個方法的回調需要時間,還沒有執行到代碼塊進程就已經終止了。
並發數設置:
下面介紹一下下載並發數的設置:NSURLSession本身就支持多任務同時下載,它會根據性能內部控制同時下載的個數,最多5個。一個任務對應一個NSURLSessionDownloadTask,所以想多任務同時下載,需要創建多個task,可以用數組或字典保存。我們定義變量去記錄當前下載文件個數及用戶設置的最大下載個數。
監聽網絡改變:用AFN監聽,可以點擊這里查看
為了增加用戶體驗,往往在設置中會給用戶一個選項, 選擇蜂窩網絡下是否允許下載。NSURLSessionConfiguration本身就有一個屬性allowsCellularAccess,默認為YES,允許蜂窩網絡下載。如果不需要用戶隨時變更這個選項,是可以用這個屬性。但是對於正在下載的任務,修改這個屬性是無效的,即我們已經通過session創建了task對象,開啟了任務,再試圖用session.configuration.allowsCellularAccess = NO;去修改這個選項是無效的。如果一定要用這個屬性修改這個選項,那么只能重新創建session:
-
// 重新創建后台NSURLSessionConfiguration,並且identifier需要改變,不能與之前一樣
-
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"HWDownloadBackgroundSessionIdentifierNew"];
-
// 修改是否允許蜂窩網絡下載
-
configuration.allowsCellularAccess = NO;
-
// 重新創建NSURLSession
-
_session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
所以我們創建NSURLSessionConfiguration時把allowsCellularAccess設為YES,然后定義一個變量去控制是否允許蜂窩網絡下載,在網絡狀態改變及用戶設置修改這個選項之后,調用暫停、開啟任務。
數據保存:用FMDB存儲數據,可以點擊這里查看
下載速度計算:
聲明兩個變量,一個記錄時間,一個記錄在特定時間內接收到的數據大小,在接收服務器返回數據的-URLSession: downloadTask: didWriteData: totalBytesWritten: totalBytesExpectedToWrite:方法中,統計接收到數據的大小,達到時間限定時,計算速度=數據/時間,然后清空變量,為方便數據庫存儲,這里用的時間戳:
-
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
-
{
-
// 記錄在特定時間內接收到的數據大小
-
model.intervalFileSize += bytesWritten;
-
-
// 獲取上次計算時間與當前時間間隔
-
NSInteger intervals = [[NSDate date] timeIntervalSinceDate:[NSDate dateWithTimeIntervalSince1970:model.lastSpeedTime * 0.001 * 0.001]];
-
if (intervals >= 1) {
-
// 計算速度
-
model.speed = model.intervalFileSize / intervals;
-
-
// 重置變量
-
model.intervalFileSize = 0;
-
model.lastSpeedTime = [[NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970] * 1000 * 1000] integerValue];
-
}
-
}
Demo效果圖:
這里在模型中加入了一個變量,記錄任務加入准備下載的時間,用於計算任務開始的先后順序,如上圖3,開啟任務08、09、10、11、12,暫停,依次開啟10、11、12、08、09,然后將最大並發數由5改為2,暫停的應該為12、08、09三個任務,當10下載完成,開啟的應該是12而不是08。
Demo下載鏈接:https://github.com/HeroWqb/HWDownloadDemo
寫博客的初心是希望大家共同交流成長,博主水平有限難免有偏頗之處,歡迎批評指正。