AFNetworking-2 waitUntilFinished not working

2019-02-03 15:16发布

问题:

I know there is another similar question, but it's for an older version of AFNetworking, and doesn't really answer it anyway.

I have the following code:

AFHTTPRequestOperationManager* manager = [AFHTTPRequestOperationManager manager];
manager.securityPolicy.allowInvalidCertificates = YES;
manager.requestSerializer = [AFJSONRequestSerializer serializer];
[manager.requestSerializer setAuthorizationHeaderFieldWithUsername: currentUser() password: currentPassword()];
__block NSDictionary* response = nil;
AFHTTPRequestOperation* operation = [manager
    GET: @"https://10.20.30.40:8765/foobar"
    parameters: [NSDictionary dictionary]
    success:^(AFHTTPRequestOperation* operation, id responseObject){
        response = responseObject;
        NSLog(@"response (block): %@", response);
    }
    failure:^(AFHTTPRequestOperation* operation, NSError* error){
        NSLog(@"Error: %@", error);}
];
[operation waitUntilFinished];
NSLog(@"response: %@", response);
...

If I run this, what I'll see in my log is:

2013-12-09 09:26:20.105 myValve[409:60b] response: (null)
2013-12-09 09:26:20.202 myValve[409:60b] response (block): {
    F00005 = "";
    F00008 = "";
    F00013 = "";
}

The NSLog that is after the waitUntilFinished fires first. I expected it to fire second. What am I missing?

回答1:

A couple of thoughts:

  1. The issue is that waitUntilFinished will wait for the core network operation to complete, but it will not wait for the success or failure completion blocks. If you want to wait for the completion blocks, you can use a semaphore:

    __block NSDictionary* response = nil;
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    AFHTTPRequestOperation* operation = [manager GET: @"https://10.20.30.40:8765/foobar"
                                          parameters: [NSDictionary dictionary]
                                             success:^(AFHTTPRequestOperation* operation, id responseObject){
                                                 response = responseObject;
                                                 NSLog(@"response (block): %@", response);
                                                 dispatch_semaphore_signal(semaphore);
                                             }
                                             failure:^(AFHTTPRequestOperation* operation, NSError* error){
                                                 NSLog(@"Error: %@", error);
                                                 dispatch_semaphore_signal(semaphore);
                                             }];
    
    NSLog(@"waiting");
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // [operation waitUntilFinished];
    NSLog(@"response: %@", response);
    

    You can, alternatively, wrap this in your own concurrent NSOperation subclass, posting isFinished in the AFHTTPRequestOperation completion blocks, eliminating the semaphore in the process.

    Note, make sure to specify completionQueue if doing semaphores on the main queue because, in the absence of that, AFNetworking defaults to dispatching completion handlers to the main queue and you can deadlock.

  2. As an aside, you should never block the main queue (poor UX, your app could be killed by watchdog process, etc.), so if you're doing this from the main queue, I'd discourage the use of either waitUntilFinished or the semaphore. It's better to just initiate whatever you need from within the completion blocks, letting the main queue continue execution while this asynchronous network operation is in progress, e.g.:

    [activityIndicatorView startAnimating];
    
    AFHTTPRequestOperation* operation = [manager GET: @"https://10.20.30.40:8765/foobar"
                                          parameters: [NSDictionary dictionary]
                                             success:^(AFHTTPRequestOperation* operation, id responseObject){
    
                                                 // do whatever you want with `responseObject` here
    
                                                 // now update the UI, e.g.:
    
                                                 [activityIndicatorView stopAnimating];
                                                 [self.tableView reloadData];
                                             }
                                             failure:^(AFHTTPRequestOperation* operation, NSError* error){
    
                                                 // put your error handling here
    
                                                 // now update the UI, e.g.:
    
                                                 [activityIndicatorView stopAnimating];
                                             }];
    
    // NSLog(@"waiting");
    // dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // // [operation waitUntilFinished];
    // NSLog(@"response: %@", response);
    

It sounds like you want to let your model let the UI do any necessary updates when the model object is done doing its updates. So, you can use your own block parameters so that the view controller can tell the model object what to do when its done (instead of using waitUntilFinished or semaphore to make the network operation block the main queue). For example, let's assume your model had some method like this:

- (void)updateModelWithSuccess:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
    AFHTTPRequestOperationManager* manager = [AFHTTPRequestOperationManager manager];
    manager.securityPolicy.allowInvalidCertificates = YES;
    manager.requestSerializer = [AFJSONRequestSerializer serializer];
    [manager.requestSerializer setAuthorizationHeaderFieldWithUsername: currentUser() password: currentPassword()];
    AFHTTPRequestOperation* operation = [manager GET: @"https://10.20.30.40:8765/foobar"
                                          parameters: [NSDictionary dictionary]
                                             success:^(AFHTTPRequestOperation* operation, id responseObject){

                                                 // do your model update here

                                                 // then call the success block passed to this method (if any),
                                                 // for example to update the UI

                                                 if (success)
                                                     success();
                                             }
                                             failure:^(AFHTTPRequestOperation* operation, NSError* error){
                                                 NSLog(@"Error: %@", error);

                                                 // if caller provided a failure block, call that

                                                 if (failure)
                                                     failure(error);
                                             }];
}

Then your view controller can do something like:

[modelObject updateModelWithSuccess:^{
    // specify UI updates to perform upon success, e.g.
    // stop activity indicator view, reload table, etc.
} failure:^(NSError *error){
    // specify any UI updates to perform upon failure
}]

Bottom line, your code can use the same style of completion blocks that AFNetworking uses. If you want the model to pass information back, you can add additional parameters to the completion blocks, themselves, but I presume the above illustrates the basic idea.