Jak ustawić limit czasu w AFNetworking

79

Mój projekt korzysta z AFNetworking.

https://github.com/AFNetworking/AFNetworking

Jak skrócić limit czasu? W przypadku braku połączenia z Internetem blokada awarii nie jest wyzwalana przez około 2 minuty. Czekaj zbyt długo ...

jennas
źródło
2
Zdecydowanie odradzam wszelkie rozwiązania, które próbują zastąpić odstępy czasu, szczególnie te, które używają performSelector:afterDelay:...do ręcznego anulowania istniejących operacji. Zobacz moją odpowiedź, aby uzyskać więcej informacji.
mattt

Odpowiedzi:

110

Zmiana limitu czasu prawie na pewno nie jest najlepszym rozwiązaniem opisywanego problemu. Zamiast tego wydaje się, że tak naprawdę chcesz, aby klient HTTP poradził sobie z utratą dostępu do sieci, nie?

AFHTTPClientjuż ma wbudowany mechanizm poinformować, gdy internet jest stracone, -setReachabilityStatusChangeBlock:.

Żądania mogą zająć dużo czasu w powolnych sieciach. Lepiej zaufać systemowi iOS, aby wiedzieć, jak radzić sobie z powolnymi połączeniami i odróżnić to od braku połączenia.


Aby rozwinąć moje rozumowanie, dlaczego należy unikać innych podejść wspomnianych w tym wątku, oto kilka myśli:

  • Żądania można anulować, zanim jeszcze zostaną rozpoczęte. Umieszczenie żądania w kolejce nie gwarantuje, kiedy faktycznie się rozpocznie.
  • Limity czasu nie powinny anulować długotrwałych żądań - zwłaszcza POST. Wyobraź sobie, że próbujesz pobrać lub przesłać plik wideo o rozmiarze 100 MB. Jeśli żądanie przebiega najlepiej, jak to możliwe w wolnej sieci 3G, po co miałbyś je niepotrzebnie zatrzymywać, jeśli trwa to trochę dłużej niż oczekiwano?
  • Takie działanie performSelector:afterDelay:...może być niebezpieczne w aplikacjach wielowątkowych. To otwiera się na niejasne i trudne do debugowania warunki wyścigu.
mattt
źródło
Wydaje mi się, że to dlatego, że pomimo tytułu pytanie brzmi, jak jak najszybciej zawieść w przypadkach „braku internetu”. Poprawnym sposobem na to jest użycie osiągalności. Korzystanie z limitów czasu albo nie działa, albo ma duże szanse na wprowadzenie trudnych do zauważenia / naprawienia błędów.
Mihai Timar
2
@mattt, chociaż zgadzam się z twoim podejściem, zmagam się z problemem z łącznością, w którym naprawdę potrzebuję funkcji limitu czasu. Rzecz w tym, że wchodzę w interakcję z urządzeniem, które ujawnia „hotspot WiFi”. Po podłączeniu do tej „sieci Wi-Fi” pod względem osiągalności nie jestem połączony, chociaż mogę wysyłać żądania do urządzenia. Z drugiej strony chcę mieć możliwość określenia, czy samo urządzenie jest osiągalne. jakieś pomysły? Dzięki
Stavash
42
dobre punkty, ale nie odpowiada na pytanie, jak ustawić limit czasu
Max MacLeod
3
W dokumencie AFNetworking Reference: „Osiągalność sieci jest narzędziem diagnostycznym, którego można użyć do zrozumienia, dlaczego żądanie mogło się nie powieść. w sekcji „setReachabilityStatusChangeBlock”. Więc myślę, że 'setReachabilityStatusChangeBlock' nie jest rozwiązaniem ...
LKM
1
Rozumiem, dlaczego powinniśmy dwa razy pomyśleć o ręcznym ustawieniu limitu czasu, ale ta zaakceptowana odpowiedź nie odpowiada na pytanie. Moja aplikacja musi jak najszybciej wiedzieć o utracie połączenia internetowego. Korzystając z AFNetworking, ReachabilityManager nie wykrywa przypadku, gdy urządzenie jest podłączone do hotspotu Wi-Fi, ale sam hotspot traci połączenie z Internetem (jego wykrycie często zajmuje kilka minut). Dlatego wysłanie żądania do Google z czasem oczekiwania wynoszącym ~ 5 sekund wydaje się być najlepszą opcją w tym scenariuszu. Chyba że jest coś, czego mi brakuje?
Tanner Semerad
44

Zdecydowanie polecam przyjrzenie się powyższej odpowiedzi Mattta - chociaż ta odpowiedź nie jest obarczona problemami, o których wspomina w ogóle, w przypadku oryginalnego pytania dotyczącego plakatów sprawdzenie osiągalności jest znacznie lepszym rozwiązaniem.

Jeśli jednak nadal chcesz ustawić limit czasu (bez wszystkich problemów związanych z performSelector:afterDelay:itp., Wówczas żądanie ściągnięcia Lego wspomina o sposobie zrobienia tego jako jeden z komentarzy, po prostu wykonaj:

NSMutableURLRequest *request = [client requestWithMethod:@"GET" path:@"/" parameters:nil];
[request setTimeoutInterval:120];

AFHTTPRequestOperation *operation = [client HTTPRequestOperationWithRequest:request success:^{...} failure:^{...}];
[client enqueueHTTPRequestOperation:operation];

ale zobacz zastrzeżenie @KCHarwood wspomina, że ​​wygląda na to, że Apple nie zezwala na zmianę tego dla żądań POST (co jest naprawione w iOS 6 i nowszych).

Jak wskazuje @ChrisopherPickslay, nie jest to ogólny limit czasu, jest to limit czasu między otrzymaniem (lub wysłaniem danych). Nie znam żadnego sposobu na rozsądne przekroczenie limitu czasu. Dokumentacja Apple dotycząca setTimeoutInterval mówi:

Limit czasu w sekundach. Jeśli podczas próby połączenia żądanie pozostaje bezczynne przez czas dłuższy niż limit czasu, uważa się, że upłynął limit czasu żądania. Domyślny limit czasu to 60 sekund.

JosephH
źródło
Tak, wypróbowałem to i nie działa, gdy wykonuję żądanie POST.
borisdiakur
7
Uwaga dodatkowa: Apple naprawił to w iOS 6
Raptor
2
To nie robi tego, co sugerujesz. timeoutIntervaljest licznikiem czasu bezczynności, a nie limitem czasu żądania. Więc musiałbyś otrzymywać żadne dane przez 120 sekund, aby powyższy kod wygasł. Jeśli dane powoli napływają, żądanie może trwać w nieskończoność.
Christopher Pickslay
To uczciwa uwaga, że ​​tak naprawdę nie powiedziałem wiele o tym, jak to działa - mam nadzieję, że dodałem te informacje teraz, dzięki!
JosephH
26

Limit czasu można ustawić za pomocą metody requestSerializer setTimeoutInterval. RequestSerializer można pobrać z instancji AFHTTPRequestOperationManager.

Na przykład, aby wysłać żądanie wpisu z limitem czasu 25 sekund:

    NSDictionary *params = @{@"par1": @"value1",
                         @"par2": @"value2"};

    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    [manager.requestSerializer setTimeoutInterval:25];  //Time out after 25 seconds

    [manager POST:@"URL" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {

    //Success call back bock
    NSLog(@"Request completed with response: %@", responseObject);


    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
     //Failure callback block. This block may be called due to time out or any other failure reason
    }];
Mostafa Abdellateef
źródło
7

Myślę, że w tej chwili musisz to załatać ręcznie.

Podklasuję AFHTTPClient i zmieniłem

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters

metoda poprzez dodanie

[request setTimeoutInterval:10.0];

w wierszu AFHTTPClient.m 236. Oczywiście byłoby dobrze, gdyby można to skonfigurować, ale z tego, co widzę, w tej chwili nie jest to możliwe.

Cornelius
źródło
7
Inną rzeczą do rozważenia jest to, że Apple zastępuje limit czasu dla POST. Myślę, że automatycznie jakieś 4 minuty i NIE MOŻESZ tego zmienić.
kcharwood
Ja też to widzę. Dlaczego tak się dzieje? Nie jest dobrze zmuszać użytkownika do czekania 4 minuty przed zerwaniem połączenia.
slatvick,
2
Aby dodać do odpowiedzi @KCHarwood. Od iOS 6 Apple nie zastępuje limitu czasu posta. Zostało to naprawione w iOS 6.
ADAM
7

Wreszcie dowiedziałem się, jak to zrobić za pomocą asynchronicznego żądania POST:

- (void)timeout:(NSDictionary*)dict {
    NDLog(@"timeout");
    AFHTTPRequestOperation *operation = [dict objectForKey:@"operation"];
    if (operation) {
        [operation cancel];
    }
    [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
    [self perform:[[dict objectForKey:@"selector"] pointerValue] on:[dict objectForKey:@"object"] with:nil];
}

- (void)perform:(SEL)selector on:(id)target with:(id)object {
    if (target && [target respondsToSelector:selector]) {
        [target performSelector:selector withObject:object];
    }
}

- (void)doStuffAndNotifyObject:(id)object withSelector:(SEL)selector {
    // AFHTTPRequestOperation asynchronous with selector                
    NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:
                            @"doStuff", @"task",
                            nil];

    AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:baseURL]];

    NSMutableURLRequest *request = [httpClient requestWithMethod:@"POST" path:requestURL parameters:params];
    [httpClient release];

    AFHTTPRequestOperation *operation = [[[AFHTTPRequestOperation alloc] initWithRequest:request] autorelease];

    NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
                          operation, @"operation", 
                          object, @"object", 
                          [NSValue valueWithPointer:selector], @"selector", 
                          nil];
    [self performSelector:@selector(timeout:) withObject:dict afterDelay:timeout];

    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {            
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(timeout:) object:dict];
        [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
        [self perform:selector on:object with:[operation responseString]];
    }
    failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NDLog(@"fail! \nerror: %@", [error localizedDescription]);
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(timeout:) object:dict];
        [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
        [self perform:selector on:object with:nil];
    }];

    NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
    [[AFNetworkActivityIndicatorManager sharedManager] incrementActivityCount];
    [queue addOperation:operation];
}

Przetestowałem ten kod, pozwalając mojemu serwerowi sleep(aFewSeconds).

Jeśli chcesz wykonać synchroniczne żądanie POST, NIE używaj [queue waitUntilAllOperationsAreFinished];. Zamiast tego użyj tego samego podejścia, co w przypadku żądania asynchronicznego i poczekaj na wyzwolenie funkcji, którą przekazujesz w argumencie selektora.

borisdiakur
źródło
18
Nie nie nie nie nie, proszę nie używać tego w prawdziwej aplikacji. To, co faktycznie robi twój kod, to anulowanie żądania po przedziale czasu, który rozpoczyna się w momencie utworzenia operacji, a nie po jej uruchomieniu . Może to spowodować anulowanie żądań jeszcze przed ich rozpoczęciem.
mattt
5
@mattt Podaj przykładowy kod, który działa. Właściwie to, co opisujesz, jest dokładnie tym, co chcę, aby się wydarzyło: chcę, aby przedział czasu zaczął odliczać w momencie, gdy tworzę operację.
borisdiakur
Dzięki, Lego! Używam AFNetworking i losowo około 10% czasu, w którym moje operacje z AFNetworking nigdy nie wywołują bloków sukcesu lub awarii [serwer jest w USA, użytkownik testowy jest w Chinach]. Jest to dla mnie duży problem, ponieważ celowo blokuję części interfejsu użytkownika, gdy te żądania są w toku, aby uniemożliwić użytkownikowi wysyłanie zbyt wielu żądań jednocześnie. Ostatecznie zaimplementowałem wersję opartą na tym rozwiązaniu, która przekazuje blok uzupełniania jako parametr za pośrednictwem performselektora withdelay i upewnia się, że blok jest wykonywany if! Operation.isFinished - Mattt: Dziękuję za AFNetworking!
Corey,
5

W oparciu o odpowiedzi innych osób i sugestię @ mattt na temat powiązanych problemów z projektem, oto szybki numerek, jeśli tworzysz podklasy AFHTTPClient:

@implementation SomeAPIClient // subclass of AFHTTPClient

// ...

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters {
  NSMutableURLRequest *request = [super requestWithMethod:method path:path parameters:parameters];
  [request setTimeoutInterval:120];
  return request;
}

- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block {
  NSMutableURLRequest *request = [super requestWithMethod:method path:path parameters:parameters];
  [request setTimeoutInterval:120];
  return request;
}

@end

Przetestowano pod kątem działania na iOS 6.

Gurpartap Singh
źródło
0

Czy nie możemy tego zrobić z takim timerem:

W pliku .h

{
NSInteger time;
AFJSONRequestOperation *operation;
}

W pliku .m

-(void)AFNetworkingmethod{

    time = 0;

    NSTtimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(startTimer:) userInfo:nil repeats:YES];
    [timer fire];


    operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
        [self operationDidFinishLoading:JSON];
    } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
        [self operationDidFailWithError:error];
    }];
    [operation setJSONReadingOptions:NSJSONReadingMutableContainers];
    [operation start];
}

-(void)startTimer:(NSTimer *)someTimer{
    if (time == 15&&![operation isFinished]) {
        time = 0;
        [operation invalidate];
        [operation cancel];
        NSLog(@"Timeout");
        return;
    }
    ++time;
}
Ulaş Sancak
źródło
0

Definicja „limitu czasu” ma tutaj dwa różne znaczenia.

Limit czasu jak w timeoutInterval

Chcesz porzucić żądanie, gdy stanie się bezczynne (nie będzie więcej przesyłania) dłużej niż przez dowolny okres czasu. Przykład: ustawisz timeoutInterval10 sekund, zaczynasz żądanie o 12:00:00, może przesłać część danych do 12:00:23, a następnie połączenie wygaśnie o 12:00:33. Ten przypadek jest objęty prawie wszystkimi odpowiedziami tutaj (w tym JosephH, Mostafa Abdellateef, Cornelius i Gurpartap Singh).

Limit czasu jak w timeoutDeadline

Chcesz odrzucić wniosek, gdy nadejdzie termin przypadający w dowolnym momencie. Przykład: ustawiłeś deadlinena 10 sekund w przyszłości, zaczynasz żądanie o 12:00:00, może próbować przesłać niektóre dane do 12:00:23, ale połączenie wygaśnie wcześniej o 12:00:10. Ten przypadek pokrywa borisdiakur.

Chciałbym pokazać, jak wdrożyć ten termin w Swift (3 i 4) dla AFNetworking 3.1.

let sessionManager = AFHTTPSessionManager(baseURL: baseURL)
let request = sessionManager.post(endPoint, parameters: parameters, progress: { ... }, success: { ... }, failure: { ... })
// timeout deadline at 10 seconds in the future
DispatchQueue.global().asyncAfter(deadline: .now() + 10.0) {
    request?.cancel()
}

Aby dać testowalny przykład, ten kod powinien wypisać „niepowodzenie” zamiast „sukces” ze względu na natychmiastowy limit czasu wynoszący 0,0 sekundy w przyszłości:

let sessionManager = AFHTTPSessionManager(baseURL: URL(string: "https://example.com"))
sessionManager.responseSerializer = AFHTTPResponseSerializer()
let request = sessionManager.get("/", parameters: nil, progress: nil, success: { _ in
    print("success")
}, failure: { _ in
    print("failure")
})
// timeout deadline at 0 seconds in the future
DispatchQueue.global().asyncAfter(deadline: .now() + 0.0) {
    request?.cancel()
}
Cœur
źródło
-2

Zgadzam się z Mattem, nie powinieneś próbować zmieniać timeoutInterval. Ale nie powinieneś również polegać na sprawdzaniu osiągalności, aby zdecydować, czy zamierzasz nawiązać połączenie, nie wiesz, dopóki nie spróbujesz.

Jak stwierdzono w dokumencie Apple:

Zasadniczo nie należy używać krótkich przedziałów czasu, a zamiast tego należy zapewnić użytkownikowi łatwy sposób na anulowanie długotrwałej operacji. Aby uzyskać więcej informacji, przeczytaj „Projektowanie sieci w świecie rzeczywistym”.

wątek
źródło