大家好,我只是想知道如何NSURLSessionTask按順序進行串行下載?我正在尋找的是第一次下載完成后轉到下一個,但無論我如何嘗試它仍然并行而不是按順序。我試過DISPATCH_QUEUE_SERIAL和dispatch_group_t.
唯一可行的方法是這樣,但問題是它不呼叫委托方法,因為它呼叫了完成處理程式,所以我無法向用戶更新進度。還有一件事是我不能使用NSURLSessionDownloadTask我必須使用 "DataTask" 。
這是我嘗試的最新代碼,但沒有結果
-(void)download1{
self.task1 = [ self.session dataTaskWithURL:[NSURL URLWithString:@"https://example.com/file.zip"]];
[self.task1 resume];
}
-(void)download2 {
self.task2 = [self.session dataTaskWithURL:[NSURL URLWithString:@"https://example.com/file.z01"]];
}
-(void)download3 {
self.task3 = [self.session dataTaskWithURL:[NSURL URLWithString:@"https://example.com/file.z02"]];
}
-(void)download:(id)sender {
[self testInternetConnection];
dispatch_queue_t serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
[self download1];
});
dispatch_sync(serialQueue, ^{
[self download2];
[self.task2 resume];
});
dispatch_sync(serialQueue, ^{
[self download3];
[self.task3 resume];
});
}
我只有一個UIProgressView, 和一個UILabel在下載每個檔案的程序中更新。提前致謝。
uj5u.com熱心網友回復:
每塊進度
您可以使用NSOperation實體包裝您的操作并設定它們之間的依賴關系。它對您的場景非常方便,因為它NSOperationQueue支持NSProgress開箱即用的報告。我仍然會將解決方案包裝在以下界面中(一個簡約的示例,但您可以根據需要對其進行擴展):
@interface TDWSerialDownloader : NSObject
@property(copy, readonly, nonatomic) NSArray<NSURL *> *urls;
@property(strong, readonly, nonatomic) NSProgress *progress;
- (instancetype)initWithURLArray:(NSArray<NSURL *> *)urls;
- (void)resume;
@end
在類的匿名類別(實作檔案)中,確保您還有一個單獨的屬性要存盤NSOperationQueue(稍后需要檢索NSProgress實體):
@interface TDWSerialDownloader()
@property(strong, readonly, nonatomic) NSOperationQueue *tasksQueue;
@property(copy, readwrite, nonatomic) NSArray<NSURL *> *urls;
@end
在建構式中創建佇列并制作提供的 url 的淺表副本(NSURL沒有可變副本,不像NSArray):
- (instancetype)initWithURLArray:(NSArray<NSURL *> *)urls {
if (self = [super init]) {
_urls = [[NSArray alloc] initWithArray:urls copyItems:NO];
NSOperationQueue *queue = [NSOperationQueue new];
queue.name = @"the.dreams.wind.SerialDownloaderQueue";
queue.maxConcurrentOperationCount = 1;
_tasksQueue = queue;
}
return self;
}
不要忘記公開progress佇列的屬性,以便以后視圖可以使用它:
- (NSProgress *)progress {
return _tasksQueue.progress;
}
現在是核心部分。您實際上無法控制在哪個執行緒中NSURLSession執行請求,它總是異步發生,因此您必須在delegateQueueof NSURLSession(執行佇列回呼)和NSOperationQueue內部操作之間手動同步。我通常為此使用信號量,但是對于這種情況當然有不止一種方法。此外,如果您向 中添加操作NSOperationQueue,它會嘗試立即運行它們,但您不想要它,因為首先您需要設定它們之間的依賴關系。出于這個原因,您應該將suspended屬性設定為YESfor,直到添加了所有操作并設定了依賴項。這些想法的完整實作都在resume方法內部:
- (void)resume {
NSURLSession *session = NSURLSession.sharedSession;
// Prevents queue from starting the download straight away
_tasksQueue.suspended = YES;
NSOperation *lastOperation;
for (NSURL *url in _urls.reverseObjectEnumerator) {
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@ started", url);
__block dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@ was downloaded", url);
// read data here if needed
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// 4 minutes timeout
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 60 * 4));
NSLog(@"%@ finished", url);
}];
if (lastOperation) {
[lastOperation addDependency:operation];
}
lastOperation = operation;
[_tasksQueue addOperation:operation];
}
_tasksQueue.progress.totalUnitCount = _tasksQueue.operationCount;
_tasksQueue.suspended = NO;
}
請注意,沒有任何方法/屬性TDWSerialDownloader是執行緒安全的,因此請確保您從單個執行緒中使用它。
在客戶端代碼中如何使用此類:
TDWSerialDownloader *downloader = [[TDWSerialDownloader alloc] initWithURLArray:@[
[[NSURL alloc] initWithString:@"https://google.com"],
[[NSURL alloc] initWithString:@"https://stackoverflow.com/"],
[[NSURL alloc] initWithString:@"https://developer.apple.com/"]
]];
_mProgressView.observedProgress = downloader.progress;
[downloader resume];
_mProgressView這里是UIProgressView類的一個實體。您還希望保持對 的強參考,downloader直到所有操作完成(否則可能會提前釋放任務佇列)。
百分比進度
對于您在評論中提供的要求,即NSURLSessionDataTask僅使用時的百分比進度跟蹤,您不能依賴NSOperationQueue它自己(progress該類的屬性僅跟蹤已完成任務的數量)。這是一個復雜得多的問題,可以分為三個高級步驟:
- 向服務器請求整個資料的長度;
- 設立
NSURLSessionDataDelegate代表; - 依次執行資料任務,并將獲取的資料進度報告給 UI;
步驟1
如果您無法控制服務器實作,或者它不支持以任何方式通知客戶端整個資料長度,則無法執行此步驟。具體如何完成取決于協議實作,但通常您使用部分Range或HEAD請求。在我的示例中,我將使用HEAD請求:
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
if (!weakSelf) {
return;
}
typeof(weakSelf) __strong strongSelf = weakSelf;
[strongSelf p_changeProgressSynchronised:^(NSProgress *progress) {
progress.totalUnitCount = 0;
}];
__block dispatch_group_t lengthRequestsGroup = dispatch_group_create();
for (NSURL *url in strongSelf.urls) {
dispatch_group_enter(lengthRequestsGroup);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"HEAD";
typeof(self) __weak weakSelf = strongSelf;
NSURLSessionDataTask *task = [strongSelf->_urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse
*_Nullable response, NSError *_Nullable error) {
if (!weakSelf) {
return;
}
typeof(weakSelf) __strong strongSelf = weakSelf;
[strongSelf p_changeProgressSynchronised:^(NSProgress *progress) {
progress.totalUnitCount = response.expectedContentLength;
dispatch_group_leave(lengthRequestsGroup);
}];
}];
[task resume];
}
dispatch_group_wait(lengthRequestsGroup, DISPATCH_TIME_FOREVER);
}];
如您所見,所有零件長度都需要作為單個NSOperation. 這里的 http 請求不需要以任何特定的順序甚至順序執行,但是操作仍然需要等到所有這些都完成,所以這時候dispatch_group就派上用場了。
還值得一提的是,這NSProgress是一個相當復雜的物件,它需要一些小的同步來避免競爭條件。此外,由于這個實作不再依賴于 的內置進度屬性NSOperationQueue,我們必須維護我們自己的這個物件的實體。考慮到這一點,這里是屬性及其訪問方法的實作:
@property(strong, readonly, nonatomic) NSProgress *progress;
...
- (NSProgress *)progress {
__block NSProgress *localProgress;
dispatch_sync(_progressAcessQueue, ^{
localProgress = _progress;
});
return localProgress;
}
- (void)p_changeProgressSynchronised:(void (^)(NSProgress *))progressChangeBlock {
typeof(self) __weak weakSelf = self;
dispatch_barrier_async(_progressAcessQueue, ^{
if (!weakSelf) {
return;
}
typeof(weakSelf) __strong strongSelf = weakSelf;
progressChangeBlock(strongSelf->_progress);
});
}
_progressAccessQueue并發調度佇列在哪里:
_progressAcessQueue = dispatch_queue_create("the.dreams.wind.queue.ProgressAcess", DISPATCH_QUEUE_CONCURRENT);
第2步
面向塊的 APINSURLSession很方便,但不是很靈活。它只能在請求完全完成時報告回應。為了獲得更精細的回應,我們可以使用NSURLSessionDataDelegate協議方法并將我們自己的類設定為會話實體的委托:
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
_urlSession = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
為了在委托方法中監聽 http 請求的進度,我們必須將基于塊的方法替換為沒有它們的對應對應物。我還將超時設定為 4 分鐘,這對于大塊資料更合理。最后但同樣重要的是,信號量現在需要在多個方法中使用,所以它必須變成一個屬性:
@property(strong, nonatomic) dispatch_semaphore_t taskSemaphore;
...
strongSelf.taskSemaphore = dispatch_semaphore_create(0);
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:kRequestTimeout];
[[session dataTaskWithRequest:request] resume];
最后我們可以像這樣實作委托方法:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
[self cancel];
// 3.2 Failed completion
_callback([_data copy], error);
}
dispatch_semaphore_signal(_taskSemaphore);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[_data appendData:data];
[self p_changeProgressSynchronised:^(NSProgress *progress) {
progress.completedUnitCount = data.length;
}];
}
URLSession:task:didCompleteWithError:方法還檢查錯誤場景,但它主要應該只是通過信號量發出當前請求已完成的信號。另一種方法累積接收到的資料并報告當前進度。
第 3 步
最后一步與我們為Per Chunk Progress實作的實作并沒有什么不同,但是對于示例資料,我決定這次谷歌搜索一些大的視頻檔案:
typeof(self) __weak weakSelf = self;
TDWSerialDataTaskSequence *dataTaskSequence = [[TDWSerialDataTaskSequence alloc] initWithURLArray:@[
[[NSURL alloc] initWithString:@"https://download.samplelib.com/mp4/sample-5s.mp4"],
// [[NSURL alloc] initWithString:@"https://error.url/sample-20s.mp4"], // uncomment to check error scenario
[[NSURL alloc] initWithString:@"https://download.samplelib.com/mp4/sample-30s.mp4"],
[[NSURL alloc] initWithString:@"https://download.samplelib.com/mp4/sample-20s.mp4"]
] callback:^(NSData * _Nonnull data, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!weakSelf) {
return;
}
typeof(weakSelf) __strong strongSelf = weakSelf;
if (error) {
strongSelf->_dataLabel.text = error.localizedDescription;
} else {
strongSelf->_dataLabel.text = [NSString stringWithFormat:@"Data length loaded: %lu", data.length];
}
});
}];
_progressView.observedProgress = dataTaskSequence.progress;
實作了所有花哨的東西后,這個示例有點太大了,無法涵蓋所有??特性作為 SO 答案,所以請隨時參考這個 repo以供參考。
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/508281.html
標籤:目标-c 下载 nsurlsession
