Ausgabe
Hallo zusammen, ich habe mich nur gefragt, wie ich einen seriellen Download mit NSURLSessionTask
der Reihenfolge machen kann? Was ich suche, ist, das erste Mal herunterzuladen, sobald es fertig ist, zum nächsten zu gehen, aber egal wie ich es versuche, es geht immer noch parallel und nicht in der Reihenfolge. Ich habe versucht DISPATCH_QUEUE_SERIAL
und dispatch_group_t
.
Die einzige Möglichkeit funktioniert so, aber das Problem ist, dass die Delegate-Methoden nicht aufgerufen werden, da der Completion-Handler aufgerufen wird, sodass ich den Benutzer nicht über den Fortschritt auf dem Laufenden halten kann. Eine weitere Sache ist, dass ich nicht verwenden kann, NSURLSessionDownloadTask
ich muss “DataTask” verwenden.
Hier ist der neueste Code, den ich ohne Ergebnis versucht habe
-(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];
});
}
Ich habe nur eine UIProgressView
und eine UILabel
, die ich während des Downloads jeder Datei aktualisieren muss. Danke im Voraus.
Lösung
Pro Chunk-Fortschritt
Sie können Ihre Operationen mit NSOperation
Instanzen umhüllen und Abhängigkeiten zwischen ihnen einrichten. Es ist besonders praktisch für Ihr Szenario, da NSOperationQueue
die NSProgress
Berichterstellung sofort einsatzbereit ist. Ich würde die Lösung immer noch in die folgende Schnittstelle packen (ein minimalistisches Beispiel, aber Sie können es nach Bedarf erweitern):
@interface TDWSerialDownloader : NSObject
@property(copy, readonly, nonatomic) NSArray<NSURL *> *urls;
@property(strong, readonly, nonatomic) NSProgress *progress;
- (instancetype)initWithURLArray:(NSArray<NSURL *> *)urls;
- (void)resume;
@end
Stellen Sie in der anonymen Kategorie der Klasse (Implementierungsdatei) sicher, dass Sie auch eine separate Eigenschaft zum Speichern haben NSOperationQueue
(sie wird später zum Abrufen der NSProgress
Instanz benötigt):
@interface TDWSerialDownloader()
@property(strong, readonly, nonatomic) NSOperationQueue *tasksQueue;
@property(copy, readwrite, nonatomic) NSArray<NSURL *> *urls;
@end
Erstellen Sie im Konstruktor die Warteschlange und erstellen Sie eine flache Kopie der bereitgestellten URLs ( NSURL
hat im Gegensatz zu kein veränderliches Gegenstück 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;
}
Vergessen Sie nicht, die progress
Eigenschaft der Warteschlange verfügbar zu machen, damit Views sie später verwenden können:
- (NSProgress *)progress {
return _tasksQueue.progress;
}
Now the centrepiece part. You actually don’t have control over which thread the NSURLSession
performs the requests in, it always happens asynchronously, thus you have to synchronise manually between the delegateQueue
of NSURLSession
(the queue callbacks are performed in) and the NSOperationQueue
inside of operations. I usually use semaphores for that, but of course there is more than one method for such a scenario. Also, if you add operations to the NSOperationQueue
, it will try to run them straight away, but you don’t want it, as first you need to set up dependencies between them. For this reason you should set suspended
property to YES
for until all operations are added and dependencies set up. Complete implementation of those ideas are inside of the resume
method:
- (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;
}
Be advised that no methods/properties of TDWSerialDownloader
are thread safe, so ensure you work with it from a single thread.
Here how use of this class looks like in the client code:
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
is an instance of UIProgressView
class here. You also want to keep a strong reference to the downloader
until all operations are finished (otherwise it may have the tasks queue prematurely deallocated).
Per Cent Progress
For the requirements you provided in the comments, i.e. per cent progress tracking when using NSURLSessionDataTask
only, you can’t rely on the NSOperationQueue
on its own (the progress
property of the class just tracks number of completed tasks). This is a much more complicated problem, which can be split into three high-level steps:
- Requesting length of the entire data from the server;
- Setting up
NSURLSessionDataDelegate
delegate; - Performing the data tasks sequentially and reporting obtained data progress to the UI;
Step 1
This step cannot be done if you don’t have control over the server implementation or if it doesn’t already support any way to inform the client about the entire data length. How exactly this is done is up to the protocol implementation, but commonly you either use a partial Range
or HEAD
request. In my example i’ll be using the HEAD
request:
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);
}];
As you can see all parts lengths need to be requested as a single NSOperation
. The http requests here don’t need to be performed in any particular order or even sequently, however the operation still needs to wait until all of them are done, so here is when dispatch_group
comes handy.
It’s also worth mentioning that NSProgress
is quite a complex object and it requires some minor synchronisation to avoid race condition. Also, since this implementation no longer can rely on built-in progress property of NSOperationQueue
, we’ll have to maintain our own instance of this object. With that in mind here is the property and its access methods implementation:
@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);
});
}
Where _progressAccessQueue
is a concurrent dispatch queue:
_progressAcessQueue = dispatch_queue_create("the.dreams.wind.queue.ProgressAcess", DISPATCH_QUEUE_CONCURRENT);
Step 2
Block-oriented API of NSURLSession
is convenient but not very flexible. It can only report response when the request is completely finished. In order to get more granular response, we can get use of NSURLSessionDataDelegate
protocol methods and set our own class as a delegate to the session instance:
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
_urlSession = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
In order to listen to the http requests progress inside of the delegate methods, we have to replace block-based methods with corresponding counterparts without them. I also set the timeout to 4 minutes, which is more reasonable for large chunks of data. Last but not least, the semaphore now needs to be used in multiple methods, so it has to turn into a property:
@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];
And finally we can implement the delegate methods like this:
- (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:
methods additionally checks for error scenarios, but it predominantly should just signal that the current request is finished via the semaphore. Another method accumulates received data and reports current progress.
Step 3
The last step is not really different from what we implemented for Per Chunk Progress implementation, but for sample data I decided to google for some big video-files this time:
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;
Mit all den ausgefallenen Dingen, die implementiert wurden, wurde dieses Beispiel etwas zu groß, um alle Besonderheiten als SO-Antwort abzudecken, also zögern Sie nicht, auf dieses Repo als Referenz zu verweisen.
Beantwortet von – The Dreams Wind
Antwort geprüft von – Pedro (FixError Volunteer)