信号量等待导致 AFNetworking 阻塞?

Posted

技术标签:

【中文标题】信号量等待导致 AFNetworking 阻塞?【英文标题】:Semaphore wait causing a block of AFNetworking? 【发布时间】:2014-05-09 14:47:10 【问题描述】:

我正在尝试使用单元测试来测试网络。在我的测试之外调用时, loginWithUserName:password: 方法正确失败并调用外部失败块。从我的测试方法中调用时,永远不会调用失败块(成功块也不会)。

我认为也许我的信号量等待导致网络也等待,但我不这么认为,因为它位于不同的线程上。我希望它在不同的线程上,所以我可以进行异步调用。我可以修复它以使其正常工作吗?我应该使用不同的技术吗?

我的测试方法设置如下:

typedef void (^CompleteBlock)();

- (void)testLogin 
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);

    [self callLoginWithCompletion:^
        XCTAssertTrue(true, @"login Complete"); // expand on this when I get basic premise working
        NSLog(@"asserted true");
        dispatch_semaphore_signal(sema);
    ];

    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);


- (void)callLoginWithCompletion:(CompleteBlock)completeBlock 
    NSLog(@"login method called");
    [[NSNotificationCenter defaultCenter] addObserverForName:kLoginComplete
                                                      object:nil
                                                       queue:[NSOperationQueue mainQueue]
                                                  usingBlock:^(NSNotification *note) 
                                                      completeBlock();
                                                  ];

    [Network loginWithUserName:@"dummyUser" password:@"dummyPassword"];

我的登录方法如下所示: 静态 AFNetworkReachabilityManager *_reachabilityManager;

+ (AFHTTPRequestOperationManager *)gatewayClient 
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^
        _gatewayClient = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:[NSURL URLWithString:kGatewayBaseURL]];
        _gatewayClient.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/plain"];
    );

    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
    securityPolicy.allowInvalidCertificates = YES;
    [AFHTTPRequestOperationManager manager].securityPolicy = securityPolicy;

    return _gatewayClient;


+ (NSString *)baseURLForType:(NSString *)type method:(NSString *)method 
    return [NSString stringWithFormat:@"api/%@/%@", type, method];


+ (void)loginWithUserName:(NSString *)userName password:(NSString *)password 
    [Network.gatewayClient
     GET:[self baseURLForType:@"auth"
                       method:@"getpubtoken"]
     parameters:nil
     success:^(AFHTTPRequestOperation *operation, id responseObject) 
         _authToken = responseObject;

         NSDictionary *parameters = @
                                      @"UserId": userName
                                      , @"Password": password
                                      , kApiVersion: kApiVersion
                                      , kApiKey: kApiKeyValue
                                      , kAuthToken: _authToken
                                      , kFundraisingPlatform: @(Blackbaud)
                                      ;

         [Network.gatewayClient
          POST:[self baseURLForType:@"auth"
                             method:@"loginfundraiser"]
          parameters:parameters
          success:^(AFHTTPRequestOperation *operation, id responseObject) 
              NSDictionary *responseDict = (NSDictionary *)responseObject;
              NSDictionary *userInfo = nil;

              _authToken = responseDict[kAuthToken];

              if ([responseDict[@"Successful"] boolValue]) 
                  userInfo = @ kAuthToken: responseObject[kAuthToken] ;
               else 
                  userInfo = @ @"error": [[NSError alloc] initWithDomain:@"Authorization"
                                                                     code:-1000
                                                                 userInfo:@ @"message": responseDict[@"ExtendedMessages"] ] ;
              

              [[NSNotificationCenter defaultCenter] postNotificationName:kLoginComplete
                                                                  object:nil
                                                                userInfo:userInfo];
          

          failure:^(AFHTTPRequestOperation *operation, NSError *error) 
              NSDictionary *userInfo = @ @"error": error ;
              [[NSNotificationCenter defaultCenter] postNotificationName:kLoginComplete
                                                                  object:nil
                                                                userInfo:userInfo];
          ];
     

     failure:^(AFHTTPRequestOperation *operation, NSError *error) 
         NSDictionary *userInfo = @ @"error": error ;
         [[NSNotificationCenter defaultCenter] postNotificationName:kLoginComplete
                                                             object:nil
                                                           userInfo:userInfo];
     ];

【问题讨论】:

AFNetworking 将其完成块调度到主队列,所以如果你用信号量阻塞主线程,完成块将死锁。 在这种情况下,在登录完成后发布通知会是一种更好的模式,并且所有需要执行进一步程序的类都订阅以接收该通知,您可以继续无需使用信号量。 在网络登录方法的成功/失败块中,我发送通知。问题是测试,在收到网络通知之前如何暂停测试?我可以告诉 AFNetworking 使用不同的队列来阻止呼叫吗? 【参考方案1】:

好的...主要问题是 AFNetworking 的队列并且通知在主线程上。信号量阻塞了主线程,所以两个响应都被阻塞了。

为了测试,必须指定一个新的 NSOperationQueue 来代替 mainQueue。 对于网络类,必须指定一个新的完成队列。似乎没有办法在这些方法上设置默认的完成队列。已就此提出问题。

新的测试代码和网络子集如下所示。

@implementation NetworkTests

NSOperationQueue * testOperationQueue;

- (void)setUp 
    [super setUp];
    testOperationQueue = [[NSOperationQueue alloc] init];


- (void)testLogin 
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);

    [[NSNotificationCenter defaultCenter] addObserverForName:kLoginComplete
                                                      object:nil
                                                       queue:testOperationQueue
                                                  usingBlock:^(NSNotification *note) 
                                                      XCTAssertTrue(true, @"login Complete"); // expand on this when I get basic premise working
                                                      NSLog(@"asserted true");
                                                      dispatch_semaphore_signal(sema);
                                                  ];


    [Network loginWithUserName:@"testname" password:@"password"];
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);


@end

对于网络,它有这个:

static dispatch_queue_t completionQueue; // initialized when the _gatewayClient object is created

+ (void)loginWithUserName:(NSString *)userName password:(NSString *)password 
    AFHTTPRequestOperation *outerOperation =
    [Network.gatewayClient
     GET:[self baseURLForType:@"auth"
                       method:@"getpubtoken"]
     parameters:nil
     success:^(AFHTTPRequestOperation *operation, id responseObject) 
         // do stuff here
     

     failure:^(AFHTTPRequestOperation *operation, NSError *error) 
         NSDictionary *userInfo = @ @"error": error ;
         [[NSNotificationCenter defaultCenter] postNotificationName:kLoginComplete
                                                             object:nil
                                                           userInfo:userInfo];
     ];

    outerOperation.completionQueue = completionQueue;

【讨论】:

【参考方案2】:

我知道现在回答为时已晚。对于那些来到这个帖子的人来说 afnetwork manager 有一个完成队列,它的默认值是 main queue 。 您可以将其更改为另一个队列,如后台队列以避免锁定 [AFHTTPSessionManager manager].completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

【讨论】:

以上是关于信号量等待导致 AFNetworking 阻塞?的主要内容,如果未能解决你的问题,请参考以下文章

等待来自父母的信号并再次完成工作和阻塞

五种IO模型

Linux下的五种IO模型

iOS 利用runloop阻塞主线程

IPC方法-信号

进程的阻塞和挂起的区别