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 ...


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.

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.


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
    }];

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.


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.


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.


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;
}

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()
}

-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”.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.