NSOperation and NSURLConnection mystified

2019-02-25 18:55发布

问题:

I am trying to download multiple images from some server using NSOperation and NSOperationQueue. My main question is what is the difference between the code snippet below, and this link http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/, performance wise? I prefer the second solution, because we have a lot more control over the operations, it is a lot cleaner, and if a connection fails you can handle it properly.

If I try to download about 300 images from a server with the code below, my app will have a pretty big delay, and if I launch the app, and go to the home screen immediately, then go back into the app, I will crash, because there wasn't wasn't enough time for the app to become active again. If I uncomment [queue setMaxConcurrentOperationCount:1], the user interface is responsive and it won't crash if it enters the background and returns to the foreground.

But if I implement something similar to the link above, I don't need to worry about setting maxConcurrentOperationCount, the default value is fine. Everything is responsive, no crashing, and it seems like all the queue gets completed faster.

So this brings me to my second question, why does [queue setMaxConcurrentOperationCount:1] have such a big affect in my code below? From the documentation, I thought that leaving the maxConcurrentOperationCount at its default value was fine, and that just tells the queue to decide what the best value should be based on certain factors.

This is my first post on Stack Overflow so hopefully this makes sense, thanks for any help!

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//[queue setMaxConcurrentOperationCount:1];

for(NSURL *URL in URLArray) {
    [queue addOperationWithBlock:^{
        NSHTTPURLResponse *response = nil;
        NSError *error = nil;
        NSData * data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:URL] returningResponse:&response error:&error];

        if(!error && data) {
            [data writeToFile:path atomically:YES];
        }
    }];
}

回答1:

I'm going to tackle these in reverse order. You ask:

So this brings me to my second question, why does [queue setMaxConcurrentOperationCount:1] have such a big affect in my code below? From the documentation, I thought that leaving the maxConcurrentOperationCount at its default value was fine, and that just tells the queue to decide what the best value should be based on certain factors.

With NSURLConnection, you cannot have more than four or five connections downloading concurrently. Thus, if you don't set maxConcurrentOperationCount, the operation queue doesn't know you're dealing with NSURLConnection and therefore when you add 300 NSOperation objects to your queue, the queue will try to start a very large number of them (64-ish, I think) concurrently. But since only 4 or 5 NSURLConnection requests can run concurrently, the rest of them that were started by the queue will wait until one of the four or five possible connections are available, and with so many download requests, it's quite likely that many of them will time out and fail.

By using maxConcurrentOperationCount of 1, that applies a rather heavy-handed solution to this problem, only running one at a time. I'd suggest a compromise, namely a maxConcurrentOperationCount of 4, which enjoys a degree of concurrency (and huge performance gain), but not so many that we risk having connections time out and fail.

Going back to Dave Drubin's NSOperation, his is great improvement over your synchronousRequest wrapped in an operation. Having said that, he's neglected to address a fairly basic feature of concurrent requests, namely cancelation. You should include a check to see if the operation has been canceled, and if so, cancel the connection:

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    if ([self isCancelled]) {
        [connection cancel];
        [self finish];
        return;
    }

    [_data appendData:data];
}

Likewise, when he should be doing that in the start method, too.

- (void)start
{
    // The Apple docs say "Always check for cancellation before launching the task."

    if ([self isCancelled]) {
        [self willChangeValueForKey:@"isFinished"];
        _isFinished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }

    if (![NSThread isMainThread])
    {
        [self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
        return;
    }

    NSLog(@"opeartion for <%@> started.", _url);

    [self willChangeValueForKey:@"isExecuting"];
    _isExecuting = YES;
    [self didChangeValueForKey:@"isExecuting"];

    NSURLRequest * request = [NSURLRequest requestWithURL:_url];
    _connection = [[NSURLConnection alloc] initWithRequest:request
                                                  delegate:self];
    if (_connection == nil)
        [self finish];
}

I might suggest other stylistic improvements to Dave's example, but it's all minor stuff, and I think he got most of the big picture stuff spot on. The failure to check for cancelation was the only obvious big issue that leapt out at me.

Anyway, for discussion of concurrent operations, see the Configuring Operations for Concurrent Execution section of the Concurrency Programming Guide.

Also, when testing huge downloads like these, I'd encourage you to stress test your app with the Network Link Conditioner (available for the Mac/simulator as a download available under "Hardware IO tools" on the "Xcode" - "Open Developer Tool" - "More Developer Tools"; if you enable your iOS device for development, there is also a network link conditioner setting under "General" - "Developer" in the Settings app). A lot of these timeout-related problems don't manifest themselves when we test our apps in our highly optimized scenario of our development environment. It's important to use the network link conditioner to simulate less than ideal, real-world scenarios.



回答2:

I prefer the second solution, because we have a lot more control over the operations

If you refer to your own solution, then actually the opposite is the case:

Due to the synchronous convenience method sendSynchronousRequest: you have basically no way to accomplish more practical requirements, like authentication, better error handling, and customized data handling, and many other features usually required in any app which is not a demo or toy app.

The bummer however is the lack of the ability to cancel operations. You cannot cancel a block operation once it has started. And it's also not clear how you want to cancel the "for loop". So, once the loop is started you cannot stop it.

You may want to search for more sophisticated (and more modern) approaches in the web and on SO.



回答3:

When the queue's MaxConcurrentOperationCount is > 1 there is a good chance that the queue is getting locked and unlocked by the operations completing while you are still adding more jobs to the queue however when you set it to 1, the queue gets more completely full before the jobs start to work off, meaning the looping of the URLArray finishes faster (avoiding being 'deadlocked' constantly).

If I recall the NSOperationQueue api correctly you can "suspend" and "resume" it... I recommend you set it to "suspended" until you are done adding all the jobs, then set it resumed