Cómo establecer un tiempo de espera con AFNetworking

79

Mi proyecto utiliza AFNetworking.

https://github.com/AFNetworking/AFNetworking

¿Cómo reduzco el tiempo de espera? Cajero automático sin conexión a Internet, el bloqueo de fallas no se activa durante lo que se siente como aproximadamente 2 minutos. Mucho tiempo ...

Jennas
fuente
2
Recomiendo encarecidamente cualquier solución que intente anular los intervalos de tiempo de espera, en particular los que utilizan performSelector:afterDelay:...para cancelar manualmente las operaciones existentes. Consulte mi respuesta para obtener más detalles.
mattt

Respuestas:

110

Es casi seguro que cambiar el intervalo de tiempo de espera no es la mejor solución al problema que está describiendo. En cambio, parece que lo que realmente desea es que el cliente HTTP maneje la red que se vuelve inalcanzable, ¿no?

AFHTTPClient ya tiene un mecanismo incorporado para avisarle cuando se pierde la conexión a Internet, -setReachabilityStatusChangeBlock: .

Las solicitudes pueden tardar bastante en redes lentas. Es mejor confiar en iOS para saber cómo manejar conexiones lentas y diferenciar entre eso y no tener ninguna conexión.


Para ampliar mi razonamiento sobre por qué se deben evitar otros enfoques mencionados en este hilo, aquí hay algunos pensamientos:

  • Las solicitudes se pueden cancelar incluso antes de que se inicien. Poner en cola una solicitud no garantiza cuándo comienza realmente.
  • Los intervalos de tiempo de espera no deben cancelar las solicitudes de larga ejecución, especialmente POST. Imagínese si estuviera intentando descargar o cargar un video de 100 MB. Si la solicitud se realiza de la mejor manera posible en una red 3G lenta, ¿por qué la detendría innecesariamente si está tardando un poco más de lo esperado?
  • Hacerlo performSelector:afterDelay:...puede ser peligroso en aplicaciones de subprocesos múltiples. Esto se abre a condiciones de carrera oscuras y difíciles de depurar.
mattt
fuente
Creo que es porque la pregunta es, a pesar de su título, acerca de cómo fallar lo más rápido posible en casos de "No Internet". La forma correcta de hacerlo es mediante la accesibilidad. El uso de tiempos de espera no funciona o tiene una alta probabilidad de introducir errores difíciles de notar / corregir.
Mihai Timar
2
@mattt, aunque estoy de acuerdo con su enfoque, estoy luchando con un problema de conectividad en el que realmente necesito la funcionalidad de tiempo de espera. La cuestión es que estoy interactuando con un dispositivo que expone un "punto de acceso WiFi". Cuando estoy conectado a esta "red WiFi", en términos de accesibilidad, no estoy conectado, aunque puedo realizar solicitudes al dispositivo. Por otro lado, quiero poder determinar si el dispositivo en sí es accesible. ¿Alguna idea? Gracias
Stavash
42
buenos puntos pero no responde a la pregunta sobre cómo establecer un tiempo de espera
Max MacLeod
3
En AFNetworking Reference, "La accesibilidad a la red es una herramienta de diagnóstico que se puede usar para comprender por qué una solicitud puede haber fallado. No debe usarse para determinar si se debe realizar una solicitud o no". en la sección de 'setReachabilityStatusChangeBlock'. Así que creo que 'setReachabilityStatusChangeBlock' no es la solución ...
LKM
1
Entiendo por qué deberíamos pensar dos veces antes de establecer manualmente un tiempo de espera, pero esta respuesta aceptada no responde a la pregunta. Mi aplicación necesita saber lo antes posible cuando se pierde la conexión a Internet. Al usar AFNetworking, el ReachabilityManager no detecta el caso en el que el dispositivo está conectado a un punto de acceso Wifi, pero el punto de acceso en sí pierde Internet (a menudo se necesitan minutos para detectarlo). Entonces, hacer una solicitud a Google con un tiempo de espera de ~ 5 segundos parece ser mi mejor opción en ese escenario. ¿A menos que haya algo que me esté perdiendo?
Tanner Semerad
44

Recomiendo encarecidamente mirar la respuesta de mattt anterior, aunque esta respuesta no cae en contra de los problemas que menciona en general, para la pregunta de los carteles originales, verificar la accesibilidad es mucho mejor.

Sin embargo, si aún desea establecer un tiempo de espera (sin todos los problemas inherentes a performSelector:afterDelay:, etc., entonces la solicitud de extracción que Lego menciona describe una forma de hacerlo como uno de los comentarios, simplemente haga:

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

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

pero vea la advertencia @KCHarwood menciona que parece que Apple no permite que esto se cambie para las solicitudes POST (que se corrigió en iOS 6 y versiones posteriores).

Como señala @ChrisopherPickslay, este no es un tiempo de espera general, es un tiempo de espera entre la recepción (o el envío de datos). No tengo conocimiento de ninguna forma sensata de hacer un tiempo de espera general. La documentación de Apple para setTimeoutInterval dice:

El intervalo de tiempo de espera, en segundos. Si durante un intento de conexión la solicitud permanece inactiva durante más tiempo que el intervalo de tiempo de espera, se considera que la solicitud ha expirado. El intervalo de tiempo de espera predeterminado es de 60 segundos.

JosephH
fuente
Sí, lo probé y no funciona cuando hago una solicitud POST.
borisdiakur
7
nota al margen: Apple arregló esto en iOS 6
Raptor
2
Esto no hace lo que sugieres que haga. timeoutIntervales un temporizador inactivo, no un tiempo de espera de solicitud. Por lo tanto, no tendría que recibir ningún dato durante 120 segundos para que el código anterior expire. Si los datos se filtran lentamente, la solicitud podría continuar indefinidamente.
Christopher Pickslay
Es un punto justo que realmente no dije mucho sobre cómo funciona. Espero haber agregado esta información ahora, ¡gracias!
JosephH
26

Puede establecer el intervalo de tiempo de espera a través del método requestSerializer setTimeoutInterval. Puede obtener el requestSerializer de una instancia AFHTTPRequestOperationManager.

Por ejemplo, para hacer una solicitud de publicación con un tiempo de espera de 25 segundos:

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

Creo que tienes que parchear eso manualmente en este momento.

Estoy subclasificando AFHTTPClient y cambié el

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

método agregando

[request setTimeoutInterval:10.0];

en AFHTTPClient.m línea 236. Por supuesto, sería bueno si eso pudiera configurarse, pero por lo que veo, eso no es posible en este momento.

Cornelio
fuente
7
Otra cosa a considerar es que Apple anula el tiempo de espera para una POST. Creo que automáticamente algo así como 4 minutos, y NO PUEDES cambiarlo.
kcharwood
Yo también lo veo. ¿Por que es esto entonces? No es bueno hacer que el usuario espere 4 minutos antes de que falle la conexión.
Slatvick
2
Para agregar a la respuesta de @KCHarwood. A partir de iOS 6, Apple no anula el tiempo de espera de la publicación. Esto se ha corregido en iOS 6.
ADAM
7

Finalmente descubrí cómo hacerlo con una solicitud POST asincrónica:

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

Probé este código dejando que mi servidor sleep(aFewSeconds).

Si necesita hacer una solicitud POST sincrónica, NO use [queue waitUntilAllOperationsAreFinished];. En su lugar, use el mismo enfoque que para la solicitud asincrónica y espere a que se active la función que pasa en el argumento del selector.

borisdiakur
fuente
18
No no no no no, por favor, no use esta en una aplicación real. Lo que realmente hace su código es cancelar la solicitud después de un intervalo de tiempo que comienza cuando se crea la operación, no cuando se inicia . Esto podría hacer que las solicitudes se cancelen incluso antes de comenzar.
mattt
5
@mattt Proporcione un ejemplo de código que funcione. En realidad, lo que describe es exactamente lo que quiero que suceda: quiero que el intervalo de tiempo comience a funcionar justo en el momento en que creo la operación.
borisdiakur
¡Gracias Lego! Estoy usando AFNetworking y, al azar, aproximadamente el 10% del tiempo, mis operaciones desde AFNetworking nunca llaman a los bloques de éxito o falla [el servidor está en EE. UU., El usuario de prueba está en China]. Este es un gran problema para mí, ya que estoy bloqueando intencionalmente partes de la interfaz de usuario mientras esas solicitudes están en curso para evitar que el usuario envíe demasiadas solicitudes a la vez. Finalmente implementé una versión de basada en esta solución que pasa un bloque de finalización como parámetro a través del selector de rendimiento con retraso y se asegura de que el bloque se ejecute si! Operación.isFinished - Mattt: ¡Gracias por AFNetworking!
Corey
5

Según las respuestas de otros y la sugerencia de @ mattt sobre problemas relacionados con el proyecto, aquí hay un resumen rápido si está subclasificando 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

Probado para funcionar en iOS 6.

Gurpartap Singh
fuente
0

¿No podemos hacer esto con un temporizador como este?

En archivo .h

{
NSInteger time;
AFJSONRequestOperation *operation;
}

En archivo .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
fuente
0

Hay dos significados diferentes en la definición de "tiempo de espera" aquí.

Tiempo de espera como en timeoutInterval

Desea descartar una solicitud cuando esté inactiva (no más transferencia) durante más de un intervalo de tiempo arbitrario. Ejemplo: establecestimeoutInterval en 10 segundos, inicia su solicitud a las 12:00:00, puede transferir algunos datos hasta las 12:00:23, luego la conexión expirará a las 12:00:33. Este caso está cubierto por casi todas las respuestas aquí (incluidas JosephH, Mostafa Abdellateef, Cornelius y Gurpartap Singh).

Tiempo de espera como en timeoutDeadline

Desea descartar una solicitud cuando llega a una fecha límite que ocurre arbitrariamente más tarde. Ejemplo: establece deadline10 segundos en el futuro, inicia su solicitud a las 12:00:00, puede intentar transferir algunos datos hasta las 12:00:23, pero la conexión se agotará antes a las 12:00:10. Este caso está cubierto por borisdiakur.

Me gustaría mostrar cómo implementar esta fecha límite en Swift (3 y 4) para 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()
}

Y para dar un ejemplo comprobable, este código debería imprimir "falla" en lugar de "éxito" debido al tiempo de espera inmediato de 0.0 segundos en el futuro:

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
fuente
-2

De acuerdo con Matt, no debería intentar cambiar el timeoutInterval. Pero tampoco debe confiar en la verificación de accesibilidad para decidir el clima en el que realizará la conexión, no lo sabrá hasta que lo intente.

Como se indica en el documento de Apple:

Como regla general, no debe utilizar intervalos de tiempo de espera cortos y, en su lugar, debe proporcionar una manera fácil para que el usuario cancele una operación de larga duración. Para obtener más información, lea "Diseño para redes del mundo real".

guau
fuente