[FIXED] Mehrere NSURLSessionDataTask nacheinander ausführen und deren Fortschritt verfolgen

Ausgabe

Hallo zusammen, ich habe mich nur gefragt, wie ich einen seriellen Download mit NSURLSessionTaskder 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_SERIALund 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, NSURLSessionDownloadTaskich 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 UIProgressViewund 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 NSOperationInstanzen umhüllen und Abhängigkeiten zwischen ihnen einrichten. Es ist besonders praktisch für Ihr Szenario, da NSOperationQueuedie NSProgressBerichterstellung 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 NSProgressInstanz 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 ( NSURLhat 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 progressEigenschaft 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:

  1. Requesting length of the entire data from the server;
  2. Setting up NSURLSessionDataDelegate delegate;
  3. 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)

0 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like