如何使用AFNetworking设置超时时间

80

我的项目正在使用AFNetworking。

https://github.com/AFNetworking/AFNetworking

我该如何降低超时时间?目前在没有网络连接时,失败块没有被触发的时间感觉大约是2分钟,太长了...


2
我强烈反对任何试图覆盖超时间隔的解决方案,特别是那些使用 performSelector:afterDelay:... 手动取消现有操作的解决方案。请查看我的答案以获取更多详细信息。 - mattt
9个回答

110

改变超时时间几乎肯定不是解决您描述的问题的最佳方法。相反,似乎你真正想要的是HTTP客户端处理网络连接不可达的情况,对吗?

AFHTTPClient已经内置了一个机制,让你知道何时失去互联网连接,-setReachabilityStatusChangeBlock:

在缓慢的网络上请求可能需要很长时间。最好信任 iOS 知道如何处理缓慢的连接,并区分这种情况和根本没有连接的情况。


以下是我认为为什么本文提到的其他方法应该避免的原因:

  • 在开始之前,请求可以被取消。排队请求不能保证它们实际开始的时间。
  • 超时时间不应该取消长时间运行的请求,尤其是 POST 请求。想象一下,如果您正在尝试下载或上传大小为 100MB 的视频。如果请求在缓慢的 3G 网络上尽力而为,为什么要在它花费比预期更长的时间时无谓地停止它呢?
  • 在多线程应用程序中使用 performSelector:afterDelay:... 可能会存在危险。这会使自己暴露于晦涩难懂且难以调试的竞态条件。

2
@mattt 虽然我同意您的方法,但我遇到了一些连接问题,我确实需要超时功能。问题在于 - 我正在与一个公开“WiFi热点”的设备进行交互。当连接到这个“WiFi网络”时,在可达性方面,虽然我能够对该设备发出请求,但实际上我没有连接上。另一方面,我也想确定设备本身是否可达。有什么想法吗?谢谢。 - Stavash
已作为问题#1590打开 - Stavash
43
好的,但它没有回答如何设置超时的问题。 - Max MacLeod
3
在AFNetworking参考文档中,“网络可达性是一种诊断工具,可用于了解请求失败的原因。不应将其用于确定是否发出请求。”在“setReachabilityStatusChangeBlock”部分。因此,我认为“setReachabilityStatusChangeBlock”不是解决方案... - LKM
1
我理解为什么我们应该三思而后手动设置超时,但是这个被接受的答案并没有回答问题。我的应用程序需要尽快知道互联网连接何时丢失。使用AFNetworking,ReachabilityManager无法检测设备连接到WiFi热点,但热点本身失去互联网的情况(通常需要几分钟才能检测到)。因此,在这种情况下,使用超时约为5秒的请求似乎是最佳选择。除非有我不知道的东西? - Tanner Semerad
显示剩余2条评论

44

我强烈建议查看mattt在上面的回答-尽管这个答案通常不会遇到他提到的问题,但对于原始帖子的问题,检查可达性更加合适。

然而,如果你仍然想设置超时(不涉及所有与performSelector:afterDelay:等相关的问题),那么Lego提到的拉请求描述了一种方法来执行此操作,你只需执行以下操作:

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

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

但是请看@KCHarwood提到的警告,似乎苹果不允许更改POST请求的超时时间(这在iOS 6及以上版本中已经修复)。

正如@ChrisopherPickslay指出的那样,这不是一个总超时时间,而是接收或发送数据之间的超时时间。我不知道有什么合理的方法来实现总体超时。Apple的setTimeoutInterval文档说:

超时时间,以秒为单位。如果在连接尝试期间请求保持空闲状态的时间超过超时时间,则请求被视为已超时。默认的超时时间为60秒。


是的,我尝试过了,当进行POST请求时它不起作用。 - borisdiakur
7
附注: 这个问题在iOS 6中已得到修复。 - Raptor
2
这并不是你所建议的那样。timeoutInterval 是一个空闲计时器,而不是请求超时。因此,如果在 120 秒内没有接收到任何数据,上述代码才会超时。如果数据缓慢地流入,请求可能会无限期地继续。 - Christopher Pickslay
这是一个很公正的观点,我确实没有说太多关于它是如何工作的 - 我现在已经加入了这些信息,谢谢! - JosephH
AFNetworking 3.0中移除了AFHTTPRequestOperation类。 - AechoLiu

28
你可以通过requestSerializer的setTimeoutInterval方法来设置超时时间间隔。你可以从AFHTTPRequestOperationManager实例中获取requestSerializer。
例如,要进行一个超时为25秒的POST请求:
    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

终于找到了如何通过异步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];
}

我通过让我的服务器睡眠(几秒钟)来测试了这段代码。
如果您需要执行同步POST请求,请不要使用[queue waitUntilAllOperationsAreFinished];。而是使用与异步请求相同的方法,并等待在选择器参数中传递的函数被触发。

18
不不不,请不要在真实应用中使用这个。你的代码实际上是在创建操作时开始一个时间间隔,而不是在它启动时开始。这可能会导致请求在甚至开始之前就被取消。 - mattt
5
请提供一个可运行的代码示例,实际上您所描述的正是我想要发生的:我希望时间间隔在我创建操作的那一刻开始滴答作响。 - borisdiakur
谢谢Lego!我正在使用AFNetworking,但大约有10%的时间我的AFNetworking操作从未调用成功或失败块[服务器在美国,测试用户在中国]。这对我来说是一个大问题,因为我故意阻止UI的某些部分,同时这些请求正在进行中,以防止用户一次发送太多请求。最终,我实现了一个基于此解决方案的版本,通过performselector withdelay将完成块作为参数传递,并确保如果!operation.isFinished,则执行该块-Mattt:感谢AFNetworking! - Corey

7

我认为目前你需要手动打补丁。

我正在子类化AFHTTPClient并更改了

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

通过添加方法

[request setTimeoutInterval:10.0];

AFHTTPClient.m的第236行。

当然,如果可以配置那就太好了,但据我所见目前不可能实现。


7
还有一件需要考虑的事情是,苹果公司会覆盖POST请求的超时时间。这个时间大约是4分钟左右,自动设定,不可更改。 - kcharwood
我也看到了。为什么会这样?让用户等待4分钟后连接失败并不好。 - slatvick
2
补充@KCHarwood的回答。截至iOS 6,苹果不会覆盖帖子的超时时间。这在iOS 6中已经得到修复。 - ADAM

5

根据其他人的答案和相关项目问题上@mattt的建议,如果您正在子类化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

已经测试通过iOS 6。


0

这里有两个不同的“超时”定义意义。

作为timeoutInterval的超时

当请求在任意时间间隔内变得空闲(没有更多传输)时,您希望放弃该请求。例如:您将timeoutInterval设置为10秒,您在12:00:00开始请求,它可能会传输一些数据直到12:00:23,然后连接将在12:00:33超时。几乎所有答案都涵盖了这种情况(包括JosephH、Mostafa Abdellateef、Cornelius和Gurpartap Singh)。

作为timeoutDeadline的超时

当请求达到任意后期截止日期时,您希望放弃该请求。例如:您将deadline设置为未来的10秒,您在12:00:00开始请求,它可能会尝试传输一些数据直到12:00:23,但连接将在12:00:10之前超时。borisdiakur涵盖了这种情况。

我想展示如何在Swift(3和4)中实现此deadline,适用于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()
}

为了提供一个可测试的示例,此代码应该打印“失败”而不是“成功”,因为在未来的0.0秒内立即超时。
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()
}

0

我们不能用像这样的计时器来做吗:

在 .h 文件中

{
NSInteger time;
AFJSONRequestOperation *operation;
}

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

-2

同意Matt的观点,你不应该尝试更改timeoutInterval。但是你也不应该依赖可达性检查来决定是否要建立连接,直到你尝试过才知道。

正如苹果文档所述:

通常情况下,你不应该使用短的超时时间间隔,而应该提供一种简单的方法让用户取消长时间运行的操作。有关更多信息,请阅读“为真实世界的网络设计”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接