Pytanie Najważniejsze wskazówki dotyczące tworzenia kolejki zadań NSURLSessionTask


Jakie są najlepsze praktyki dotyczące tworzenia kolejki szeregowej NSURLSessionTasks ?

W moim przypadku muszę:

  1. Pobierz adres URL do pliku JSON (NSURLSessionDataTask)
  2. Pobierz plik pod tym adresem URL (NSURLSessionDownloadTask)

Oto, co mam do tej pory:

session = [NSURLSession sharedSession];

//Download the JSON:
NSURLRequest *dataRequest = [NSURLRequest requestWithURL:url];

NSURLSessionDataTask *task =
[session dataTaskWithRequest:dataRequest
           completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

               //Figure out the URL of the file I want to download:
               NSJSONSerialization *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
               NSURL *downloadURL = [NSURL urlWithString:[json objectForKey:@"download_url"]];

               NSURLSessionDownloadTask *fileDownloadTask =
               [session downloadTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:playlistURL]]
                              completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
                                  NSLog(@"completed!");
                              }];

               [fileDownloadTask resume];

           }
 ];

Pomijając fakt, że napisanie bloku ukończenia w ramach innego zakończenia wygląda nieładnie, otrzymuję błąd EXC_BAD_ACCESS, gdy wołam [fileDownloadTask resume]... Nawet jeśli fileDownloadTask nie jest zero!

Jaki jest najlepszy sposób sekwencjonowania? NSURLSessionTasks?


21
2017-12-16 18:59


pochodzenie


Do sekwencjonowania, a co z NSOperationQueue? - Owen Hartnett


Odpowiedzi:


Musisz użyć tego podejścia, które jest najprostsze: https://stackoverflow.com/a/31386206/2308258

Lub użyj kolejki operacji i uzależnij zadania od siebie nawzajem

================================================== =====================

Dotyczy metody HTTPMaximumConnectionsPerHost

Łatwym sposobem implementacji pierwszej w kolejności pierwszej kolejki szeregowej NSURLSessionTasks jest uruchomienie wszystkich zadań w NSURLSession, która ma właściwość HTTPMaximumConnectionsPerHost ustawioną na 1

HTTPMaximumConnectionsPerHost tylko to zapewnia udostępniony połączenie zostanie użyte do zadań tej sesji, ale nie oznacza to, że będą przetwarzane seryjnie.

Możesz to sprawdzić na poziomie sieci za pomocą http://www.charlesproxy.com/Odkryjesz, że podczas ustawiania HTTPMaximumConnectionsPerHost Twoje zadania będą nadal uruchamiane jednocześnie przez NSURLSession, a nie seryjnie.

Expriment 1:

  • Deklarowanie NSURLSession za pomocą HTTPMaximumConnectionsPerHost na 1
  • Z task1: url = download.thinkbroadband.com/20MB.zip
  • Z task2: url = download.thinkbroadband.com/20MB.zip

    1. wywołanie [wznowienie zadania1];
    2. wywoływanie [wznowienie zadania 2];

Wynik: task1 completionBlock jest wywoływane, a następnie wywoływane jest task2 completionBlock

Bloki ukończenia mogą być wywoływane w kolejności, w jakiej oczekiwałeś, gdyby zadania trwały tyle samo czasu jednak jeśli spróbujesz pobrać dwie różne rzeczy za pomocą tej samej NSURLSession, odkryjesz, że NSURLSession nie ma żadnego bazowego uporządkowania twoich połączeń, ale kończy tylko to, co zostało zakończone jako pierwsze.

Expriment 2:

  • Deklarowanie NSURLSession za pomocą HTTPMaximumConnectionsPerHost na 1
  • task1: url = download.thinkbroadband.com/20MB.zip
  • task2: url = download.thinkbroadband.com/10MB.zip (mniejszy plik)

    1. wywołanie [wznowienie zadania1];
    2. wywoływanie [wznowienie zadania 2];

Wynik: task2 completionBlock jest wywoływane, a następnie wywoływane jest task1 completionBlock

Podsumowując, należy samemu dokonać zamówienia, NSURLSession nie ma żadnej logiki dotyczącej zamawiania żądań, po prostu wywoła metodę completionBlock tego, co kończy się na pierwszym miejscu, nawet przy ustawianiu maksymalnej liczby połączeń na hosta na 1

PS: Przepraszam za format postu, nie mam wystarczającej reputacji, by publikować zrzuty ekranu.


16
2017-09-18 15:20





Edytować:

Jak zauważył mataejoon, ustawienie HTTPMaximumConnectionsPerHost do 1 nie gwarantują, że połączenia są przetwarzane seryjnie. Spróbuj innego podejścia (jak w mojej oryginalnej odpowiedzi poniżej), jeśli potrzebujesz niezawodnej kolejki szeregowej NSURLSessionTask.


Łatwy sposób na implementację pierwszej w kolejności pierwszej kolejki NSURLSessionTasks jest uruchamianie wszystkich zadań na NSURLSession to ma swoje HTTPMaximumConnectionsPerHost właściwość ustawiona na 1:

+ (NSURLSession *)session
{
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

        [configuration setHTTPMaximumConnectionsPerHost:1];

        session = [NSURLSession sessionWithConfiguration:configuration];

    });
    return session;
}

następnie dodaj do niego zadania w wybranej kolejności.

NSURLSessionDataTask *sizeTask = 
[[[self class] session] dataTaskWithURL:url 
                          completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

12
2018-01-09 11:29



Powinniśmy przesłonić session: metoda? Też natknąłem się na ten sam problem, wszelkie nowe podejścia? - David Liu
@DavidLiu: Nie przeważasz nad session: metoda; tworzysz go od zera, nie jest częścią NSURLSession API. - Eric
masz na myśli dodać tę metodę klasy do NSURLSession przez nową kategorię? - David Liu
Mam na myśli dodanie tej metody klasy do którejkolwiek klasy, która jest odpowiedzialna za tworzenie sieci. Alternatywnie może również działać jako kategoria NSURLSession. - Eric
Zobacz odpowiedź dotyczącą HTTPMaximumConnectionsPerHost poniżej - Max MacLeod


Używam NSOperationQueue (jak zasugerował Owen). Umieść NSURLSessionTasks w podklasach NSOperation i ustaw dowolne zależności. Zależne zadania będą czekać, aż zadanie, na które są zależne, zostanie ukończone przed uruchomieniem, ale nie sprawdzi stanu (powodzenia lub niepowodzenia), więc dodaj trochę logiki, aby kontrolować proces.

W moim przypadku pierwsze zadanie polega na sprawdzeniu, czy użytkownik ma ważne konto i jeśli to konieczne, jego utworzenie. W pierwszym zadaniu aktualizuję wartość NSUserDefault, aby wskazać, że konto jest prawidłowe (lub wystąpił błąd). Drugie zadanie sprawdza wartość NSUserDefault i jeśli wszystko jest w porządku, korzysta z poświadczeń użytkownika, aby opublikować niektóre dane na serwerze.

(Trzymanie NSURLSessionTasks w oddzielnych podklasach NSOperation również ułatwiło nawigację mojego kodu)

Dodaj podklasy NSOperation do NSOperationQueue i ustaw dowolne zależności:

 NSOperationQueue  *ftfQueue = [NSOperationQueue new];
 FTFCreateAccount *createFTFAccount = [[FTFCreateAccount alloc]init];
 [createFTFAccount setUserid:@"********"];  // Userid to be checked / created
 [ftfQueue addOperation:createFTFAccount];
 FTFPostRoute *postFTFRoute = [[FTFPostRoute alloc]init];
 [postFTFRoute addDependency:createFTFAccount];
 [ftfQueue addOperation:postFTFRoute];

W pierwszej podklasie NSOperation sprawdza, czy konto istnieje na serwerze:

@implementation FTFCreateAccount
{
    NSString *_accountCreationStatus;
}



- (void)main {

    NSDate *startDate = [[NSDate alloc] init];
    float timeElapsed;

    NSString *ftfAccountStatusKey    = @"ftfAccountStatus";
    NSString *ftfAccountStatus    = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setValue:@"CHECKING" forKey:ftfAccountStatusKey];

    // Setup and Run the NSURLSessionTask
    [self createFTFAccount:[self userid]];

    // Hold it here until the SessionTask completion handler updates the _accountCreationStatus
    // Or the process takes too long (possible connection error)

    while ((!_accountCreationStatus) && (timeElapsed < 5.0)) {
        NSDate *currentDate = [[NSDate alloc] init];     
        timeElapsed = [currentDate timeIntervalSinceDate:startDate];    
    }
    if ([_accountCreationStatus isEqualToString:@"CONNECTION PROBLEM"] || !_accountCreationStatus) [self cancel];

    if ([self isCancelled]) {
        NSLog(@"DEBUG FTFCreateAccount Cancelled" );
        NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
        [userDefaults setValue:@"ERROR" forKey:ftfAccountStatusKey];            
    }    
}

W następnych danych postów NSOperation:

@implementation FTFPostRoute
{
    NSString *_routePostStatus;
}

- (void)main {

    NSDate *startDate = [[NSDate alloc] init];
    float timeElapsed;
    NSString *ftfAccountStatusKey    = @"ftfAccountStatus";
    NSString *ftfAccountStatus    = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];

    if ([ftfAccountStatus isEqualToString:@"ERROR"])
    {
        // There was a ERROR in creating / accessing the user account.  Cancel the post
        [self cancel];
    } else
    {
        // Call method to setup and run json post

        // Hold it here until a reply comes back from the operation
        while ((!_routePostStatus) && (timeElapsed < 3)) {
            NSDate *currentDate = [[NSDate alloc] init];          
            timeElapsed = [currentDate timeIntervalSinceDate:startDate];                
            NSLog(@"FTFPostRoute time elapsed: %f", timeElapsed);                
        }

    }


    if ([self isCancelled]) {
        NSLog(@"FTFPostRoute operation cancelled");
    }
}

0
2017-12-30 12:09



Lubię prostotę twojego podejścia, ale "trzymanie" blokuje niepotrzebny procesor. Czy zmieniłeś później swoje podejście? - katit
@greentor Pytanie dotyczy kolejki przy użyciu NSURLSessionTasks, które nie rób dziedzicz z NSOperation. Funkcja NSOperationQueue będzie działać tylko z obiektami NSOperation lub obiektami, które są podklasą NSOperation. Ktoś mnie poprawi, jeśli się mylę? - skålfyfan
@katit Zgadzam się, trzymanie bloków to zły pomysł. Moje podejście polega teraz na tym, aby moduł obsługi zakończenia wywoływał inną operację lub wysyłał delegowany komunikat na podstawie odpowiedzi json z serwera. Operacjami zarządza klasa kontrolera, która zawiera funkcję limitu czasu, która anuluje połączenie, jeśli trwa to zbyt długo. Używam też silnika aplikacji Google jako serwera, więc była to okazja do przeprojektowania połączeń. O wiele bardziej złożona niż oryginalna implementacja NSOperation, ale wymagana przez zwiększoną złożoność aplikacji. - greentor


#import "SessionTaskQueue.h"

@interface SessionTaskQueue ()

@property (nonatomic, strong) NSMutableArray * sessionTasks;
@property (nonatomic, strong) NSURLSessionTask * currentTask;

@end

@implementation SessionTaskQueue

- (instancetype)init {

    self = [super init];
    if (self) {

        self.sessionTasks = [[NSMutableArray alloc] initWithCapacity:15];

    }
    return self;

}

- (void)addSessionTask:(NSURLSessionTask *)sessionTask {

    [self.sessionTasks addObject:sessionTask];
    [self resume];

}

// call in the completion block of the sessionTask
- (void)sessionTaskFinished:(NSURLSessionTask *)sessionTask {

    self.currentTask = nil;
    [self resume];

}

- (void)resume {

    if (self.currentTask) {
        return;
    }

    self.currentTask = [self.sessionTasks firstObject];
    if (self.currentTask) {
        [self.sessionTasks removeObjectAtIndex:0];
        [self.currentTask resume];
    }

}

@end

i używać w ten sposób

    __block __weak NSURLSessionTask * wsessionTask;
    use_wself();

    wsessionTask = [[CommonServices shared] doSomeStuffWithCompletion:^(NSError * _Nullable error) {
        use_sself();

        [self.sessionTaskQueue sessionTaskFinished:wsessionTask];

        ...

    }];

    [self.sessionTaskQueue addSessionTask:wsessionTask];

0
2018-06-08 16:47